diff --git a/README.md b/README.md index 91ef20a..e9d7063 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ A framework for code security that provides abstractions for static analysis tools and datasets to support their integration, testing, and evaluation. + +> [!WARNING] +> This project is under active development. New versions may introduce breaking changes that can affect existing configurations or previously generated results. Use with caution. ## Table Of Contents @@ -13,9 +16,13 @@ A framework for code security that provides abstractions for static analysis too - [Features](#features) - [SAST Tool Integration Status](#sast-tool-integration-status) - [Usage](#usage) + - [Running the Tool](#running-the-tool) - [Command-line interface](#command-line-interface) - [Docker](#docker) - [Python API](#python-api) + - [Report generation](#report-generation) + - [HTML](#html) + - [SARIF](#sarif) ## Overview @@ -23,8 +30,8 @@ A framework for code security that provides abstractions for static analysis too **CodeSecTools** is a collection of scripts and wrappers that abstract external resources (such as SAST tools, datasets, and codebases), providing standardized interfaces to help them interact easily.
- Workflow - Workflow + Workflow + Workflow example
For step-by-step instructions on installation, configuration, and basic usage, please refer to the [**quick start guide**](https://oppida.github.io/CodeSecTools/home/quick_start_guide.html). @@ -57,31 +64,33 @@ For more details on the design and integration of SAST tools and datasets in Cod ## Usage +### Running the Tool + #### Command-line interface ```bash -$ cstools - - Usage: cstools [OPTIONS] COMMAND [ARGS]... - - CodeSecTools CLI. - -╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ --debug -d Show debugging messages and disable pretty exceptions. │ -│ --version -v Show the tool's version. │ -│ --install-completion Install completion for the current shell. │ -│ --show-completion Show completion for the current shell, to copy it or customize the installation. │ -╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ status Display the availability of SAST tools and datasets. │ -│ allsast Run all available SAST tools together. │ -│ bearer Bearer SAST │ -│ coverity Coverity Static Analysis │ -│ cppcheck Cppcheck │ -│ semgrepce Semgrep Community Edition Engine │ -│ snykcode Snyk Code │ -│ spotbugs SpotBugs │ -╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +cstools + + Usage: cstools [OPTIONS] COMMAND [ARGS]... + + CodeSecTools CLI. + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +│ --debug -d Show debugging messages and disable pretty exceptions. │ +│ --version -v Show the tool's version. │ +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────╮ +│ status Display the availability of SAST tools and datasets. │ +│ docker Start the Docker environment for the specified target (current │ +│ directory by default). │ +│ allsast Run all available SAST tools together. │ +│ bearer Bearer SAST │ +│ coverity Coverity Static Analysis │ +│ cppcheck Cppcheck │ +│ semgrepce Semgrep Community Edition Engine │ +│ snykcode Snyk Code │ +│ spotbugs SpotBugs │ +╰──────────────────────────────────────────────────────────────────────────────╯ ``` #### Docker @@ -101,18 +110,23 @@ Mount necessary directories if you want to include: A simpler way is to use the CLI: ```bash -$ cstools docker --help - - Usage: cstools docker [OPTIONS] - - Start the Docker environment for the specified target (current directory by default). - -╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ --target PATH The directory to mount inside the container. [default: .] │ -│ --isolation --no-isolation Enable network isolation for the container (disables host network sharing). [default: no-isolation] │ -│ --help Show this message and exit. │ -╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - +cstools docker --help + + Usage: cstools docker [OPTIONS] + + Start the Docker environment for the specified target (current directory by + default). + +╭─ Options ────────────────────────────────────────────────────────────────────╮ +│ --target PATH The directory to mount inside the │ +│ container. │ +│ [default: .] │ +│ --isolation --no-isolation Enable network isolation for the │ +│ container (disables host network │ +│ sharing). │ +│ [default: no-isolation] │ +│ --help Show this message and exit. │ +╰──────────────────────────────────────────────────────────────────────────────╯ ``` #### Python API @@ -144,4 +158,77 @@ for plot_function in graphics.plot_functions: fig = plot_function() fig.show() ``` - + +### Report generation + +CodeSecTools can generate reports when running with `allsast`: +```bash +cstools allsast report --help + + Usage: cstools allsast report [OPTIONS] PROJECT + + Generate an HTML report + +╭─ Arguments ──────────────────────────────────────────────────────────────────╮ +│ * project CHOICE [required] │ +╰──────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ────────────────────────────────────────────────────────────────────╮ +│ --format [HTML|SARIF] Report format [default: HTML] │ +│ --top INTEGER Limit to a number of files by score │ +│ --overwrite Overwrite existing results │ +│ --help Show this message and exit. │ +╰──────────────────────────────────────────────────────────────────────────────╯ +``` + +Each report format provides different information and may require additional tools. + +#### HTML + +Low requirements. Good for visualization and getting a quick overview. + +- Requirements: + - A web browser with JavaScript enabled +- Pros: + - Source files are **sorted** by score + - Source files are **included** and displayed in the report + - Findings are **highlighted** and SAST tools messages are shown on hover +- Cons: + - No navigation between source files + - Intended for visualization only + - Not suitable for advanced code analysis + +| *Report* | *Finding* | *Hover* | +|:---:|:---:|:---:| +| ![HTML report example](docs/assets/readme/report/html_report.png) | ![HTML finding example](docs/assets/readme/report/html_finding.png) | ![Hover example](docs/assets/readme/report/html_hover.png) | + +#### SARIF + +Higher requirements. Best suited for advanced code analysis and triage. + +- Requirements: + - VSCode with: + - [vscode-sarif-explorer](https://github.com/trailofbits/vscode-sarif-explorer) extension + - Language Server Extension: + - C/C++: [vscode-clangd](https://github.com/clangd/vscode-clangd) + - Java: [vscode-java](https://github.com/redhat-developer/vscode-java) + - Source code +- Features: + - Triage interface with `vscode-sarif-explorer`: + - Filter findings: + - by keywords + - by path (include/exclude) + - by level (error, warning, note, none) + - Navigate directly to the source code + - Mark findings as true or false positives + - Add comments to findings + - For more details, see [vscode-sarif-explorer](https://github.com/trailofbits/vscode-sarif-explorer) + - Advanced code analysis with Language Server: + - Go to definition + - Find references + - View documentation + - And more... + +| *Triage* | *Documentation* | +|:---:|:---:| +| ![Triage](docs/assets/readme/report/sarif_triage.png) | ![Documentation](docs/assets/readme/report/sarif_documentation.png) | + \ No newline at end of file diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index b9e62b5..201f1a6 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -11,10 +11,13 @@ 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.report.HTML import HTMLReport +from codesectools.sasts.all.report.SARIF import SARIFReport from codesectools.sasts.all.sast import AllSAST from codesectools.sasts.core.sast import PrebuiltBuildlessSAST, PrebuiltSAST +REPORT_FORMATS = {"HTML": HTMLReport, "SARIF": SARIFReport} + def build_cli() -> typer.Typer: """Build the Typer CLI for running all SAST tools.""" @@ -239,6 +242,21 @@ def report( metavar="PROJECT", ), ], + format: Annotated[ + str, + typer.Option( + "--format", + click_type=Choice(REPORT_FORMATS.keys()), + help="Report format", + ), + ] = "HTML", + top: Annotated[ + int | None, + typer.Option( + "--top", + help="Limit to a number of files by score", + ), + ] = None, overwrite: Annotated[ bool, typer.Option( @@ -247,14 +265,16 @@ def report( ), ] = False, ) -> None: - """Generate an HTML report for a project's aggregated analysis results. + """Generate a report for a project's aggregated analysis results. Args: project: The name of the project to report on. + format: The format of the report to generate (e.g., HTML, SARIF). + top: The maximum number of files to include, ranked by score. overwrite: If True, overwrite existing results. """ - report_dir = all_sast.output_dir / project / "report" + report_dir = all_sast.output_dir / project / "report" / format if report_dir.is_dir(): if overwrite: shutil.rmtree(report_dir) @@ -265,7 +285,9 @@ def report( report_dir.mkdir(parents=True) - report_engine = ReportEngine(project=project, all_sast=all_sast) + report_engine = REPORT_FORMATS[format]( + project=project, all_sast=all_sast, top=top + ) report_engine.generate() print(f"Report generated at {report_dir.resolve()}") diff --git a/codesectools/sasts/all/parser.py b/codesectools/sasts/all/parser.py index b8d8296..fd7a4f5 100644 --- a/codesectools/sasts/all/parser.py +++ b/codesectools/sasts/all/parser.py @@ -207,8 +207,16 @@ def stats_by_scores(self) -> dict: } return stats - def prepare_report_data(self) -> dict: - """Prepare data needed to generate a report.""" + def prepare_report_data(self, top: int | None = None) -> dict: + """Prepare data needed to generate a report. + + Args: + top: The maximum number of files to include, ranked by score. + + Returns: + A dictionary containing the prepared report data. + + """ report = {} scores = self.stats_by_scores() @@ -234,6 +242,13 @@ def prepare_report_data(self) -> dict: "defects": defects, } + if top: + min_score = sorted([v["score"] for v in report.values()], reverse=True)[ + min(top, len(report) - 1) + ] + else: + min_score = 0 + report = { k: v for k, v in sorted( @@ -241,6 +256,7 @@ def prepare_report_data(self) -> dict: key=lambda item: item[1]["score"], reverse=True, ) + if v["score"] >= min_score } return report diff --git a/codesectools/sasts/all/report.py b/codesectools/sasts/all/report/HTML.py similarity index 91% rename from codesectools/sasts/all/report.py rename to codesectools/sasts/all/report/HTML.py index a39b4de..61fa8fd 100644 --- a/codesectools/sasts/all/report.py +++ b/codesectools/sasts/all/report/HTML.py @@ -4,11 +4,11 @@ from hashlib import sha256 from pathlib import Path -from codesectools.sasts.all.sast import AllSAST +from codesectools.sasts.all.report import Report from codesectools.utils import group_successive -class ReportEngine: +class HTMLReport(Report): """Generate interactive HTML reports for SAST analysis results. Attributes: @@ -21,6 +21,8 @@ class ReportEngine: """ + format = "HTML" + TEMPLATE = """ @@ -65,21 +67,6 @@ class ReportEngine: """ - 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, defect_file: dict) -> str: """Generate the HTML report for a single file with defects.""" from rich.console import Console @@ -187,11 +174,7 @@ def generate_single_defect(self, defect_file: dict) -> str: return html_content def generate(self) -> None: - """Generate the HTML report. - - Creates the report directory and generates HTML files for the main view - and for each file with defects. - """ + """Generate the HTML report.""" from rich.console import Console from rich.progress import track from rich.style import Style diff --git a/codesectools/sasts/all/report/SARIF.py b/codesectools/sasts/all/report/SARIF.py new file mode 100644 index 0000000..3f3bcdf --- /dev/null +++ b/codesectools/sasts/all/report/SARIF.py @@ -0,0 +1,122 @@ +"""Generates SARIF reports for aggregated SAST analysis results.""" + +from typing import Optional + +from codesectools.sasts.all.report import Report +from codesectools.sasts.core.parser.format.SARIF import ( + ArtifactLocation, + Location, + Message, + PhysicalLocation, + PropertyBag, + Region, + ReportingDescriptor, + Result, + Run, + Tool, + ToolComponent, +) +from codesectools.sasts.core.parser.format.SARIF import ( + StaticAnalysisResultsFormatSarifVersion210JsonSchema as SARIF, +) + + +class SARIFReport(Report): + """Generate SARIF reports for SAST analysis results. + + Attributes: + format (str): The format of the report, which is "SARIF". + project (str): The name of the project. + all_sast (AllSAST): The AllSAST manager instance. + report_dir (Path): The directory where reports are saved. + result (AllSASTAnalysisResult): The parsed analysis results. + report_data (dict): The data prepared for rendering the report. + + """ + + format = "SARIF" + + def generate(self) -> None: + """Generate the SARIF report.""" + included_defect_file = self.report_data.keys() + + runs: list[Run] = [] + for analysis_result in self.result.analysis_results.values(): + results: list[Result] = [] + rules: list[ReportingDescriptor] = [] + rule_ids = set() + + for defect in analysis_result.defects: + if defect.filepath_str not in included_defect_file: + continue + + relative_uri = defect.filepath.relative_to( + analysis_result.source_path + ).as_posix() + + region: Optional[Region] = None + if defect.lines: + start_line_num = min(defect.lines) + end_line_num = ( + max(defect.lines) if len(defect.lines) > 1 else start_line_num + ) + + region = Region(start_line=start_line_num, end_line=end_line_num) + + physical_location = PhysicalLocation( + artifact_location=ArtifactLocation( + uri=relative_uri, uri_base_id="%SRCROOT%" + ), + region=region, + ) + + result = Result( + rule_id=defect.checker, + level=defect.level, + message=Message(text=defect.message), + locations=[Location(physical_location=physical_location)], # ty:ignore[missing-argument] + properties=PropertyBag( + __root__={"cwe": str(defect.cwe)} # ty:ignore[unknown-argument] + ), + ) # ty:ignore[missing-argument] + results.append(result) + + if defect.checker not in rule_ids: + rules.append(ReportingDescriptor(id=defect.checker)) # ty:ignore[missing-argument] + rule_ids.add(defect.checker) + + tool = Tool( + driver=ToolComponent( + name=analysis_result.sast_name, + rules=rules, + ) # ty:ignore[missing-argument] + ) # ty:ignore[missing-argument] + + run = Run( + tool=tool, + results=results, + original_uri_base_ids={ + "%SRCROOT%": ArtifactLocation( + uri=analysis_result.source_path.resolve().as_uri() + ) + }, + properties=PropertyBag( + __root__={ + "lines_of_codes": analysis_result.lines_of_codes, + "analysis_time_seconds": analysis_result.time, + "language": analysis_result.lang, + } # ty:ignore[unknown-argument] + ), + ) # ty:ignore[missing-argument] + + runs.append(run) + + sarif_report = SARIF( + version="2.1.0", + runs=runs, + ) + + sarif_file = (self.report_dir / self.result.name).with_suffix(".sarif") + sarif_file.write_text( + sarif_report.model_dump_json(by_alias=True, exclude_none=True, indent=2) + ) diff --git a/codesectools/sasts/all/report/__init__.py b/codesectools/sasts/all/report/__init__.py new file mode 100644 index 0000000..8a48770 --- /dev/null +++ b/codesectools/sasts/all/report/__init__.py @@ -0,0 +1,42 @@ +"""Defines the base report generation functionality for aggregated SAST results.""" + +from abc import ABC, abstractmethod + +from codesectools.sasts.all.sast import AllSAST + + +class Report(ABC): + """Abstract base class for report generation. + + Attributes: + format (str): The format of the report (e.g., "HTML", "SARIF"). + project (str): The name of the project. + all_sast (AllSAST): The AllSAST manager instance. + report_dir (Path): The directory where reports are saved. + result (AllSASTAnalysisResult): The parsed analysis results. + report_data (dict): The data prepared for rendering the report. + + """ + + format: str + + def __init__(self, project: str, all_sast: AllSAST, top: int | None = None) -> None: + """Initialize the Report. + + Args: + project: The name of the project. + all_sast: The AllSAST instance. + top: The number of top files to include in the report based on score. + + """ + self.project = project + self.all_sast = all_sast + self.report_dir = all_sast.output_dir / project / "report" / self.format + + self.result = all_sast.parser.load_from_output_dir(project_name=project) + self.report_data = self.result.prepare_report_data(top=top) + + @abstractmethod + def generate(self) -> None: + """Generate the report.""" + pass diff --git a/codesectools/sasts/core/parser/format/README.md b/codesectools/sasts/core/parser/format/README.md index 1937c4e..c6771c4 100644 --- a/codesectools/sasts/core/parser/format/README.md +++ b/codesectools/sasts/core/parser/format/README.md @@ -19,6 +19,7 @@ datamodel-codegen \ --use-union-operator \ --target-python-version 3.12 \ --enum-field-as-literal all \ + --allow-population-by-field-name \ --custom-file-header '"""Static Analysis Results Interchange Format (SARIF) Version 2.1.0 data model."""' ruff format SARIF.py @@ -44,6 +45,7 @@ datamodel-codegen \ --use-union-operator \ --target-python-version 3.12 \ --enum-field-as-literal all \ + --allow-population-by-field-name \ --class-name CoverityJsonOutputV10 \ --custom-file-header '"""Coverity JSON Output V10 model."""' diff --git a/codesectools/sasts/core/parser/format/SARIF/__init__.py b/codesectools/sasts/core/parser/format/SARIF/__init__.py index b1ee1c8..762db27 100644 --- a/codesectools/sasts/core/parser/format/SARIF/__init__.py +++ b/codesectools/sasts/core/parser/format/SARIF/__init__.py @@ -12,6 +12,7 @@ class PropertyBag(BaseModel): model_config = ConfigDict( extra="allow", + validate_by_name=True, ) tags: Annotated[ list[str] | None, @@ -30,6 +31,7 @@ class ReportingConfiguration(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) enabled: Annotated[ bool | None, @@ -66,6 +68,7 @@ class ToolComponentReference(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) name: Annotated[ str | None, @@ -98,6 +101,7 @@ class Address(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) absolute_address: Annotated[ int | None, @@ -169,6 +173,7 @@ class LogicalLocation(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) name: Annotated[ str | None, @@ -221,6 +226,7 @@ class Message1(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) text: Annotated[str, Field(description="A plain text message string.")] markdown: Annotated[str | None, Field(description="A Markdown message string.")] = ( @@ -249,6 +255,7 @@ class Message2(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) text: Annotated[str | None, Field(description="A plain text message string.")] = ( None @@ -280,6 +287,7 @@ class MultiformatMessageString(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) text: Annotated[ str, Field(description="A plain text message string or format string.") @@ -300,6 +308,7 @@ class Rectangle(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) top: Annotated[ float | None, @@ -341,6 +350,7 @@ class ReportingDescriptorReference1(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[str | None, Field(description="The id of the descriptor.")] = None index: Annotated[ @@ -377,6 +387,7 @@ class ReportingDescriptorReference2(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[str | None, Field(description="The id of the descriptor.")] = None index: Annotated[ @@ -413,6 +424,7 @@ class ReportingDescriptorReference3(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[str, Field(description="The id of the descriptor.")] index: Annotated[ @@ -456,6 +468,7 @@ class ReportingDescriptorRelationship(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) target: Annotated[ ReportingDescriptorReference, @@ -484,6 +497,7 @@ class RunAutomationDetails(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) description: Annotated[ Message | None, @@ -525,6 +539,7 @@ class TranslationMetadata(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) name: Annotated[ str, Field(description="The name associated with the translation metadata.") @@ -577,6 +592,7 @@ class ArtifactContent(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) text: Annotated[ str | None, Field(description="UTF-8-encoded content from a text artifact.") @@ -606,6 +622,7 @@ class ArtifactLocation(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) uri: Annotated[ str | None, @@ -642,6 +659,7 @@ class ConfigurationOverride(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) configuration: Annotated[ ReportingConfiguration, @@ -668,6 +686,7 @@ class Edge(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[ str, @@ -705,6 +724,7 @@ class EdgeTraversal(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) edge_id: Annotated[ str, Field(alias="edgeId", description="Identifies the edge being traversed.") @@ -741,6 +761,7 @@ class ExternalPropertyFileReference1(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) location: Annotated[ ArtifactLocation, @@ -774,6 +795,7 @@ class ExternalPropertyFileReference2(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) location: Annotated[ ArtifactLocation | None, @@ -812,6 +834,7 @@ class ExternalPropertyFileReferences(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) conversion: Annotated[ ExternalPropertyFileReference | None, @@ -953,6 +976,7 @@ class GraphTraversal1(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) run_graph_index: Annotated[ int, @@ -1009,6 +1033,7 @@ class GraphTraversal2(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) run_graph_index: Annotated[ int | None, @@ -1068,6 +1093,7 @@ class LocationRelationship(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) target: Annotated[ int, Field(description="A reference to the related location.", ge=0) @@ -1094,6 +1120,7 @@ class Region(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) start_line: Annotated[ int | None, @@ -1186,6 +1213,7 @@ class Replacement(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) deleted_region: Annotated[ Region, @@ -1213,6 +1241,7 @@ class ReportingDescriptor(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[str, Field(description="A stable, opaque identifier for the report.")] deprecated_ids: Annotated[ @@ -1312,6 +1341,7 @@ class SpecialLocations(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) display_base: Annotated[ ArtifactLocation | None, @@ -1333,6 +1363,7 @@ class ToolComponent(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) guid: Annotated[ str | None, @@ -1529,6 +1560,7 @@ class VersionControlDetails(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) repository_uri: Annotated[ AnyUrl, @@ -1578,6 +1610,7 @@ class WebRequest(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) index: Annotated[ int | None, @@ -1623,6 +1656,7 @@ class WebResponse(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) index: Annotated[ int | None, @@ -1676,6 +1710,7 @@ class Artifact(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) description: Annotated[ Message | None, Field(description="A short description of the artifact.") @@ -1785,6 +1820,7 @@ class ArtifactChange(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) artifact_location: Annotated[ ArtifactLocation, @@ -1813,6 +1849,7 @@ class Attachment(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) description: Annotated[ Message | None, @@ -1851,6 +1888,7 @@ class Fix(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) description: Annotated[ Message | None, @@ -1879,6 +1917,7 @@ class PhysicalLocation1(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) address: Annotated[Address, Field(description="The address of the location.")] artifact_location: Annotated[ @@ -1908,6 +1947,7 @@ class PhysicalLocation2(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) address: Annotated[ Address | None, Field(description="The address of the location.") @@ -1942,6 +1982,7 @@ class ResultProvenance(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) first_detection_time_utc: Annotated[ AwareDatetime | None, @@ -2003,6 +2044,7 @@ class Tool(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) driver: Annotated[ ToolComponent, Field(description="The analysis tool that was run.") @@ -2028,6 +2070,7 @@ class Location(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[ int | None, @@ -2083,6 +2126,7 @@ class Node(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[ str, @@ -2113,6 +2157,7 @@ class StackFrame(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) location: Annotated[ Location | None, @@ -2149,6 +2194,7 @@ class Suppression(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) guid: Annotated[ str | None, @@ -2190,6 +2236,7 @@ class Graph(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) description: Annotated[ Message | None, Field(description="A description of the graph.") @@ -2223,6 +2270,7 @@ class Stack(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) message: Annotated[ Message | None, Field(description="A message relevant to this call stack.") @@ -2247,6 +2295,7 @@ class ThreadFlowLocation(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) index: Annotated[ int | None, @@ -2339,6 +2388,7 @@ class Exception(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) kind: Annotated[ str | None, @@ -2374,6 +2424,7 @@ class Notification(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) locations: Annotated[ list[Location] | None, @@ -2439,6 +2490,7 @@ class ThreadFlow(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) id: Annotated[ str | None, @@ -2483,6 +2535,7 @@ class CodeFlow(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) message: Annotated[ Message | None, Field(description="A message relevant to the code flow.") @@ -2508,6 +2561,7 @@ class Invocation(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) command_line: Annotated[ str | None, @@ -2688,6 +2742,7 @@ class Result(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) rule_id: Annotated[ str | None, @@ -2908,6 +2963,7 @@ class Conversion(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) tool: Annotated[ Tool, Field(description="A tool object that describes the converter.") @@ -2940,6 +2996,7 @@ class ExternalProperties(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) schema_: Annotated[ AnyUrl | None, @@ -3108,6 +3165,7 @@ class Run(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) tool: Annotated[ Tool, @@ -3330,6 +3388,7 @@ class StaticAnalysisResultsFormatSarifVersion210JsonSchema(BaseModel): model_config = ConfigDict( extra="forbid", + validate_by_name=True, ) field_schema: Annotated[ AnyUrl | None, diff --git a/docs/assets/workflow.excalidraw b/docs/assets/readme/overview/workflow.excalidraw similarity index 100% rename from docs/assets/workflow.excalidraw rename to docs/assets/readme/overview/workflow.excalidraw diff --git a/docs/assets/workflow.svg b/docs/assets/readme/overview/workflow.svg similarity index 100% rename from docs/assets/workflow.svg rename to docs/assets/readme/overview/workflow.svg diff --git a/docs/assets/workflow_example.excalidraw b/docs/assets/readme/overview/workflow_example.excalidraw similarity index 100% rename from docs/assets/workflow_example.excalidraw rename to docs/assets/readme/overview/workflow_example.excalidraw diff --git a/docs/assets/workflow_example.svg b/docs/assets/readme/overview/workflow_example.svg similarity index 100% rename from docs/assets/workflow_example.svg rename to docs/assets/readme/overview/workflow_example.svg diff --git a/docs/assets/readme/report/html_finding.png b/docs/assets/readme/report/html_finding.png new file mode 100644 index 0000000..0627a23 Binary files /dev/null and b/docs/assets/readme/report/html_finding.png differ diff --git a/docs/assets/readme/report/html_hover.png b/docs/assets/readme/report/html_hover.png new file mode 100644 index 0000000..45b73bd Binary files /dev/null and b/docs/assets/readme/report/html_hover.png differ diff --git a/docs/assets/readme/report/html_report.png b/docs/assets/readme/report/html_report.png new file mode 100644 index 0000000..4b9361e Binary files /dev/null and b/docs/assets/readme/report/html_report.png differ diff --git a/docs/assets/readme/report/sarif_documentation.png b/docs/assets/readme/report/sarif_documentation.png new file mode 100644 index 0000000..abdd740 Binary files /dev/null and b/docs/assets/readme/report/sarif_documentation.png differ diff --git a/docs/assets/readme/report/sarif_triage.png b/docs/assets/readme/report/sarif_triage.png new file mode 100644 index 0000000..7ee440d Binary files /dev/null and b/docs/assets/readme/report/sarif_triage.png differ diff --git a/pyproject.toml b/pyproject.toml index 2f8e35c..3ca9bcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "CodeSecTools" -version = "0.15.2" +version = "0.16.0" description = "A framework for code security that provides abstractions for static analysis tools and datasets to support their integration, testing, and evaluation." readme = "README.md" license = "AGPL-3.0-only" diff --git a/tests/test_all_sasts.py b/tests/test_all_sasts.py index 7998fc8..233bf1c 100644 --- a/tests/test_all_sasts.py +++ b/tests/test_all_sasts.py @@ -76,7 +76,10 @@ def test_plot() -> None: def test_report() -> None: """Test the 'allsast report' command.""" logging.info("Testing All SAST report command on Java code") - result = runner.invoke(build_cli(), ["report", "dvja"]) - assert result.exit_code == 0 - assert (all_sast.output_dir / "dvja" / "report").is_dir() - assert list((all_sast.output_dir / "dvja" / "report").glob("*.html")) + for format in ["HTML", "SARIF"]: + result = runner.invoke(build_cli(), ["report", "dvja", "--format", format]) + assert result.exit_code == 0 + assert (all_sast.output_dir / "dvja" / "report").is_dir() + assert (all_sast.output_dir / "dvja" / "report" / format).glob( + f"*.{format.lower()}" + ) diff --git a/uv.lock b/uv.lock index d29c33b..0b40a24 100644 --- a/uv.lock +++ b/uv.lock @@ -239,7 +239,7 @@ wheels = [ [[package]] name = "codesectools" -version = "0.15.2" +version = "0.16.0" source = { editable = "." } dependencies = [ { name = "gitpython" },