diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index 94b696d..f76933b 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -15,7 +15,7 @@ from codesectools.sasts import SASTS_ALL from codesectools.sasts.all.sast import AllSAST from codesectools.sasts.core.sast import PrebuiltBuildlessSAST, PrebuiltSAST -from codesectools.utils import group_successive +from codesectools.utils import group_successive, shorten_path def build_cli() -> typer.Typer: @@ -230,6 +230,7 @@ def report( ) -> 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 @@ -293,15 +294,22 @@ def report( """ template = template.replace( - "[sasts]", ", ".join(sast.name for sast in all_sast.sasts) + "[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 (sorted by defect number)") + 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 report_data["defects"].values(): + 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" ) @@ -313,13 +321,23 @@ def report( defect_stats_table.add_column( key.replace("_", " ").title(), justify="center" ) - defect_stats_table.add_row(*[str(v) for v in defect_data["score"].values()]) + + 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( - defect_data["source_path"], style=Style(link=defect_report_name) + shorten_path(defect_data["source_path"], 60), + style=Style(link=defect_report_name), ) - main_table.add_row(defect_report_redirect) + + main_table.add_row(defect_report_redirect, *rendered_scores) # Defect table defect_table = Table(title="", show_lines=True) diff --git a/codesectools/sasts/all/graphics.py b/codesectools/sasts/all/graphics.py index c11dd7a..4ae2409 100644 --- a/codesectools/sasts/all/graphics.py +++ b/codesectools/sasts/all/graphics.py @@ -201,7 +201,7 @@ def plot_top_scores(self) -> Figure: X_files, key_values, bottom=bottoms, - label=key.replace("_", " ").title(), + label=f"{key.replace('_', ' ').title()} (x{2**i})", color=score_colors(i), ) bottoms = [b + v for b, v in zip(bottoms, key_values, strict=False)] diff --git a/codesectools/sasts/all/parser.py b/codesectools/sasts/all/parser.py index a56b16e..f932bef 100644 --- a/codesectools/sasts/all/parser.py +++ b/codesectools/sasts/all/parser.py @@ -19,7 +19,7 @@ def __init__(self, name: str, analysis_results: dict[str, AnalysisResult]) -> No self.source_path = None self.analysis_results = analysis_results self.lang = None - self.sasts = [] + self.sast_names = [] self.files = set() self.defects = [] @@ -30,12 +30,12 @@ def __init__(self, name: str, analysis_results: dict[str, AnalysisResult]) -> No else: assert analysis_result.lang == self.lang assert analysis_result.source_path == self.source_path - self.sasts.append(sast_name) + self.sast_names.append(sast_name) self.files |= set(analysis_result.files) self.defects += analysis_result.defects self.category_mapping = {} - for sast_name in self.sasts: + for sast_name in self.sast_names: sast = SASTS_ALL[sast_name]["sast"] for category_name, color in sast.color_mapping.items(): if color.lower() == "red": @@ -55,7 +55,7 @@ def __repr__(self) -> str: return f"""{self.__class__.__name__}( name: \t{self.name} lang: \t{self.lang} - sasts: \t{self.sasts} + sasts: \t{self.sast_names} file_count: \t{len(self.files)} defect_count: \t{len(self.defects)} )""" @@ -142,11 +142,15 @@ def stats_by_scores(self) -> dict: for defect_file, defects in defect_files.items(): defects_cwes = {d.cwe for d in defects if d.cwe.id != -1} - cwes_found_by_all_sasts = 0 + defects_same_cwe = 0 for cwe in defects_cwes: cwes_sasts = {d.sast for d in defects if d.cwe == cwe} - if set(self.sasts) == cwes_sasts: - cwes_found_by_all_sasts += 1 + if set(self.sast_names) == cwes_sasts: + defects_same_cwe += 1 + else: + defects_same_cwe += ( + len(set(self.sast_names) & cwes_sasts) - 1 + ) / len(self.sast_names) defect_locations = {} for defect in defects: @@ -158,7 +162,7 @@ def stats_by_scores(self) -> dict: defects_same_location = 0 defects_same_location_same_cwe = 0 for _, defects_ in defect_locations.items(): - if set(defect.sast for defect in defects_) == set(self.sasts): + if set(defect.sast for defect in defects_) == set(self.sast_names): defects_same_location += 1 defects_by_cwe = {} for defect in defects_: @@ -167,17 +171,27 @@ def stats_by_scores(self) -> dict: defects_by_cwe[defect.cwe].append(defect) for _, defects_ in defects_by_cwe.items(): - if set(defect.sast for defect in defects_) == set(self.sasts): + if set(defect.sast for defect in defects_) == set( + self.sast_names + ): defects_same_location_same_cwe += 1 + else: + defects_same_location_same_cwe += ( + len( + set(defect.sast for defect in defects_) + & set(self.sast_names) + ) + - 1 + ) / len(self.sast_names) stats[defect_file] = { "score": { "defect_number": len(defects), - "unique_cwes_number": len(defects_cwes), - "cwes_found_by_all_sasts": cwes_found_by_all_sasts, - "defects_same_location": defects_same_location, - "defects_same_location_same_cwe": defects_same_location_same_cwe, - } + "defects_same_cwe": defects_same_cwe * 2, + "defects_same_location": defects_same_location * 4, + "defects_same_location_same_cwe": defects_same_location_same_cwe + * 8, + }, } return stats