mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
151 lines
6.5 KiB
Python
151 lines
6.5 KiB
Python
"""authorization-obligations task 3.2: reports and ranking name the obligation (offline)."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from openultrasast.findings import StaticFinding
|
||
|
|
from openultrasast.reports import write_markdown_report, write_sarif_report
|
||
|
|
|
||
|
|
|
||
|
|
def _obligation() -> StaticFinding:
|
||
|
|
return StaticFinding(
|
||
|
|
finding_id="obligation:protected_read:app.py:18:identity_constraint",
|
||
|
|
path="app.py",
|
||
|
|
title="Obligation not discharged: protected_read on book without identity_constraint",
|
||
|
|
severity="high",
|
||
|
|
confidence="low",
|
||
|
|
evidence_level="suspicion",
|
||
|
|
rationale=(
|
||
|
|
"protected_read on book in leaky is reached without a dominating identity_constraint (a identity_constraint is present "
|
||
|
|
"but bound from request_input). Why the obligation exists: app.py::constrained discharges identity_constraint. "
|
||
|
|
"Label: consistency_violation. Known fix learned from the corpus: corpus:abc."
|
||
|
|
),
|
||
|
|
line=18,
|
||
|
|
function_name="leaky",
|
||
|
|
reachability_status="unknown",
|
||
|
|
reachability_evidence=[],
|
||
|
|
reachability_conditions=[],
|
||
|
|
tags=[
|
||
|
|
"obligation:protected_read",
|
||
|
|
"discharger:identity_constraint",
|
||
|
|
"obligation_evidence:consistency_violation",
|
||
|
|
"resource:book",
|
||
|
|
"mechanism:corpus:abc",
|
||
|
|
],
|
||
|
|
ranking_priority=0.0,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _proven() -> StaticFinding:
|
||
|
|
return StaticFinding(
|
||
|
|
**{
|
||
|
|
**_obligation().__dict__,
|
||
|
|
"finding_id": "python-os-command:app.py:3",
|
||
|
|
"evidence_level": "crash_reproduced",
|
||
|
|
"tags": ["injection"],
|
||
|
|
"line": 3,
|
||
|
|
"ranking_priority": 1.0,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
OBLIGATIONS = {
|
||
|
|
"obligation:protected_read:app.py:18:identity_constraint": {
|
||
|
|
"obligation": "protected_read",
|
||
|
|
"resource": "book",
|
||
|
|
"missing": "identity_constraint",
|
||
|
|
"provenance": "request_input",
|
||
|
|
"label": "consistency_violation",
|
||
|
|
"evidence": ["app.py::constrained"],
|
||
|
|
"known_fix": "corpus:abc",
|
||
|
|
"intent": None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
MECHANISMS = {
|
||
|
|
"corpus:abc": {
|
||
|
|
"summary": "missing_auth_guard: protected_read without identity_constraint; fix binds authenticated_context",
|
||
|
|
"guard": "identity_constraint",
|
||
|
|
"pairs": ["vampi"],
|
||
|
|
"cwe": "CWE-639",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_markdown_names_obligation_missing_discharger_evidence_and_known_fix(tmp_path: Path) -> None:
|
||
|
|
path = tmp_path / "report.md"
|
||
|
|
write_markdown_report([_obligation()], path, mechanisms=MECHANISMS, obligations=OBLIGATIONS)
|
||
|
|
text = path.read_text()
|
||
|
|
assert "- Obligation: `protected_read` on `book`" in text
|
||
|
|
assert "- Missing discharger: `identity_constraint` (`request_input`)" in text
|
||
|
|
assert "- Evidence: `consistency_violation`" in text and "app.py::constrained" in text
|
||
|
|
assert "- Known fix: `identity_constraint`" in text and "vampi" in text
|
||
|
|
assert "- Evidence level: `suspicion`" in text
|
||
|
|
assert "## Obligations" in text
|
||
|
|
summary = {
|
||
|
|
"sibling_sets": 2,
|
||
|
|
"under_populated": 1,
|
||
|
|
"sets": [{"module": "api/books", "resource": "book", "handlers": 3}],
|
||
|
|
"policy_version": "abc123",
|
||
|
|
}
|
||
|
|
write_markdown_report([_obligation()], path, mechanisms=MECHANISMS, obligations=OBLIGATIONS, obligations_summary=summary)
|
||
|
|
text = path.read_text()
|
||
|
|
assert "Sibling sets evaluated: 2 (under-populated: 1)" in text and "- `api/books` / `book`: 3 handlers" in text
|
||
|
|
assert "Policy version: `abc123`" in text
|
||
|
|
|
||
|
|
|
||
|
|
def test_sarif_carries_obligation_properties(tmp_path: Path) -> None:
|
||
|
|
path = tmp_path / "report.sarif"
|
||
|
|
write_sarif_report([_obligation()], [], path, mechanisms=MECHANISMS, obligations=OBLIGATIONS)
|
||
|
|
(result,) = json.loads(path.read_text())["runs"][0]["results"]
|
||
|
|
props = result["properties"]
|
||
|
|
assert props["obligation_kind"] == "protected_read" and props["obligation_missing"] == "identity_constraint"
|
||
|
|
assert props["obligation_label"] == "consistency_violation" and props["obligation_evidence"] == ["app.py::constrained"]
|
||
|
|
assert props["obligation_known_fix"] == "corpus:abc" and props["evidence_level"] == "suspicion"
|
||
|
|
|
||
|
|
|
||
|
|
def test_obligation_ranking_weighs_sensitivity_and_label_but_stays_below_proven_findings() -> None:
|
||
|
|
from openultrasast.rank import rank_obligations
|
||
|
|
|
||
|
|
declared = StaticFinding(
|
||
|
|
**{
|
||
|
|
**_obligation().__dict__,
|
||
|
|
"finding_id": "obligation:protected_read:app.py:20:identity_constraint",
|
||
|
|
"tags": ["obligation:protected_read", "discharger:identity_constraint", "obligation_evidence:declared_policy_violation"],
|
||
|
|
"line": 20,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
local = StaticFinding(
|
||
|
|
**{
|
||
|
|
**_obligation().__dict__,
|
||
|
|
"finding_id": "obligation:protected_read:app.py:22:identity_constraint",
|
||
|
|
"tags": ["obligation:protected_read", "discharger:identity_constraint", "obligation_evidence:function_local"],
|
||
|
|
"severity": "medium",
|
||
|
|
"line": 22,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
ranked = rank_obligations([_proven(), _obligation(), declared, local])
|
||
|
|
by_id = {f.finding_id: f.ranking_priority for f in ranked}
|
||
|
|
assert by_id[declared.finding_id] > by_id[_obligation().finding_id] > by_id[local.finding_id] > 0
|
||
|
|
assert max(by_id[f] for f in by_id if f.startswith("obligation:")) < by_id["python-os-command:app.py:3"]
|
||
|
|
assert by_id["python-os-command:app.py:3"] == 1.0 # non-obligation findings untouched
|
||
|
|
|
||
|
|
|
||
|
|
def test_obligation_cap_binds_on_verdict_ids_because_the_pipeline_never_sets_proof_levels_on_static_findings() -> None:
|
||
|
|
"""Round-3 finding: proof rungs live on verdicts, so the cap is keyed on the proven finding ids the sandbox returned."""
|
||
|
|
from openultrasast.rank import rank_obligations
|
||
|
|
|
||
|
|
proven_by_sandbox = StaticFinding(**{**_proven().__dict__, "evidence_level": "static_corroboration", "ranking_priority": 0.35})
|
||
|
|
ranked = rank_obligations([proven_by_sandbox, _obligation()], proven_ids={proven_by_sandbox.finding_id})
|
||
|
|
by_id = {f.finding_id: f.ranking_priority for f in ranked}
|
||
|
|
assert 0 < by_id[_obligation().finding_id] < 0.35 and by_id[proven_by_sandbox.finding_id] == 0.35
|
||
|
|
unproven = rank_obligations([proven_by_sandbox, _obligation()])
|
||
|
|
assert {f.finding_id: f.ranking_priority for f in unproven}[_obligation().finding_id] > 0.35 # no verdict, no cap
|
||
|
|
|
||
|
|
|
||
|
|
def test_reports_without_obligations_are_unchanged(tmp_path: Path) -> None:
|
||
|
|
path = tmp_path / "report.md"
|
||
|
|
write_markdown_report([_proven()], path)
|
||
|
|
assert "Obligation:" not in path.read_text() and "## Obligations" not in path.read_text()
|