237 lines
7.7 KiB
Python
237 lines
7.7 KiB
Python
# type: ignore
|
|
# TODO: Handle typing issues
|
|
import logging
|
|
import json as json_parser
|
|
from collections import defaultdict
|
|
from typing import Iterable, List, Dict, Any
|
|
|
|
from safety.formatter import FormatterAPI
|
|
from safety.models import SafetyEncoder
|
|
from safety.output_utils import get_report_brief_info
|
|
from safety.safety import find_vulnerabilities_fixed
|
|
from safety.util import get_basic_announcements, SafetyContext
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def build_json_report(
|
|
announcements: List[Dict],
|
|
vulnerabilities: List[Dict],
|
|
remediations: Dict[str, Any],
|
|
packages: List[Any],
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Build a JSON report for vulnerabilities, remediations, and packages.
|
|
|
|
Args:
|
|
announcements (List[Dict]): List of announcements.
|
|
vulnerabilities (List[Dict]): List of vulnerabilities.
|
|
remediations (Dict[str, Any]): Remediation data.
|
|
packages (List[Any]): List of packages.
|
|
|
|
Returns:
|
|
Dict[str, Any]: JSON report.
|
|
"""
|
|
vulns_ignored = [vuln.to_dict() for vuln in vulnerabilities if vuln.ignored]
|
|
vulns = [vuln.to_dict() for vuln in vulnerabilities if not vuln.ignored]
|
|
|
|
report = get_report_brief_info(
|
|
as_dict=True,
|
|
report_type=1,
|
|
vulnerabilities_found=len(vulns),
|
|
vulnerabilities_ignored=len(vulns_ignored),
|
|
remediations_recommended=remediations,
|
|
)
|
|
|
|
if "using_sentence" in report:
|
|
del report["using_sentence"]
|
|
|
|
remed = {}
|
|
for k, v in remediations.items():
|
|
if k not in remed:
|
|
remed[k] = {"requirements": v}
|
|
|
|
remed[k]["current_version"] = None
|
|
remed[k]["vulnerabilities_found"] = None
|
|
remed[k]["recommended_version"] = None
|
|
remed[k]["other_recommended_versions"] = []
|
|
remed[k]["more_info_url"] = None
|
|
|
|
return {
|
|
"report_meta": report,
|
|
"scanned_packages": {p.name: p.to_dict(short_version=True) for p in packages},
|
|
"affected_packages": {v.pkg.name: v.pkg.to_dict() for v in vulnerabilities},
|
|
"announcements": [
|
|
{"type": item.get("type"), "message": item.get("message")}
|
|
for item in get_basic_announcements(announcements)
|
|
],
|
|
"vulnerabilities": vulns,
|
|
"ignored_vulnerabilities": vulns_ignored,
|
|
"remediations": remed,
|
|
}
|
|
|
|
|
|
class JsonReport(FormatterAPI):
|
|
"""Json report, for when the output is input for something else"""
|
|
|
|
VERSIONS = ("0.5", "1.1")
|
|
|
|
def __init__(self, version="1.1", **kwargs):
|
|
"""
|
|
Initialize JsonReport with the specified version.
|
|
|
|
Args:
|
|
version (str): Report version.
|
|
"""
|
|
super().__init__(**kwargs)
|
|
self.version: str = version if version in self.VERSIONS else "1.1"
|
|
|
|
def render_vulnerabilities(
|
|
self,
|
|
announcements: List[Dict],
|
|
vulnerabilities: List[Dict],
|
|
remediations: Dict[str, Any],
|
|
full: bool,
|
|
packages: List[Any],
|
|
fixes: Iterable = (),
|
|
) -> str:
|
|
"""
|
|
Render vulnerabilities in JSON format.
|
|
|
|
Args:
|
|
announcements (List[Dict]): List of announcements.
|
|
vulnerabilities (List[Dict]): List of vulnerabilities.
|
|
remediations (Dict[str, Any]): Remediation data.
|
|
full (bool): Flag indicating full output.
|
|
packages (List[Any]): List of packages.
|
|
fixes (Iterable, optional): Iterable of fixes.
|
|
|
|
Returns:
|
|
str: Rendered JSON vulnerabilities report.
|
|
"""
|
|
if self.version == "0.5":
|
|
from safety.formatters.schemas.zero_five import VulnerabilitySchemaV05
|
|
|
|
return json_parser.dumps(
|
|
VulnerabilitySchemaV05().dump(obj=vulnerabilities, many=True), indent=4
|
|
)
|
|
|
|
remediations_recommended = len(remediations.keys())
|
|
LOG.debug(
|
|
"Rendering %s vulnerabilities, %s package remediations with full_report: %s",
|
|
len(vulnerabilities),
|
|
remediations_recommended,
|
|
full,
|
|
)
|
|
|
|
report = build_json_report(
|
|
announcements, vulnerabilities, remediations, packages
|
|
)
|
|
template = self.__render_fixes(report, fixes)
|
|
|
|
return json_parser.dumps(template, indent=4, cls=SafetyEncoder)
|
|
|
|
def render_licenses(self, announcements: List[Dict], licenses: List[Dict]) -> str:
|
|
"""
|
|
Render licenses in JSON format.
|
|
|
|
Args:
|
|
announcements (List[Dict]): List of announcements.
|
|
licenses (List[Dict]): List of licenses.
|
|
|
|
Returns:
|
|
str: Rendered JSON licenses report.
|
|
"""
|
|
unique_license_types = set([lic["license"] for lic in licenses])
|
|
report = get_report_brief_info(
|
|
as_dict=True, report_type=2, licenses_found=len(unique_license_types)
|
|
)
|
|
|
|
template = {
|
|
"report_meta": report,
|
|
"announcements": get_basic_announcements(announcements),
|
|
"licenses": licenses,
|
|
}
|
|
|
|
return json_parser.dumps(template, indent=4)
|
|
|
|
def render_announcements(self, announcements: List[Dict]) -> str:
|
|
"""
|
|
Render announcements in JSON format.
|
|
|
|
Args:
|
|
announcements (List[Dict]): List of announcements.
|
|
|
|
Returns:
|
|
str: Rendered JSON announcements.
|
|
"""
|
|
return json_parser.dumps(
|
|
{"announcements": get_basic_announcements(announcements)}, indent=4
|
|
)
|
|
|
|
def __render_fixes(
|
|
self, scan_template: Dict[str, Any], fixes: Iterable
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Render fixes and update the scan template with remediations information.
|
|
|
|
Args:
|
|
scan_template (Dict[str, Any]): Initial scan template.
|
|
fixes (Iterable): Iterable of fixes.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Updated scan template with remediations.
|
|
"""
|
|
|
|
applied = defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
|
|
skipped = defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
|
|
|
|
fixes_applied = []
|
|
total_applied = 0
|
|
|
|
for fix in fixes:
|
|
if fix.status == "APPLIED":
|
|
total_applied += 1
|
|
applied[fix.applied_at][fix.package][fix.previous_spec] = {
|
|
"previous_version": str(fix.previous_version),
|
|
"previous_spec": str(fix.previous_spec),
|
|
"updated_version": str(fix.updated_version),
|
|
"update_type": str(fix.update_type),
|
|
"fix_type": fix.fix_type,
|
|
}
|
|
fixes_applied.append(fix)
|
|
else:
|
|
skipped[fix.applied_at][fix.package][fix.previous_spec] = {
|
|
"scanned_version": str(fix.previous_version)
|
|
if fix.previous_version
|
|
else None,
|
|
"scanned_spec": str(fix.previous_spec)
|
|
if fix.previous_spec
|
|
else None,
|
|
"skipped_reason": fix.status,
|
|
}
|
|
|
|
vulnerabilities = scan_template.get("vulnerabilities", {})
|
|
remediation_mode = "NON_INTERACTIVE"
|
|
|
|
if SafetyContext().params.get("prompt_mode", False):
|
|
remediation_mode = "INTERACTIVE"
|
|
|
|
scan_template["report_meta"].update(
|
|
{
|
|
"remediations_attempted": len(fixes),
|
|
"remediations_completed": total_applied,
|
|
"remediation_mode": remediation_mode,
|
|
}
|
|
)
|
|
|
|
scan_template["remediations_results"] = {
|
|
"vulnerabilities_fixed": find_vulnerabilities_fixed(
|
|
vulnerabilities, fixes_applied
|
|
),
|
|
"remediations_applied": applied,
|
|
"remediations_skipped": skipped,
|
|
}
|
|
|
|
return scan_template
|