2026-06-17 21:13:51 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-06-18 10:16:25 +02:00
|
|
|
import json
|
2026-09-03 19:44:04 +02:00
|
|
|
from collections.abc import Sequence
|
2026-06-18 10:16:25 +02:00
|
|
|
from dataclasses import asdict
|
2026-06-17 21:13:51 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from .findings import StaticFinding
|
2026-06-18 10:16:25 +02:00
|
|
|
from .run import ScanRun
|
|
|
|
|
from .verification import VerificationResult
|
2026-06-17 21:13:51 +02:00
|
|
|
|
|
|
|
|
|
2026-06-22 20:00:42 +02:00
|
|
|
def write_markdown_report(
|
|
|
|
|
findings: list[StaticFinding], path: Path, verifications: list[VerificationResult] | None = None, *, redact: bool = True
|
|
|
|
|
) -> None:
|
2026-06-17 21:13:51 +02:00
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
2026-06-18 10:16:25 +02:00
|
|
|
verification_by_id = _verification_by_id(verifications or [])
|
2026-09-03 23:14:00 +02:00
|
|
|
lines = ["# OpenUltraSAST Report", "", f"Findings: {len(findings)}", "", "## Inventory", ""]
|
2026-06-17 21:13:51 +02:00
|
|
|
if not findings:
|
|
|
|
|
lines.append("No quick-mode findings were emitted.")
|
|
|
|
|
for finding in findings:
|
2026-06-18 10:16:25 +02:00
|
|
|
verification = verification_by_id.get(finding.finding_id)
|
2026-06-17 21:13:51 +02:00
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
f"## {finding.title}",
|
|
|
|
|
"",
|
|
|
|
|
f"- ID: `{finding.finding_id}`",
|
|
|
|
|
f"- Path: `{finding.path}`",
|
|
|
|
|
f"- Line: `{finding.line}`",
|
2026-06-18 07:57:06 +02:00
|
|
|
f"- Function: `{finding.function_name or 'unknown'}`",
|
2026-06-17 21:13:51 +02:00
|
|
|
f"- Severity: `{finding.severity}`",
|
|
|
|
|
f"- Confidence: `{finding.confidence}`",
|
|
|
|
|
f"- Evidence level: `{finding.evidence_level}`",
|
2026-06-18 10:16:25 +02:00
|
|
|
f"- Verification: `{verification.status if verification else 'not_run'}`",
|
2026-06-18 07:57:06 +02:00
|
|
|
f"- Reachability: `{finding.reachability_status}`",
|
|
|
|
|
f"- Conditions: `{', '.join(finding.reachability_conditions) or 'none'}`",
|
2026-06-17 21:13:51 +02:00
|
|
|
f"- Ranking priority: `{finding.ranking_priority}`",
|
|
|
|
|
f"- Tags: `{', '.join(finding.tags)}`",
|
|
|
|
|
"",
|
|
|
|
|
finding.rationale,
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
2026-06-22 20:00:42 +02:00
|
|
|
report = "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
if redact:
|
|
|
|
|
from .redaction import redact_secrets
|
|
|
|
|
|
|
|
|
|
report = redact_secrets(report)
|
|
|
|
|
path.write_text(report)
|
2026-06-18 10:16:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_sarif_report(findings: list[StaticFinding], verifications: list[VerificationResult], path: Path) -> None:
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
verification_by_id = _verification_by_id(verifications)
|
|
|
|
|
rules = _sarif_rules(findings)
|
|
|
|
|
payload = {
|
|
|
|
|
"version": "2.1.0",
|
|
|
|
|
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
|
|
|
"runs": [
|
|
|
|
|
{
|
|
|
|
|
"tool": {
|
|
|
|
|
"driver": {
|
|
|
|
|
"name": "OpenUltraSAST",
|
|
|
|
|
"informationUri": "https://github.com/norandom/OpenUltraSAST",
|
|
|
|
|
"rules": list(rules.values()),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"results": [_sarif_result(finding, verification_by_id.get(finding.finding_id)) for finding in findings],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_manifest(
|
|
|
|
|
*,
|
|
|
|
|
run: ScanRun,
|
|
|
|
|
findings: list[StaticFinding],
|
|
|
|
|
verifications: list[VerificationResult],
|
|
|
|
|
artifact_paths: dict[str, Path],
|
|
|
|
|
path: Path,
|
2026-06-22 12:31:35 +02:00
|
|
|
score: dict[str, object] | None = None,
|
2026-06-22 16:29:52 +02:00
|
|
|
degradations: list[dict[str, object]] | None = None,
|
2026-06-22 18:57:03 +02:00
|
|
|
fusion: list[dict[str, object]] | None = None,
|
2026-09-03 23:14:00 +02:00
|
|
|
stages: dict[str, object] | None = None,
|
2026-09-03 23:26:54 +02:00
|
|
|
complexity: dict[str, object] | None = None,
|
2026-06-18 10:16:25 +02:00
|
|
|
) -> None:
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
verification_by_id = _verification_by_id(verifications)
|
2026-06-22 12:31:35 +02:00
|
|
|
payload: dict[str, object] = {
|
2026-06-18 10:16:25 +02:00
|
|
|
"scan_id": run.scan_id,
|
|
|
|
|
"target": str(run.target),
|
|
|
|
|
"artifacts": {name: _relative_artifact(run.root, artifact) for name, artifact in sorted(artifact_paths.items())},
|
|
|
|
|
"findings": [
|
|
|
|
|
{
|
|
|
|
|
"finding_id": finding.finding_id,
|
|
|
|
|
"path": finding.path,
|
|
|
|
|
"line": finding.line,
|
|
|
|
|
"severity": finding.severity,
|
|
|
|
|
"evidence_level": finding.evidence_level,
|
|
|
|
|
"verification_status": verification_by_id[finding.finding_id].status
|
|
|
|
|
if finding.finding_id in verification_by_id
|
|
|
|
|
else "not_run",
|
2026-06-18 10:59:25 +02:00
|
|
|
"artifact_refs": _artifact_refs(run.root, artifact_paths),
|
2026-06-18 10:16:25 +02:00
|
|
|
}
|
|
|
|
|
for finding in findings
|
|
|
|
|
],
|
|
|
|
|
}
|
2026-06-22 12:31:35 +02:00
|
|
|
if score is not None:
|
|
|
|
|
payload["score"] = score
|
2026-06-22 16:29:52 +02:00
|
|
|
if degradations is not None:
|
|
|
|
|
payload["degradations"] = degradations
|
2026-06-22 18:57:03 +02:00
|
|
|
if fusion is not None:
|
|
|
|
|
payload["fusion"] = fusion
|
2026-09-03 23:14:00 +02:00
|
|
|
if stages is not None:
|
|
|
|
|
payload["stages"] = stages
|
2026-09-03 23:26:54 +02:00
|
|
|
if complexity is not None:
|
|
|
|
|
payload["complexity"] = complexity
|
2026-06-18 10:16:25 +02:00
|
|
|
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 19:44:04 +02:00
|
|
|
def scan_exit_code(
|
|
|
|
|
findings: list[StaticFinding],
|
|
|
|
|
verifications: list[VerificationResult],
|
|
|
|
|
policy: str,
|
|
|
|
|
worth_fixing_verdicts: Sequence[object] | None = None,
|
|
|
|
|
) -> int:
|
2026-06-18 10:16:25 +02:00
|
|
|
if policy == "never":
|
|
|
|
|
return 0
|
|
|
|
|
if policy == "findings":
|
|
|
|
|
return 1 if findings else 0
|
|
|
|
|
if policy == "verified":
|
|
|
|
|
return 1 if any(result.verified for result in verifications) else 0
|
2026-09-03 19:44:04 +02:00
|
|
|
if policy == "worth-fixing":
|
|
|
|
|
return 1 if any(getattr(verdict, "worth_fixing", False) for verdict in worth_fixing_verdicts or ()) else 0
|
2026-06-18 10:16:25 +02:00
|
|
|
raise ValueError(f"unknown fail policy: {policy}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sarif_rules(findings: list[StaticFinding]) -> dict[str, dict[str, object]]:
|
|
|
|
|
rules: dict[str, dict[str, object]] = {}
|
|
|
|
|
for finding in findings:
|
|
|
|
|
rule_id = _rule_id(finding)
|
|
|
|
|
rules.setdefault(
|
|
|
|
|
rule_id,
|
|
|
|
|
{
|
|
|
|
|
"id": rule_id,
|
|
|
|
|
"name": finding.title,
|
|
|
|
|
"shortDescription": {"text": finding.title},
|
|
|
|
|
"properties": {"tags": finding.tags},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return rules
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sarif_result(finding: StaticFinding, verification: VerificationResult | None) -> dict[str, object]:
|
|
|
|
|
return {
|
|
|
|
|
"ruleId": _rule_id(finding),
|
|
|
|
|
"level": _sarif_level(finding.severity),
|
|
|
|
|
"message": {"text": finding.rationale},
|
|
|
|
|
"locations": [
|
|
|
|
|
{
|
|
|
|
|
"physicalLocation": {
|
|
|
|
|
"artifactLocation": {"uri": finding.path},
|
|
|
|
|
"region": {"startLine": finding.line or 1},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"partialFingerprints": {"openultrasastFindingId": finding.finding_id},
|
|
|
|
|
"properties": {
|
|
|
|
|
"finding_id": finding.finding_id,
|
|
|
|
|
"severity": finding.severity,
|
|
|
|
|
"confidence": finding.confidence,
|
|
|
|
|
"evidence_level": finding.evidence_level,
|
|
|
|
|
"verification": asdict(verification) if verification else None,
|
|
|
|
|
"reachability_status": finding.reachability_status,
|
|
|
|
|
"reachability_conditions": finding.reachability_conditions,
|
|
|
|
|
"ranking_priority": finding.ranking_priority,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rule_id(finding: StaticFinding) -> str:
|
|
|
|
|
return finding.finding_id.split(":", 1)[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sarif_level(severity: str) -> str:
|
|
|
|
|
if severity in {"critical", "high"}:
|
|
|
|
|
return "error"
|
|
|
|
|
if severity == "medium":
|
|
|
|
|
return "warning"
|
|
|
|
|
return "note"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verification_by_id(verifications: list[VerificationResult]) -> dict[str, VerificationResult]:
|
|
|
|
|
return {result.finding_id: result for result in verifications}
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 10:59:25 +02:00
|
|
|
def _artifact_refs(root: Path, artifact_paths: dict[str, Path]) -> dict[str, str]:
|
|
|
|
|
refs = {
|
|
|
|
|
"findings_json": _relative_artifact(root, artifact_paths["findings"]),
|
|
|
|
|
"verification_json": _relative_artifact(root, artifact_paths["verification"]),
|
|
|
|
|
"markdown": _relative_artifact(root, artifact_paths["markdown"]),
|
|
|
|
|
"sarif": _relative_artifact(root, artifact_paths["sarif"]),
|
|
|
|
|
}
|
|
|
|
|
if "trajectories" in artifact_paths:
|
|
|
|
|
refs["trajectories_jsonl"] = _relative_artifact(root, artifact_paths["trajectories"])
|
|
|
|
|
return refs
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 10:16:25 +02:00
|
|
|
def _relative_artifact(root: Path, path: Path) -> str:
|
|
|
|
|
try:
|
|
|
|
|
return path.relative_to(root).as_posix()
|
|
|
|
|
except ValueError:
|
|
|
|
|
return path.as_posix()
|