- Move flat service manifests into structured subdirs (apps/, bootstrap-jobs/, repair-jobs/, migration-jobs/, validation-jobs/, node-ops/, networking/) - Retire oneoffs/ directories across services - Remove oceanus cluster and its host roles; add aether cluster + terraform scaffolding - Reorganize scripts/ into ops/, render/, sync/, manual-tests/ - Add Makefile with render/validate/test/flux targets and repo-structure tests - Update flux-system application CRs to the new paths - Add hermes-automated-triage-24h-plan knowledge doc (+ comms mirror) - Refresh knowledge catalogs, dashboards, vmalert rules, quality contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
171 lines
5.3 KiB
Python
171 lines
5.3 KiB
Python
"""Unit tests for Semgrep report normalization."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from ci.scripts import semgrep_report
|
|
|
|
|
|
def test_build_report_counts_severities_and_blocks_errors() -> None:
|
|
"""ERROR findings should fail the report while lower severities remain visible."""
|
|
|
|
report = semgrep_report.build_report(
|
|
{
|
|
"results": [
|
|
{"extra": {"severity": "ERROR"}},
|
|
{"extra": {"severity": "WARNING"}},
|
|
{"extra": {"severity": "INFO"}},
|
|
{"extra": {"severity": "unknown-ish"}},
|
|
],
|
|
"errors": [],
|
|
},
|
|
semgrep_exit_code=1,
|
|
)
|
|
|
|
assert report["status"] == "failed"
|
|
assert report["findings_total"] == 4
|
|
assert report["blocking_findings"] == 1
|
|
assert report["severity_counts"] == {
|
|
"ERROR": 1,
|
|
"WARNING": 1,
|
|
"INFO": 1,
|
|
"UNKNOWN": 1,
|
|
}
|
|
|
|
|
|
def test_build_report_marks_clean_scan_ok() -> None:
|
|
"""Clean Semgrep JSON with exit code zero should publish an OK report."""
|
|
|
|
report = semgrep_report.build_report(
|
|
{"results": [], "errors": []},
|
|
semgrep_exit_code=0,
|
|
)
|
|
|
|
assert report["status"] == "ok"
|
|
assert report["blocking_findings"] == 0
|
|
assert report["errors_total"] == 0
|
|
|
|
|
|
def test_build_sonar_issues_converts_findings() -> None:
|
|
"""Semgrep findings should be importable by SonarQube generic issues."""
|
|
|
|
issues = semgrep_report.build_sonar_issues(
|
|
{
|
|
"results": [
|
|
{
|
|
"check_id": "python.lang.security.audit.danger",
|
|
"path": "app/main.py",
|
|
"start": {"line": 7},
|
|
"end": {"line": 9},
|
|
"extra": {"severity": "ERROR", "message": "dangerous call"},
|
|
},
|
|
{"check_id": "missing-path", "extra": {"severity": "INFO"}},
|
|
]
|
|
}
|
|
)
|
|
|
|
assert len(issues["issues"]) == 1
|
|
assert issues["issues"][0]["engineId"] == "semgrep"
|
|
assert issues["issues"][0]["severity"] == "CRITICAL"
|
|
assert issues["issues"][0]["primaryLocation"]["filePath"] == "app/main.py"
|
|
assert issues["issues"][0]["primaryLocation"]["textRange"] == {
|
|
"startLine": 7,
|
|
"endLine": 9,
|
|
}
|
|
|
|
|
|
def test_build_sonar_issues_uses_safe_defaults_for_sparse_findings() -> None:
|
|
"""Sparse findings should still produce valid file-level Sonar issues."""
|
|
|
|
issues = semgrep_report.build_sonar_issues(
|
|
{
|
|
"results": [
|
|
{
|
|
"path": "app/config.yaml",
|
|
"start": {"line": "bad"},
|
|
"end": {"line": 0},
|
|
"extra": "not-a-dict",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
issue = issues["issues"][0]
|
|
assert issue["ruleId"] == "semgrep.unknown"
|
|
assert issue["type"] == "CODE_SMELL"
|
|
assert issue["severity"] == "INFO"
|
|
assert issue["primaryLocation"]["message"] == "Semgrep finding"
|
|
assert issue["primaryLocation"]["textRange"] == {"startLine": 1, "endLine": 1}
|
|
|
|
|
|
def test_read_json_handles_invalid_json_and_non_object(tmp_path: Path) -> None:
|
|
"""Report loading should fail closed for bad JSON and wrong top-level shapes."""
|
|
|
|
bad_json = tmp_path / "bad.json"
|
|
bad_json.write_text("{bad", encoding="utf-8")
|
|
list_json = tmp_path / "list.json"
|
|
list_json.write_text("[]", encoding="utf-8")
|
|
|
|
assert semgrep_report._read_json(bad_json)["errors"][0]["message"].startswith("invalid JSON:")
|
|
assert semgrep_report._read_json(list_json) == {
|
|
"errors": [{"message": "report payload is not an object"}],
|
|
"results": [],
|
|
}
|
|
|
|
|
|
def test_main_handles_missing_or_invalid_input(tmp_path: Path) -> None:
|
|
"""Missing or invalid Semgrep JSON should still produce a failed report file."""
|
|
|
|
output = tmp_path / "build" / "semgrep-report.json"
|
|
|
|
rc = semgrep_report.main(
|
|
[
|
|
"--semgrep-json",
|
|
str(tmp_path / "missing.json"),
|
|
"--exit-code",
|
|
"2",
|
|
"--output",
|
|
str(output),
|
|
"--sonar-issues-output",
|
|
str(tmp_path / "build" / "sonar-issues.json"),
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
payload = json.loads(output.read_text(encoding="utf-8"))
|
|
assert payload["status"] == "failed"
|
|
assert payload["errors_total"] == 1
|
|
sonar_payload = json.loads((tmp_path / "build" / "sonar-issues.json").read_text(encoding="utf-8"))
|
|
assert sonar_payload == {"issues": []}
|
|
|
|
|
|
def test_main_honors_custom_blocking_severity_without_sonar_output(tmp_path: Path) -> None:
|
|
"""CLI callers can choose stricter blocking severities without writing Sonar issues."""
|
|
|
|
semgrep_json = tmp_path / "semgrep.json"
|
|
semgrep_json.write_text(
|
|
json.dumps({"results": [{"path": "app.py", "extra": {"severity": "WARNING"}}], "errors": []}),
|
|
encoding="utf-8",
|
|
)
|
|
output = tmp_path / "semgrep-report.json"
|
|
|
|
rc = semgrep_report.main(
|
|
[
|
|
"--semgrep-json",
|
|
str(semgrep_json),
|
|
"--exit-code",
|
|
"1",
|
|
"--output",
|
|
str(output),
|
|
"--blocking-severity",
|
|
"WARNING",
|
|
]
|
|
)
|
|
|
|
assert rc == 0
|
|
payload = json.loads(output.read_text(encoding="utf-8"))
|
|
assert payload["status"] == "failed"
|
|
assert payload["blocking_findings"] == 1
|