From 536c081cf22b5f30bc27ec23d2afab4cedc6b7f9 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 15:29:23 +0100 Subject: [PATCH 1/7] fix(report): shorten paths to save horizontal space --- codesectools/sasts/all/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index 94b696d..f897f68 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: @@ -317,7 +317,8 @@ def report( 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) From f5dc47f69102769969e9b227f0de94b98446a8d2 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 15:59:26 +0100 Subject: [PATCH 2/7] refactor(parser): rename `sasts` to `sast_names` to avoid confusion --- codesectools/sasts/all/cli.py | 2 +- codesectools/sasts/all/parser.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index f897f68..756054d 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -293,7 +293,7 @@ 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()) diff --git a/codesectools/sasts/all/parser.py b/codesectools/sasts/all/parser.py index a56b16e..11bc241 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)} )""" @@ -145,7 +145,7 @@ def stats_by_scores(self) -> dict: cwes_found_by_all_sasts = 0 for cwe in defects_cwes: cwes_sasts = {d.sast for d in defects if d.cwe == cwe} - if set(self.sasts) == cwes_sasts: + if set(self.sast_names) == cwes_sasts: cwes_found_by_all_sasts += 1 defect_locations = {} @@ -158,7 +158,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,7 +167,9 @@ 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 stats[defect_file] = { From d85bc0b2409f0a991dd59bad3026c89b6ada1af5 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 16:10:31 +0100 Subject: [PATCH 3/7] feat(parser): rework scoring to include partial match and weight Previously, **all** SAST tools must have found the same CWE or location to count it. Now, if at least two SAST tools found the same CWE or location, it will be counted. Weight has been added to prioritize rarer matches. --- codesectools/sasts/all/parser.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/codesectools/sasts/all/parser.py b/codesectools/sasts/all/parser.py index 11bc241..f932bef 100644 --- a/codesectools/sasts/all/parser.py +++ b/codesectools/sasts/all/parser.py @@ -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.sast_names) == cwes_sasts: - cwes_found_by_all_sasts += 1 + 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: @@ -171,15 +175,23 @@ def stats_by_scores(self) -> dict: 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 From 367678f5b2da4454eb59c158d0e8f5bdb479e43e Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 16:14:32 +0100 Subject: [PATCH 4/7] feat(graphics): add score multiplier to score plot legend --- codesectools/sasts/all/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)] From 7176be2a4ed8729640cc8c22583b834b7c5b8763 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 16:17:37 +0100 Subject: [PATCH 5/7] feat(report): show progress bar during long report generation --- codesectools/sasts/all/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index 756054d..4686a1d 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -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 @@ -301,7 +302,10 @@ def report( main_table = Table(title="") main_table.add_column("Files (sorted by defect number)") - 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" ) From 3861f36f3cc1905cba437614f1a3add5b385bc79 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 16:26:19 +0100 Subject: [PATCH 6/7] feat(report): display approximate count for partial matches --- codesectools/sasts/all/cli.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index 4686a1d..85ef5b3 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -317,7 +317,15 @@ 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( From c1150c30e125734db7108e4bbd79296d514c5294 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Fri, 21 Nov 2025 16:26:46 +0100 Subject: [PATCH 7/7] feat(report): add defect count on home page --- codesectools/sasts/all/cli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index 85ef5b3..f76933b 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -300,7 +300,11 @@ def report( 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 track( report_data["defects"].values(), @@ -332,7 +336,8 @@ def report( 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)