diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index f76933b..d3264f6 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -1,8 +1,6 @@ """Defines the command-line interface for running all available SAST tools.""" -import io import shutil -from hashlib import sha256 from pathlib import Path import typer @@ -13,9 +11,9 @@ from codesectools.datasets import DATASETS_ALL from codesectools.datasets.core.dataset import FileDataset, GitRepoDataset from codesectools.sasts import SASTS_ALL +from codesectools.sasts.all.report import ReportEngine from codesectools.sasts.all.sast import AllSAST from codesectools.sasts.core.sast import PrebuiltBuildlessSAST, PrebuiltSAST -from codesectools.utils import group_successive, shorten_path def build_cli() -> typer.Typer: @@ -85,7 +83,14 @@ def analyze( ), ] = False, ) -> None: - """Run analysis on the current project with all available SAST tools.""" + """Run analysis on the current project with all available SAST tools. + + Args: + lang: The source code language to analyze. + artifacts: The path to pre-built artifacts (for PrebuiltSAST only). + overwrite: If True, overwrite existing analysis results for the current project. + + """ for sast in all_sast.sasts_by_lang.get(lang, []): if isinstance(sast, PrebuiltBuildlessSAST) and artifacts is None: print( @@ -140,7 +145,14 @@ def benchmark( ), ] = False, ) -> None: - """Run a benchmark on a dataset using all available SAST tools.""" + """Run a benchmark on a dataset using all available SAST tools. + + Args: + dataset: The name of the dataset to benchmark. + overwrite: If True, overwrite existing results. + testing: If True, run benchmark over a single dataset unit for testing. + + """ dataset_name, lang = dataset.split("_") for sast in all_sast.sasts_by_dataset.get(DATASETS_ALL[dataset_name], []): dataset = DATASETS_ALL[dataset_name](lang) @@ -205,7 +217,14 @@ def plot( typer.Option("--format", help="Figures export format"), ] = "png", ) -> None: - """Generate and display plots for a project's aggregated analysis results.""" + """Generate and display plots for a project's aggregated analysis results. + + Args: + project: The name of the project to visualize. + overwrite: If True, overwrite existing figures. + format: The export format for the figures. + + """ from codesectools.sasts.all.graphics import ProjectGraphics project_graphics = ProjectGraphics(project_name=project) @@ -228,14 +247,13 @@ def report( ), ] = False, ) -> None: - """Generate an HTML report for a project's aggregated analysis results.""" - from rich.console import Console - from rich.progress import track - from rich.style import Style - from rich.syntax import Syntax - from rich.table import Table - from rich.text import Text + """Generate an HTML report for a project's aggregated analysis results. + + Args: + project: The name of the project to report on. + overwrite: If True, overwrite existing results. + """ report_dir = all_sast.output_dir / project / "report" if report_dir.is_dir(): if overwrite: @@ -247,197 +265,8 @@ def report( report_dir.mkdir(parents=True) - result = all_sast.parser.load_from_output_dir(project_name=project) - report_data = result.prepare_report_data() - - template = """ - - -
- - - - -{code}
-
-
-
- ^
-
-
- """
- template = template.replace(
- "[sasts]", ", ".join(sast_name for sast_name in result.sast_names)
- )
-
- home_page = Console(record=True, file=io.StringIO())
-
- main_table = Table(title="")
- main_table.add_column("Files")
- for key in list(report_data["defects"].values())[0]["score"].keys():
- main_table.add_column(
- key.replace("_", " ").title(), justify="center", no_wrap=True
- )
-
- for defect_data in track(
- report_data["defects"].values(),
- description="Generating report for source file with defects...",
- ):
- defect_report_name = (
- f"{sha256(defect_data['source_path'].encode()).hexdigest()}.html"
- )
- defect_page = Console(record=True, file=io.StringIO())
-
- # Defect stat table
- defect_stats_table = Table(title="")
- for key in list(report_data["defects"].values())[0]["score"].keys():
- defect_stats_table.add_column(
- key.replace("_", " ").title(), justify="center"
- )
-
- rendered_scores = []
- for v in defect_data["score"].values():
- if isinstance(v, float):
- rendered_scores.append(f"~{v}")
- else:
- rendered_scores.append(str(v))
-
- defect_stats_table.add_row(*rendered_scores)
- defect_page.print(defect_stats_table)
-
- defect_report_redirect = Text(
- shorten_path(defect_data["source_path"], 60),
- style=Style(link=defect_report_name),
- )
-
- main_table.add_row(defect_report_redirect, *rendered_scores)
-
- # Defect table
- defect_table = Table(title="", show_lines=True)
- defect_table.add_column("Location", justify="center")
- defect_table.add_column("SAST", justify="center")
- defect_table.add_column("CWE", justify="center")
- defect_table.add_column("Message")
- rows = []
- for defect in defect_data["raw"]:
- groups = group_successive(defect.lines)
- if groups:
- for group in groups:
- start, end = group[0], group[-1]
- shortcut = Text(f"{start}", style=Style(link=f"#L{start}"))
- cwe_link = (
- Text(
- f"CWE-{defect.cwe.id}",
- style=Style(
- link=f"https://cwe.mitre.org/data/definitions/{defect.cwe.id}.html"
- ),
- )
- if defect.cwe.id != -1
- else "None"
- )
- rows.append(
- (start, shortcut, defect.sast, cwe_link, defect.message)
- )
- else:
- cwe_link = (
- Text(
- f"CWE-{defect.cwe.id}",
- style=Style(
- link=f"https://cwe.mitre.org/data/definitions/{defect.cwe.id}.html"
- ),
- )
- if defect.cwe.id != -1
- else "None"
- )
- rows.append(
- (float("inf"), "None", defect.sast, cwe_link, defect.message)
- )
-
- for row in sorted(rows, key=lambda r: r[0]):
- defect_table.add_row(*row[1:])
- defect_page.print(defect_table)
-
- # Syntax
- if not Path(defect_data["source_path"]).is_file():
- tippy_calls = ""
- print(
- f"Source file {defect_data['source_path']} not found, skipping it..."
- )
- else:
- syntax = Syntax.from_path(defect_data["source_path"], line_numbers=True)
- tooltips = {}
- highlights = {}
- for location in defect_data["locations"]:
- sast, cwe, message, (start, end) = location
- for i in range(start, end + 1):
- text = (
- f"{sast}: {message} (CWE-{cwe.id})"
- if cwe.id != -1
- else f"{sast}: {message}"
- )
- if highlights.get(i):
- highlights[i].add(text)
- else:
- highlights[i] = {text}
-
- for line, texts in highlights.items():
- element_id = f"L{line}"
- bgcolor = "red" if len(texts) > 1 else "yellow"
- syntax.stylize_range(
- Style(bgcolor=bgcolor, link=f"HACK{element_id}"),
- start=(line, 0),
- end=(line + 1, 0),
- )
- tooltips[element_id] = "{code}
+
+
+
+ ^
+
+
+ """
+
+ def __init__(self, project: str, all_sast: AllSAST) -> None:
+ """Initialize the ReportEngine.
+
+ Args:
+ project: The name of the project.
+ all_sast: The AllSAST instance.
+
+ """
+ self.project = project
+ self.all_sast = all_sast
+ self.report_dir = all_sast.output_dir / project / "report"
+
+ self.result = all_sast.parser.load_from_output_dir(project_name=project)
+ self.report_data = self.result.prepare_report_data()
+
+ def generate_single_defect(self, file_data: dict) -> tuple:
+ """Generate the HTML report for a single file with defects."""
+ from rich.console import Console
+ from rich.style import Style
+ from rich.syntax import Syntax
+ from rich.table import Table
+ from rich.text import Text
+
+ file_report_name = (
+ f"{sha256(file_data['source_path'].encode()).hexdigest()}.html"
+ )
+ file_page = Console(record=True, file=io.StringIO())
+
+ # Defect stat table
+ file_stats_table = Table(title="")
+ for key in list(self.report_data["files"].values())[0]["count"].keys():
+ file_stats_table.add_column(key.replace("_", " ").title(), justify="center")
+
+ rendered_scores = []
+ for v in file_data["count"].values():
+ if isinstance(v, float):
+ rendered_scores.append(f"~{v}")
+ else:
+ rendered_scores.append(str(v))
+
+ file_stats_table.add_row(*rendered_scores)
+ file_page.print(file_stats_table)
+
+ file_report_redirect = Text(
+ shorten_path(file_data["source_path"], 60),
+ style=Style(link=file_report_name),
+ )
+
+ # Defect table
+ defect_table = Table(title="", show_lines=True)
+ defect_table.add_column("Location", justify="center")
+ defect_table.add_column("SAST", justify="center")
+ defect_table.add_column("CWE", justify="center")
+ defect_table.add_column("Message")
+ rows = []
+ for defect in file_data["defects"]:
+ groups = group_successive(defect.lines)
+ if groups:
+ for group in groups:
+ start, end = group[0], group[-1]
+ shortcut = Text(f"{start}", style=Style(link=f"#L{start}"))
+ cwe_link = (
+ Text(
+ f"CWE-{defect.cwe.id}",
+ style=Style(
+ link=f"https://cwe.mitre.org/data/definitions/{defect.cwe.id}.html"
+ ),
+ )
+ if defect.cwe.id != -1
+ else "None"
+ )
+ rows.append(
+ (start, shortcut, defect.sast, cwe_link, defect.message)
+ )
+ else:
+ cwe_link = (
+ Text(
+ f"CWE-{defect.cwe.id}",
+ style=Style(
+ link=f"https://cwe.mitre.org/data/definitions/{defect.cwe.id}.html"
+ ),
+ )
+ if defect.cwe.id != -1
+ else "None"
+ )
+ rows.append(
+ (float("inf"), "None", defect.sast, cwe_link, defect.message)
+ )
+
+ for row in sorted(rows, key=lambda r: r[0]):
+ defect_table.add_row(*row[1:])
+ file_page.print(defect_table)
+
+ # Syntax
+ if not Path(file_data["source_path"]).is_file():
+ tippy_calls = ""
+ print(f"Source file {file_data['source_path']} not found, skipping it...")
+ else:
+ syntax = Syntax.from_path(file_data["source_path"], line_numbers=True)
+ tooltips = {}
+ highlights = {}
+ for location in file_data["locations"]:
+ sast, cwe, message, (start, end) = location
+ for i in range(start, end + 1):
+ text = (
+ f"{sast}: {message} (CWE-{cwe.id})"
+ if cwe.id != -1
+ else f"{sast}: {message}"
+ )
+ if highlights.get(i):
+ highlights[i].add(text)
+ else:
+ highlights[i] = {text}
+
+ for line, texts in highlights.items():
+ element_id = f"L{line}"
+ bgcolor = "red" if len(texts) > 1 else "yellow"
+ syntax.stylize_range(
+ Style(bgcolor=bgcolor, link=f"HACK{element_id}"),
+ start=(line, 0),
+ end=(line + 1, 0),
+ )
+ tooltips[element_id] = "