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.
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* |
+|:---:|:---:|:---:|
+|  |  |  |
+
+#### 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* |
+|:---:|:---:|
+|  |  |
+
\ 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" },