updates
This commit is contained in:
824
Backend/venv/lib/python3.12/site-packages/safety/scan/render.py
Normal file
824
Backend/venv/lib/python3.12/site-packages/safety/scan/render.py
Normal file
@@ -0,0 +1,824 @@
|
||||
# type: ignore
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
import sys
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.padding import Padding
|
||||
from rich.prompt import Prompt
|
||||
from rich.text import Text
|
||||
from safety_schemas.models import (
|
||||
Ecosystem,
|
||||
FileType,
|
||||
IgnoreCodes,
|
||||
PolicyFileModel,
|
||||
PolicySource,
|
||||
ProjectModel,
|
||||
PythonDependency,
|
||||
ReportModel,
|
||||
Vulnerability,
|
||||
)
|
||||
|
||||
from safety import safety
|
||||
from safety.auth.constants import SAFETY_PLATFORM_URL
|
||||
from safety.errors import SafetyException
|
||||
from safety.meta import get_version
|
||||
from safety.output_utils import parse_html
|
||||
from safety.scan.constants import DEFAULT_SPINNER
|
||||
from safety.util import clean_project_id, get_basic_announcements
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def render_header(targets: List[Path], is_system_scan: bool) -> Text:
|
||||
"""
|
||||
Render the header text for the scan.
|
||||
|
||||
Args:
|
||||
targets (List[Path]): List of target paths for the scan.
|
||||
is_system_scan (bool): Indicates if the scan is a system scan.
|
||||
|
||||
Returns:
|
||||
Text: Rendered header text.
|
||||
"""
|
||||
version = get_version()
|
||||
scan_datetime = datetime.datetime.now(datetime.timezone.utc).strftime(
|
||||
"%Y-%m-%d %H:%M:%S %Z"
|
||||
)
|
||||
|
||||
action = f"scanning {', '.join([str(t) for t in targets])}"
|
||||
if is_system_scan:
|
||||
action = "running [bold]system scan[/bold]"
|
||||
|
||||
return Text.from_markup(f"[bold]Safety[/bold] {version} {action}\n{scan_datetime}")
|
||||
|
||||
|
||||
def print_header(console, targets: List[Path], is_system_scan: bool = False) -> None:
|
||||
"""
|
||||
Print the header for the scan.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
targets (List[Path]): List of target paths for the scan.
|
||||
is_system_scan (bool): Indicates if the scan is a system scan.
|
||||
"""
|
||||
console.print(render_header(targets, is_system_scan), markup=True)
|
||||
|
||||
|
||||
def print_announcements(console: Console, ctx: typer.Context):
|
||||
"""
|
||||
Print announcements from Safety.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
ctx (typer.Context): The context of the Typer command.
|
||||
"""
|
||||
colors = {"error": "red", "warning": "yellow", "info": "default"}
|
||||
|
||||
announcements = safety.get_announcements(
|
||||
ctx.obj.auth.client,
|
||||
telemetry=ctx.obj.config.telemetry_enabled,
|
||||
with_telemetry=ctx.obj.telemetry,
|
||||
)
|
||||
basic_announcements = get_basic_announcements(announcements, False)
|
||||
|
||||
if any(basic_announcements):
|
||||
console.print()
|
||||
console.print("[bold]Safety Announcements:[/bold]")
|
||||
console.print()
|
||||
for announcement in announcements:
|
||||
color = colors.get(announcement.get("type", "info"), "default")
|
||||
console.print(f"[{color}]* {announcement.get('message')}[/{color}]")
|
||||
|
||||
|
||||
def print_detected_ecosystems_section(
|
||||
console: Console, file_paths: Dict[str, Set[Path]], include_safety_prjs: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Print detected ecosystems section.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
file_paths (Dict[str, Set[Path]]): Dictionary of file paths by type.
|
||||
include_safety_prjs (bool): Whether to include safety projects.
|
||||
"""
|
||||
detected: Dict[Ecosystem, Dict[FileType, int]] = {}
|
||||
|
||||
for file_type_key, f_paths in file_paths.items():
|
||||
file_type = FileType(file_type_key)
|
||||
if file_type.ecosystem:
|
||||
if file_type.ecosystem not in detected:
|
||||
detected[file_type.ecosystem] = {}
|
||||
detected[file_type.ecosystem][file_type] = len(f_paths)
|
||||
|
||||
for ecosystem, f_type_count in detected.items():
|
||||
if not include_safety_prjs and ecosystem is Ecosystem.SAFETY_PROJECT:
|
||||
continue
|
||||
|
||||
brief = "Found "
|
||||
file_types = []
|
||||
|
||||
for f_type, count in f_type_count.items():
|
||||
file_types.append(f"{count} {f_type.human_name(plural=count > 1)}")
|
||||
|
||||
if len(file_types) > 1:
|
||||
brief += ", ".join(file_types[:-1]) + " and " + file_types[-1]
|
||||
else:
|
||||
brief += file_types[0]
|
||||
|
||||
msg = f"{ecosystem.name.replace('_', ' ').title()} detected. {brief}"
|
||||
|
||||
console.print(msg)
|
||||
|
||||
|
||||
def print_fixes_section(
|
||||
console: Console,
|
||||
requirements_txt_found: bool = False,
|
||||
is_detailed_output: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Print the section on applying fixes.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
requirements_txt_found (bool): Indicates if a requirements.txt file was found.
|
||||
is_detailed_output (bool): Indicates if detailed output is enabled.
|
||||
"""
|
||||
console.print("-" * console.size.width)
|
||||
console.print("Apply Fixes")
|
||||
console.print("-" * console.size.width)
|
||||
|
||||
console.print()
|
||||
|
||||
if requirements_txt_found:
|
||||
console.print(
|
||||
"[green]Run `safety scan --apply-fixes`[/green] to update these packages and fix these vulnerabilities. "
|
||||
"Documentation, limitations, and configurations for applying automated fixes: [link]https://docs.safetycli.com/safety-docs/vulnerability-remediation/applying-fixes[/link]"
|
||||
)
|
||||
console.print()
|
||||
console.print(
|
||||
"Alternatively, use your package manager to update packages to their secure versions. Always check for breaking changes when updating packages."
|
||||
)
|
||||
else:
|
||||
msg = "Use your package manager to update packages to their secure versions. Always check for breaking changes when updating packages."
|
||||
console.print(msg)
|
||||
|
||||
if not is_detailed_output:
|
||||
console.print(
|
||||
"[tip]Tip[/tip]: For more detailed output on each vulnerability, add the `--detailed-output` flag to safety scan."
|
||||
)
|
||||
|
||||
console.print()
|
||||
console.print("-" * console.size.width)
|
||||
|
||||
|
||||
def print_summary(
|
||||
console: Console,
|
||||
total_issues_with_duplicates: int,
|
||||
total_ignored_issues: int,
|
||||
project: ProjectModel,
|
||||
dependencies_count: int = 0,
|
||||
fixes_count: int = 0,
|
||||
resolved_vulns_per_fix: int = 0,
|
||||
is_detailed_output: bool = False,
|
||||
ignored_vulns_data: Optional[Dict[str, Vulnerability]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Prints a concise summary of scan results including vulnerabilities, fixes, and ignored vulnerabilities.
|
||||
|
||||
This function summarizes the results of a security scan, displaying the number of dependencies scanned,
|
||||
vulnerabilities found, suggested fixes, and the impact of those fixes. It also optionally provides a
|
||||
detailed breakdown of ignored vulnerabilities based on predefined policies.
|
||||
|
||||
Args:
|
||||
console (Console): The console object used to print formatted output.
|
||||
total_issues_with_duplicates (int): The total number of security issues, including duplicates.
|
||||
total_ignored_issues (int): The number of issues that were ignored based on project policies.
|
||||
project (ProjectModel): The project model containing the scanned project details and policies.
|
||||
dependencies_count (int, optional): The total number of dependencies scanned for vulnerabilities. Defaults to 0.
|
||||
fixes_count (int, optional): The number of fixes suggested by the scan. Defaults to 0.
|
||||
resolved_vulns_per_fix (int, optional): The number of vulnerabilities that can be resolved by the suggested fixes. Defaults to 0.
|
||||
is_detailed_output (bool, optional): Flag to indicate whether detailed output, especially for ignored vulnerabilities, should be shown. Defaults to False.
|
||||
ignored_vulns_data (Optional[Dict[str, Vulnerability]], optional): A dictionary of vulnerabilities that were ignored, categorized by their reason for being ignored. Defaults to None.
|
||||
|
||||
Returns:
|
||||
None: This function does not return any value. It prints the summary to the console.
|
||||
|
||||
Usage:
|
||||
Call this function after a vulnerability scan to display the results in a clear, formatted manner.
|
||||
Example:
|
||||
print_summary(console, unique_issues, 10, 2, project_model, dependencies_count=5, fixes_count=2)
|
||||
|
||||
"""
|
||||
|
||||
from ..util import pluralize
|
||||
|
||||
# Set the policy message based on the project source
|
||||
if project.policy:
|
||||
policy_msg = (
|
||||
"policy fetched from Safety Platform"
|
||||
if project.policy.source is PolicySource.cloud
|
||||
else f"local {project.id or 'scan policy file'} project scan policy"
|
||||
)
|
||||
else:
|
||||
policy_msg = "default Safety CLI policies"
|
||||
|
||||
console.print(
|
||||
f"Tested [number]{dependencies_count}[/number] {pluralize('dependency', dependencies_count)} for security issues using {policy_msg}"
|
||||
)
|
||||
|
||||
if total_issues_with_duplicates == 0:
|
||||
console.print("0 security issues found, 0 fixes suggested.")
|
||||
else:
|
||||
# Print security issues and ignored vulnerabilities
|
||||
console.print(
|
||||
f"[number]{total_issues_with_duplicates}[/number] {pluralize('vulnerability', total_issues_with_duplicates)} found, "
|
||||
f"[number]{total_ignored_issues}[/number] ignored due to policy."
|
||||
)
|
||||
|
||||
console.print(
|
||||
f"[number]{fixes_count}[/number] {pluralize('fix', fixes_count)} suggested, resolving [number]{resolved_vulns_per_fix}[/number] vulnerabilities."
|
||||
)
|
||||
|
||||
if is_detailed_output:
|
||||
if not ignored_vulns_data:
|
||||
ignored_vulns_data = iter([])
|
||||
|
||||
manual_ignored = {}
|
||||
cvss_severity_ignored = {}
|
||||
cvss_severity_ignored_pkgs = set()
|
||||
unpinned_ignored = {}
|
||||
unpinned_ignored_pkgs = set()
|
||||
environment_ignored = {}
|
||||
environment_ignored_pkgs = set()
|
||||
|
||||
for vuln_data in ignored_vulns_data:
|
||||
code = IgnoreCodes(vuln_data.ignored_code)
|
||||
if code is IgnoreCodes.manual:
|
||||
manual_ignored[vuln_data.vulnerability_id] = vuln_data
|
||||
elif code is IgnoreCodes.cvss_severity:
|
||||
cvss_severity_ignored[vuln_data.vulnerability_id] = vuln_data
|
||||
cvss_severity_ignored_pkgs.add(vuln_data.package_name)
|
||||
elif code is IgnoreCodes.unpinned_specification:
|
||||
unpinned_ignored[vuln_data.vulnerability_id] = vuln_data
|
||||
unpinned_ignored_pkgs.add(vuln_data.package_name)
|
||||
elif code is IgnoreCodes.environment_dependency:
|
||||
environment_ignored[vuln_data.vulnerability_id] = vuln_data
|
||||
environment_ignored_pkgs.add(vuln_data.package_name)
|
||||
|
||||
if manual_ignored:
|
||||
count = len(manual_ignored)
|
||||
console.print(
|
||||
f"[number]{count}[/number] were manually ignored due to the project policy:"
|
||||
)
|
||||
for vuln in manual_ignored.values():
|
||||
render_to_console(
|
||||
vuln,
|
||||
console,
|
||||
rich_kwargs={"emoji": True, "overflow": "crop"},
|
||||
detailed_output=is_detailed_output,
|
||||
)
|
||||
if cvss_severity_ignored:
|
||||
count = len(cvss_severity_ignored)
|
||||
console.print(
|
||||
f"[number]{count}[/number] {pluralize('vulnerability', count)} {pluralize('was', count)} ignored because "
|
||||
"of their severity or exploitability impacted the following"
|
||||
f" {pluralize('package', len(cvss_severity_ignored_pkgs))}: {', '.join(cvss_severity_ignored_pkgs)}"
|
||||
)
|
||||
|
||||
if environment_ignored:
|
||||
count = len(environment_ignored)
|
||||
console.print(
|
||||
f"[number]{count}[/number] {pluralize('vulnerability', count)} {pluralize('was', count)} ignored because "
|
||||
"they are inside an environment dependency."
|
||||
)
|
||||
|
||||
if unpinned_ignored:
|
||||
count = len(unpinned_ignored)
|
||||
console.print(
|
||||
f"[number]{count}[/number] {pluralize('vulnerability', count)} {pluralize('was', count)} ignored because "
|
||||
f"{pluralize('this', len(unpinned_ignored_pkgs))} {pluralize('package', len(unpinned_ignored_pkgs))} {pluralize('has', len(unpinned_ignored_pkgs))} unpinned specs: "
|
||||
f"{', '.join(unpinned_ignored_pkgs)}"
|
||||
)
|
||||
|
||||
|
||||
def print_wait_project_verification(
|
||||
console: Console,
|
||||
project_id: str,
|
||||
closure: Tuple[Any, Dict[str, Any]],
|
||||
on_error_delay: int = 1,
|
||||
) -> Any:
|
||||
"""
|
||||
Print a waiting message while verifying a project.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
project_id (str): The project ID.
|
||||
closure (Tuple[Any, Dict[str, Any]]): The function and its arguments to call.
|
||||
on_error_delay (int): Delay in seconds on error.
|
||||
|
||||
Returns:
|
||||
Any: The status of the project verification.
|
||||
"""
|
||||
status = None
|
||||
wait_msg = f"Verifying project {project_id} with Safety Platform."
|
||||
|
||||
with console.status(wait_msg, spinner=DEFAULT_SPINNER):
|
||||
try:
|
||||
f, kwargs = closure
|
||||
status = f(**kwargs)
|
||||
except Exception as e:
|
||||
LOG.exception(f"Unable to verify the project, reason: {e}")
|
||||
reason = (
|
||||
"We are currently unable to verify the project, "
|
||||
"and it is necessary to link the scan to a specific "
|
||||
f"project. \n\nAdditional Information: \n{e}"
|
||||
)
|
||||
raise SafetyException(message=reason)
|
||||
|
||||
if not status:
|
||||
wait_msg = f'Unable to verify "{project_id}". Starting again...'
|
||||
time.sleep(on_error_delay)
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def print_project_info(console: Console, project: ProjectModel):
|
||||
"""
|
||||
Print information about the project.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
project (ProjectModel): The project model.
|
||||
"""
|
||||
config_msg = "loaded without policies or custom configuration."
|
||||
|
||||
if project.policy:
|
||||
if project.policy.source is PolicySource.local:
|
||||
rel_location = (
|
||||
project.policy.location.name if project.policy.location else ""
|
||||
)
|
||||
config_msg = f"configuration and policies fetched from {rel_location}."
|
||||
else:
|
||||
config_msg = " policies fetched from Safety Platform."
|
||||
|
||||
msg = f"[bold]{project.id} project found[/bold] - {config_msg}"
|
||||
console.print(msg)
|
||||
|
||||
|
||||
def print_wait_policy_download(
|
||||
console: Console, closure: Tuple[Any, Dict[str, Any]]
|
||||
) -> Optional[PolicyFileModel]:
|
||||
"""
|
||||
Print a waiting message while downloading a policy from the cloud.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
closure (Tuple[Any, Dict[str, Any]]): The function and its arguments to call.
|
||||
|
||||
Returns:
|
||||
Optional[PolicyFileModel]: The downloaded policy file model.
|
||||
"""
|
||||
policy = None
|
||||
wait_msg = "Looking for a policy from cloud..."
|
||||
|
||||
with console.status(wait_msg, spinner=DEFAULT_SPINNER):
|
||||
try:
|
||||
f, kwargs = closure
|
||||
policy = f(**kwargs)
|
||||
except Exception as e:
|
||||
LOG.exception(f"Policy download failed, reason: {e}")
|
||||
console.print("Not using cloud policy file.")
|
||||
|
||||
if policy:
|
||||
wait_msg = "Policy fetched from Safety Platform."
|
||||
else:
|
||||
# TODO: Send a log
|
||||
pass
|
||||
return policy
|
||||
|
||||
|
||||
def prompt_project_id(console: Console, default_id: str) -> str:
|
||||
"""
|
||||
Prompt the user to set a project ID, on a non-interactive mode it will
|
||||
fallback to the default ID parameter.
|
||||
"""
|
||||
default_prj_id = clean_project_id(default_id)
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
LOG.info("Fallback to default project id, because of non-interactive mode.")
|
||||
|
||||
return default_prj_id
|
||||
|
||||
prompt_text = f"\nEnter a name for this codebase (or press [bold]Enter[/bold] to use '\\[{default_prj_id}]')"
|
||||
|
||||
while True:
|
||||
result = Prompt.ask(
|
||||
prompt_text, console=console, default=default_prj_id, show_default=False
|
||||
)
|
||||
|
||||
return clean_project_id(result) if result != default_prj_id else default_prj_id
|
||||
|
||||
|
||||
def prompt_link_project(console: Console, prj_name: str, prj_admin_email: str) -> bool:
|
||||
"""
|
||||
Prompt the user to link the scan with an existing project. If the console is not interactive
|
||||
it will fallback to True.
|
||||
|
||||
Args:
|
||||
console (Console): The console for output.
|
||||
prj_name (str): The project name.
|
||||
prj_admin_email (str): The project admin email.
|
||||
|
||||
Returns:
|
||||
bool: True if the user wants to link the scan, False otherwise.
|
||||
"""
|
||||
if not sys.stdin.isatty():
|
||||
LOG.info("Linking to existing project because of non-interactive mode.")
|
||||
return True
|
||||
|
||||
console.print(
|
||||
"\n[bold]Safety found an existing codebase with this name in your organization:[/bold]"
|
||||
)
|
||||
|
||||
for detail in (
|
||||
f"[bold]Codebase name:[/bold] {prj_name}",
|
||||
f"[bold]Codebase admin:[/bold] {prj_admin_email}",
|
||||
):
|
||||
console.print(Padding(detail, (0, 0, 0, 2)), emoji=True)
|
||||
|
||||
console.print()
|
||||
prompt_question = "Do you want to link it with this existing codebase?"
|
||||
|
||||
answer = Prompt.ask(
|
||||
prompt=prompt_question,
|
||||
choices=["y", "n"],
|
||||
default="y",
|
||||
show_default=True,
|
||||
console=console,
|
||||
).lower()
|
||||
|
||||
return answer == "y"
|
||||
|
||||
|
||||
def render_to_console(
|
||||
cls: Vulnerability,
|
||||
console: Console,
|
||||
rich_kwargs: Dict[str, Any],
|
||||
detailed_output: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Render a vulnerability to the console.
|
||||
|
||||
Args:
|
||||
cls (Vulnerability): The vulnerability instance.
|
||||
console (Console): The console for output.
|
||||
rich_kwargs (Dict[str, Any]): Additional arguments for rendering.
|
||||
detailed_output (bool): Indicates if detailed output is enabled.
|
||||
"""
|
||||
cls.__render__(console, detailed_output, rich_kwargs)
|
||||
|
||||
|
||||
def get_render_console(entity_type: Any) -> Any:
|
||||
"""
|
||||
Get the render function for a specific entity type.
|
||||
|
||||
Args:
|
||||
entity_type (Any): The entity type.
|
||||
|
||||
Returns:
|
||||
Any: The render function.
|
||||
"""
|
||||
|
||||
if entity_type is Vulnerability:
|
||||
|
||||
def __render__(self, console: Console, detailed_output: bool, rich_kwargs):
|
||||
if not rich_kwargs:
|
||||
rich_kwargs = {}
|
||||
|
||||
pre = " Ignored:" if self.ignored else ""
|
||||
severity_detail = None
|
||||
|
||||
if self.severity and self.severity.source:
|
||||
severity_detail = self.severity.source
|
||||
|
||||
if self.severity.cvssv3 and "base_severity" in self.severity.cvssv3:
|
||||
severity_detail += f", CVSS Severity {self.severity.cvssv3['base_severity'].upper()}"
|
||||
|
||||
advisory_length = 200 if detailed_output else 110
|
||||
|
||||
console.print(
|
||||
Padding(
|
||||
f"->{pre} Vuln ID [vuln_id]{self.vulnerability_id}[/vuln_id]: {severity_detail if severity_detail else ''}",
|
||||
(0, 0, 0, 2),
|
||||
),
|
||||
**rich_kwargs,
|
||||
)
|
||||
console.print(
|
||||
Padding(
|
||||
f"{self.advisory[:advisory_length]}{'...' if len(self.advisory) > advisory_length else ''}",
|
||||
(0, 0, 0, 5),
|
||||
),
|
||||
**rich_kwargs,
|
||||
)
|
||||
|
||||
if detailed_output:
|
||||
console.print(
|
||||
Padding(
|
||||
f"For more information: [link]{self.more_info_url}[/link]",
|
||||
(0, 0, 0, 5),
|
||||
),
|
||||
**rich_kwargs,
|
||||
)
|
||||
|
||||
return __render__
|
||||
|
||||
|
||||
def render_scan_html(report: ReportModel, obj: Any) -> str:
|
||||
"""
|
||||
Render the scan report to HTML.
|
||||
|
||||
Args:
|
||||
report (ReportModel): The scan report model.
|
||||
obj (Any): The object containing additional settings.
|
||||
|
||||
Returns:
|
||||
str: The rendered HTML report.
|
||||
"""
|
||||
from safety.scan.command import ScannableEcosystems
|
||||
|
||||
project = report.projects[0] if any(report.projects) else None
|
||||
|
||||
scanned_packages = 0
|
||||
affected_packages = 0
|
||||
ignored_packages = 0
|
||||
remediations_recommended = 0
|
||||
ignored_vulnerabilities = 0
|
||||
vulnerabilities = 0
|
||||
vulns_per_file = defaultdict(int)
|
||||
remed_per_file = defaultdict(int)
|
||||
|
||||
for file in project.files:
|
||||
scanned_packages += len(file.results.dependencies)
|
||||
affected_packages += len(file.results.get_affected_dependencies())
|
||||
ignored_vulnerabilities += len(file.results.ignored_vulns)
|
||||
|
||||
for spec in file.results.get_affected_specifications():
|
||||
vulnerabilities += len(spec.vulnerabilities)
|
||||
vulns_per_file[file.location] += len(spec.vulnerabilities)
|
||||
if spec.remediation:
|
||||
remed_per_file[file.location] += 1
|
||||
remediations_recommended += 1
|
||||
|
||||
ignored_packages += len(file.results.ignored_vulns)
|
||||
|
||||
# TODO: Get this information for the report model (?)
|
||||
summary = {
|
||||
"scanned_packages": scanned_packages,
|
||||
"affected_packages": affected_packages,
|
||||
"remediations_recommended": remediations_recommended,
|
||||
"ignored_vulnerabilities": ignored_vulnerabilities,
|
||||
"vulnerabilities": vulnerabilities,
|
||||
}
|
||||
|
||||
vulnerabilities = []
|
||||
|
||||
# TODO: This should be based on the configs per command
|
||||
ecosystems = [
|
||||
(
|
||||
f"{ecosystem.name.title()}",
|
||||
[file_type.human_name(plural=True) for file_type in ecosystem.file_types],
|
||||
)
|
||||
for ecosystem in [
|
||||
Ecosystem(member.value) for member in list(ScannableEcosystems)
|
||||
]
|
||||
]
|
||||
|
||||
settings = {
|
||||
"audit_and_monitor": True,
|
||||
"platform_url": SAFETY_PLATFORM_URL,
|
||||
"ecosystems": ecosystems,
|
||||
}
|
||||
template_context = {
|
||||
"report": report,
|
||||
"summary": summary,
|
||||
"announcements": [],
|
||||
"project": project,
|
||||
"platform_enabled": obj.platform_enabled,
|
||||
"settings": settings,
|
||||
"vulns_per_file": vulns_per_file,
|
||||
"remed_per_file": remed_per_file,
|
||||
}
|
||||
|
||||
return parse_html(kwargs=template_context, template="scan/index.html")
|
||||
|
||||
|
||||
def generate_spdx_creation_info(spdx_version: str, project_identifier: str) -> Any:
|
||||
"""
|
||||
Generate SPDX creation information.
|
||||
|
||||
Args:
|
||||
spdx_version (str): The SPDX version.
|
||||
project_identifier (str): The project identifier.
|
||||
|
||||
Returns:
|
||||
Any: The SPDX creation information.
|
||||
"""
|
||||
from spdx_tools.spdx.model import (
|
||||
Actor,
|
||||
ActorType,
|
||||
CreationInfo,
|
||||
)
|
||||
|
||||
version = int(time.time())
|
||||
SPDX_ID_TYPE = "SPDXRef-DOCUMENT"
|
||||
DOC_NAME = f"{project_identifier}-{version}"
|
||||
|
||||
DOC_NAMESPACE = f"https://spdx.org/spdxdocs/{DOC_NAME}"
|
||||
# DOC_NAMESPACE = f"urn:safety:{project_identifier}:{version}"
|
||||
|
||||
DOC_COMMENT = f"This document was created using SPDX {spdx_version}"
|
||||
CREATOR_COMMENT = (
|
||||
"Safety CLI automatically created this SPDX document from a scan report."
|
||||
)
|
||||
|
||||
TOOL_ID = "safety"
|
||||
TOOL_VERSION = get_version()
|
||||
|
||||
doc_creator = Actor(
|
||||
actor_type=ActorType.TOOL, name=f"{TOOL_ID}-{TOOL_VERSION}", email=None
|
||||
)
|
||||
|
||||
creation_info = CreationInfo(
|
||||
spdx_version=f"SPDX-{spdx_version}",
|
||||
spdx_id=SPDX_ID_TYPE,
|
||||
name=DOC_NAME,
|
||||
document_namespace=DOC_NAMESPACE,
|
||||
creators=[doc_creator],
|
||||
created=datetime.datetime.now(),
|
||||
document_comment=DOC_COMMENT,
|
||||
creator_comment=CREATOR_COMMENT,
|
||||
)
|
||||
return creation_info
|
||||
|
||||
|
||||
def create_pkg_ext_ref(*, package: PythonDependency, version: Optional[str]) -> Any:
|
||||
"""
|
||||
Create an external package reference for SPDX.
|
||||
|
||||
Args:
|
||||
package (PythonDependency): The package dependency.
|
||||
version (Optional[str]): The package version.
|
||||
|
||||
Returns:
|
||||
Any: The external package reference.
|
||||
"""
|
||||
from spdx_tools.spdx.model import (
|
||||
ExternalPackageRef,
|
||||
ExternalPackageRefCategory,
|
||||
)
|
||||
|
||||
version_detail = f"@{version}" if version else ""
|
||||
pkg_ref = ExternalPackageRef(
|
||||
ExternalPackageRefCategory.PACKAGE_MANAGER,
|
||||
"purl",
|
||||
f"pkg:pypi/{package.name}{version_detail}",
|
||||
)
|
||||
return pkg_ref
|
||||
|
||||
|
||||
def create_packages(dependencies: List[PythonDependency]) -> List[Any]:
|
||||
"""
|
||||
Create a list of SPDX packages.
|
||||
|
||||
Args:
|
||||
dependencies (List[PythonDependency]): List of Python dependencies.
|
||||
|
||||
Returns:
|
||||
List[Any]: List of SPDX packages.
|
||||
"""
|
||||
from spdx_tools.spdx.model import (
|
||||
Package,
|
||||
)
|
||||
from spdx_tools.spdx.model.spdx_no_assertion import SpdxNoAssertion
|
||||
|
||||
doc_pkgs = []
|
||||
pkgs_added = set([])
|
||||
for dependency in dependencies:
|
||||
for spec in dependency.specifications:
|
||||
pkg_version = (
|
||||
next(iter(spec.specifier)).version
|
||||
if spec.is_pinned()
|
||||
else f"{spec.specifier}"
|
||||
)
|
||||
dep_name = dependency.name.replace("_", "-")
|
||||
pkg_id = (
|
||||
f"SPDXRef-pip-{dep_name}-{pkg_version}"
|
||||
if spec.is_pinned()
|
||||
else f"SPDXRef-pip-{dep_name}"
|
||||
)
|
||||
if pkg_id in pkgs_added:
|
||||
continue
|
||||
pkg_ref = create_pkg_ext_ref(package=dependency, version=pkg_version)
|
||||
|
||||
pkg = Package(
|
||||
spdx_id=pkg_id,
|
||||
name=f"pip:{dep_name}",
|
||||
download_location=SpdxNoAssertion(),
|
||||
version=pkg_version,
|
||||
file_name="",
|
||||
supplier=SpdxNoAssertion(),
|
||||
files_analyzed=False,
|
||||
license_concluded=SpdxNoAssertion(),
|
||||
license_declared=SpdxNoAssertion(),
|
||||
copyright_text=SpdxNoAssertion(),
|
||||
external_references=[pkg_ref],
|
||||
)
|
||||
pkgs_added.add(pkg_id)
|
||||
doc_pkgs.append(pkg)
|
||||
return doc_pkgs
|
||||
|
||||
|
||||
def create_spdx_document(*, report: ReportModel, spdx_version: str) -> Optional[Any]:
|
||||
"""
|
||||
Create an SPDX document.
|
||||
|
||||
Args:
|
||||
report (ReportModel): The scan report model.
|
||||
spdx_version (str): The SPDX version.
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The SPDX document.
|
||||
"""
|
||||
from spdx_tools.spdx.model import (
|
||||
Document,
|
||||
Relationship,
|
||||
RelationshipType,
|
||||
)
|
||||
|
||||
project = report.projects[0] if any(report.projects) else None
|
||||
|
||||
if not project:
|
||||
return None
|
||||
|
||||
prj_id = project.id
|
||||
|
||||
if not prj_id:
|
||||
parent_name = project.project_path.parent.name
|
||||
prj_id = parent_name if parent_name else str(int(time.time()))
|
||||
|
||||
creation_info = generate_spdx_creation_info(
|
||||
spdx_version=spdx_version, project_identifier=prj_id
|
||||
)
|
||||
|
||||
depedencies = iter([])
|
||||
for file in project.files:
|
||||
depedencies = itertools.chain(depedencies, file.results.dependencies)
|
||||
|
||||
packages = create_packages(depedencies)
|
||||
|
||||
# Requirement for document to have atleast one relationship
|
||||
relationship = Relationship(
|
||||
"SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-DOCUMENT"
|
||||
)
|
||||
spdx_doc = Document(creation_info, packages, [], [], [], [relationship], [])
|
||||
return spdx_doc
|
||||
|
||||
|
||||
def render_scan_spdx(
|
||||
report: ReportModel, obj: Any, spdx_version: Optional[str]
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
Render the scan report to SPDX format.
|
||||
|
||||
Args:
|
||||
report (ReportModel): The scan report model.
|
||||
obj (Any): The object containing additional settings.
|
||||
spdx_version (Optional[str]): The SPDX version.
|
||||
|
||||
Returns:
|
||||
Optional[Any]: The rendered SPDX document in JSON format.
|
||||
"""
|
||||
from spdx_tools.spdx.writer.write_utils import convert, validate_and_deduplicate
|
||||
|
||||
# Set to latest supported if a version is not specified
|
||||
if not spdx_version:
|
||||
spdx_version = "2.3"
|
||||
|
||||
document_obj = create_spdx_document(report=report, spdx_version=spdx_version)
|
||||
document_obj = validate_and_deduplicate(
|
||||
document=document_obj, validate=True, drop_duplicates=True
|
||||
)
|
||||
doc = None
|
||||
|
||||
if document_obj:
|
||||
doc = convert(document=document_obj, converter=None)
|
||||
|
||||
return json.dumps(doc) if doc else None
|
||||
Reference in New Issue
Block a user