Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion stellargate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from stellargate.aggregator import all_findings, run_all
from stellargate.config import VALID_SEVERITIES, Config, ConfigError
from stellargate.report import gate_passed, to_json, to_markdown
from stellargate.report import gate_passed, to_json, to_markdown, to_sarif

logger = logging.getLogger("stellargate")

Expand Down Expand Up @@ -56,6 +56,7 @@ def main(argv: list[str] | None = None) -> int:
)
run_parser.add_argument("--config", default="stellargate.yaml")
run_parser.add_argument("--json-report", default=None, help="Write JSON report to this path")
run_parser.add_argument("--sarif-report", default=None, help="Write SARIF report to this path")
run_parser.add_argument("--md-report", default=None, help="Write Markdown report to this path")
run_parser.add_argument("--fail-on", default=None, help="Override fail_on threshold from config")

Expand Down Expand Up @@ -113,6 +114,10 @@ def _run(args: argparse.Namespace) -> int:
with open(args.json_report, "w") as f:
json.dump(to_json(results, fail_on, passed), f, indent=2)

if args.sarif_report:
with open(args.sarif_report, "w") as f:
json.dump(to_sarif(results, fail_on, passed), f, indent=2)

if args.md_report:
with open(args.md_report, "w") as f:
f.write(to_markdown(results, fail_on, passed))
Expand Down
60 changes: 60 additions & 0 deletions stellargate/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,66 @@ def to_json(results: list[ToolRunResult], fail_on: str, gate_passed: bool) -> di
}


SEVERITY_TO_LEVEL = {
"critical": "error",
"high": "error",
"medium": "warning",
"low": "note",
}


def to_sarif(results: list[ToolRunResult], fail_on: str, gate_passed: bool) -> dict:
findings = [f for r in results for f in r.findings]

rules = {}
for f in findings:
if f.rule_id in rules:
continue
rules[f.rule_id] = {
"id": f.rule_id,
"name": f.rule_id,
"shortDescription": {
"text": f"{f.tool}: {f.message}",
},
}

sarif_results = []
for f in findings:
result = {
"ruleId": f.rule_id,
"level": SEVERITY_TO_LEVEL[f.severity],
"message": {"text": f.message},
}
if f.location:
result["locations"] = [
{
"physicalLocation": {
"artifactLocation": {"uri": f.location},
}
}
]
sarif_results.append(result)

run = {
"tool": {
"driver": {
"name": "stellargate",
"version": "0.1.0",
"informationUri": "https://github.com/BreachDirect/stellargate",
}
},
"results": sarif_results,
}
if rules:
run["tool"]["driver"]["rules"] = list(rules.values())

return {
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [run],
}


def to_markdown(results: list[ToolRunResult], fail_on: str, gate_passed: bool) -> str:
findings = [f for r in results for f in r.findings]
lines: list[str] = []
Expand Down
83 changes: 82 additions & 1 deletion tests/test_aggregator_and_report.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from unittest.mock import patch
from unittest.mock import mock_open, patch

from hypothesis import given, settings
from hypothesis import strategies as st

from stellargate.aggregator import ToolRunResult, all_findings, run_all
from stellargate.cli import _run
from stellargate.config import Config, ToolConfig
from stellargate.report import gate_passed, to_json, to_markdown
from stellargate.schema import SEVERITY_ORDER, Finding
Expand Down Expand Up @@ -63,6 +64,86 @@ def test_to_markdown_shows_failed_status_and_tool_error():
assert "STELLAR-001" in md


def test_to_sarif_has_schema_version_and_runs():
results = make_results()
sarif = to_sarif(results, "high", False)
assert sarif["$schema"] == "https://json.schemastore.org/sarif-2.1.0.json"
assert sarif["version"] == "2.1.0"
assert isinstance(sarif["runs"], list) and len(sarif["runs"]) == 1
driver = sarif["runs"][0]["tool"]["driver"]
assert driver["name"] == "stellargate"
assert driver["version"] == "0.1.0"


def test_to_sarif_severity_level_mapping():
findings = [
Finding("t", "R1", "critical", "m"),
Finding("t", "R2", "high", "m"),
Finding("t", "R3", "medium", "m"),
Finding("t", "R4", "low", "m"),
]
sarif = to_sarif(
[ToolRunResult("t", findings, None)], "high", False
)
levels = [
SEVERITY_TO_LEVEL[f.severity]
for f in findings
]
assert levels == ["error", "error", "warning", "note"]
result_levels = [r["level"] for r in sarif["runs"][0]["results"]]
assert result_levels == levels


def test_to_sarif_creates_one_result_per_finding_with_ruleid_and_location():
results = make_results()
sarif = to_sarif(results, "high", False)
sarif_results = sarif["runs"][0]["results"]
assert len(sarif_results) == 2
assert [r["ruleId"] for r in sarif_results] == ["AUTH-001", "STELLAR-001"]
auth = [r for r in sarif_results if r["ruleId"] == "AUTH-001"][0]
uri = auth["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
assert uri == "vault.rs:42"


def test_to_sarif_empty_findings_yields_empty_results():
sarif = to_sarif([ToolRunResult("t", [], None)], "high", True)
assert sarif["runs"][0]["results"] == []
assert "rules" not in sarif["runs"][0]["tool"]["driver"]


def test_cli_writes_sarif_report_file():
from types import SimpleNamespace

args = SimpleNamespace(
config="stellargate.example.yaml",
fail_on=None,
json_report=None,
sarif_report="build/report.sarif",
md_report=None,
)
config = Config(
target=".",
fail_on="high",
tools={},
)
with patch("stellargate.cli.Config.load", return_value=config), patch(
"stellargate.cli.run_all", return_value=make_results()
) as mock_run_all, patch(
"stellargate.cli.gate_passed", return_value=False
), patch("builtins.open", mock_open()) as mock_file:
_run(args)
mock_run_all.assert_called_once()
handle = mock_file()
written = "".join(call.args[0] for call in handle.write.call_args_list)
import json

data = json.loads(written)
assert data["$schema"].endswith("sarif-2.1.0.json")
assert data["version"] == "2.1.0"
assert len(data["runs"]) == 1
assert data["runs"][0]["tool"]["driver"]["name"] == "stellargate"


def test_run_all_survives_an_unexpected_adapter_exception():
"""Regression test: a bug in one adapter (e.g. a bare KeyError, not an
AdapterError) must not crash the whole run — every other tool's findings
Expand Down
Loading