mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(three-stage-scan): select bounded regress candidates with sev-5 extras
This commit is contained in:
@@ -93,7 +93,7 @@ This plan ships the three-stage scan without rebuilding CWE policy or the regex
|
|||||||
- _Boundary: DockerCliRunner_
|
- _Boundary: DockerCliRunner_
|
||||||
- _Depends: 4.1_
|
- _Depends: 4.1_
|
||||||
|
|
||||||
- [ ] 4.3 Reject unsafe regression snippets before execution
|
- [x] 4.3 Reject unsafe regression snippets before execution
|
||||||
- Snippets that refer to the Docker socket, host network, or writes under the source mount are not executed and become `inconclusive`.
|
- Snippets that refer to the Docker socket, host network, or writes under the source mount are not executed and become `inconclusive`.
|
||||||
- Observable completion: each forbidden shape is rejected in a unit test and no runner job is created.
|
- Observable completion: each forbidden shape is rejected in a unit test and no runner job is created.
|
||||||
- _Requirements: 5.8, 6.6_
|
- _Requirements: 5.8, 6.6_
|
||||||
@@ -103,7 +103,7 @@ This plan ships the three-stage scan without rebuilding CWE policy or the regex
|
|||||||
## Phase 3 — Core: Regression verdicts
|
## Phase 3 — Core: Regression verdicts
|
||||||
|
|
||||||
- [ ] 5. Produce worth-fixing verdicts from sandboxed candidates
|
- [ ] 5. Produce worth-fixing verdicts from sandboxed candidates
|
||||||
- [ ] 5.1 Select a bounded candidate set from hotspots and severe reachable inventory
|
- [x] 5.1 Select a bounded candidate set from hotspots and severe reachable inventory
|
||||||
- Selection honors max-candidates and always includes reachable severity-5 inventory even when the ledger demoted the hotspot.
|
- Selection honors max-candidates and always includes reachable severity-5 inventory even when the ledger demoted the hotspot.
|
||||||
- Observable completion: a unit test with six hotspots and max-candidates three returns three, plus the sev-5 reachable extra when present.
|
- Observable completion: a unit test with six hotspots and max-candidates three returns three, plus the sev-5 reachable extra when present.
|
||||||
- _Requirements: 5.1, 7.4, 9.5_
|
- _Requirements: 5.1, 7.4, 9.5_
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Isolated regression snippets and verdicts."""
|
"""Isolated regression snippets and verdicts."""
|
||||||
|
|
||||||
from .candidate import INCONCLUSIVE, SAFETY_REJECTED, RegressionRunner, RegressionVerdict
|
from .candidate import INCONCLUSIVE, SAFETY_REJECTED, RegressionRunner, RegressionVerdict, select_candidates
|
||||||
from .safety import UnsafeSnippetError, check_snippet_safety
|
from .safety import UnsafeSnippetError, check_snippet_safety
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -10,4 +10,5 @@ __all__ = [
|
|||||||
"RegressionVerdict",
|
"RegressionVerdict",
|
||||||
"UnsafeSnippetError",
|
"UnsafeSnippetError",
|
||||||
"check_snippet_safety",
|
"check_snippet_safety",
|
||||||
|
"select_candidates",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
"""Safety-gated regression execution (task 4.3)."""
|
"""Candidate selection and safety-gated regression execution."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ..complexity.ledger import hotspot_key, select_forced_candidates
|
||||||
|
from ..complexity.map import Hotspot
|
||||||
|
from ..findings import StaticFinding
|
||||||
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
|
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
|
||||||
from .safety import UnsafeSnippetError, check_snippet_safety
|
from .safety import UnsafeSnippetError, check_snippet_safety
|
||||||
|
|
||||||
@@ -13,6 +17,58 @@ TRIGGERABLE = "triggerable"
|
|||||||
NOT_TRIGGERABLE = "not_triggerable"
|
NOT_TRIGGERABLE = "not_triggerable"
|
||||||
|
|
||||||
|
|
||||||
|
def select_candidates(
|
||||||
|
hotspots: Sequence[Hotspot],
|
||||||
|
findings: Sequence[StaticFinding],
|
||||||
|
*,
|
||||||
|
max_candidates: int,
|
||||||
|
policy_severity_by_id: Mapping[str, object] | None = None,
|
||||||
|
rule_cwe: Mapping[str, str] | None = None,
|
||||||
|
) -> tuple[Hotspot, ...]:
|
||||||
|
"""Select min(max_candidates, ranked hotspots) ∪ sev-5 reachable inventory.
|
||||||
|
|
||||||
|
The cap applies to the ranked hotspot slice. Forced policy-severity-5
|
||||||
|
reachable (or inferred-file-surface) inventory stays in the set even when a
|
||||||
|
ledger overlay demoted its hotspot out of that slice.
|
||||||
|
"""
|
||||||
|
ranked = tuple(sorted(hotspots, key=lambda item: (-item.score, item.path, item.function_name or "")))
|
||||||
|
selected = list(ranked[: max(max_candidates, 0)])
|
||||||
|
seen = {hotspot_key(item.path, item.function_name) for item in selected}
|
||||||
|
extras: list[Hotspot] = []
|
||||||
|
for finding in select_forced_candidates(findings, policy_severity_by_id or {}, rule_cwe=rule_cwe):
|
||||||
|
hotspot = _hotspot_for_finding(finding, ranked) or _hotspot_from_forced_finding(finding)
|
||||||
|
key = hotspot_key(hotspot.path, hotspot.function_name)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
extras.append(hotspot)
|
||||||
|
return tuple(selected + extras)
|
||||||
|
|
||||||
|
|
||||||
|
def _hotspot_for_finding(finding: StaticFinding, hotspots: Sequence[Hotspot]) -> Hotspot | None:
|
||||||
|
for hotspot in hotspots:
|
||||||
|
if finding.finding_id in hotspot.inventory_finding_ids:
|
||||||
|
return hotspot
|
||||||
|
key = hotspot_key(finding.path, finding.function_name)
|
||||||
|
for hotspot in hotspots:
|
||||||
|
if hotspot_key(hotspot.path, hotspot.function_name) == key:
|
||||||
|
return hotspot
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _hotspot_from_forced_finding(finding: StaticFinding) -> Hotspot:
|
||||||
|
return Hotspot(
|
||||||
|
path=finding.path,
|
||||||
|
function_name=finding.function_name,
|
||||||
|
score=0.0,
|
||||||
|
band="low",
|
||||||
|
signals={},
|
||||||
|
rationale="forced reachable severity-5 inventory",
|
||||||
|
test_hint=None,
|
||||||
|
inventory_finding_ids=(finding.finding_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RegressionVerdict:
|
class RegressionVerdict:
|
||||||
verdict: str
|
verdict: str
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""Bounded regression candidate selection from hotspots and severe inventory (task 5.1)."""
|
||||||
|
|
||||||
|
from openultrasast.complexity.ledger import NOT_TRIGGERABLE_DELTA, apply_overlay, record_verdict
|
||||||
|
from openultrasast.complexity.map import Hotspot
|
||||||
|
from openultrasast.findings import StaticFinding
|
||||||
|
from openultrasast.regress.candidate import select_candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _finding(
|
||||||
|
path: str,
|
||||||
|
finding_id: str,
|
||||||
|
*,
|
||||||
|
function_name: str | None = None,
|
||||||
|
reachability: str = "unknown",
|
||||||
|
severity: str = "low",
|
||||||
|
) -> StaticFinding:
|
||||||
|
return StaticFinding(
|
||||||
|
finding_id=finding_id,
|
||||||
|
path=path,
|
||||||
|
title="pattern hit",
|
||||||
|
severity=severity,
|
||||||
|
confidence="medium",
|
||||||
|
evidence_level="static_corroboration",
|
||||||
|
rationale="inventory",
|
||||||
|
line=1,
|
||||||
|
function_name=function_name,
|
||||||
|
reachability_status=reachability,
|
||||||
|
reachability_evidence=[],
|
||||||
|
reachability_conditions=[],
|
||||||
|
tags=[],
|
||||||
|
ranking_priority=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _hotspot(
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
function_name: str | None = None,
|
||||||
|
score: float = 4.0,
|
||||||
|
band: str = "medium",
|
||||||
|
inventory_finding_ids: tuple[str, ...] = (),
|
||||||
|
) -> Hotspot:
|
||||||
|
return Hotspot(
|
||||||
|
path=path,
|
||||||
|
function_name=function_name,
|
||||||
|
score=score,
|
||||||
|
band=band,
|
||||||
|
signals={"loc": 10, "inventory_hit_count": len(inventory_finding_ids)},
|
||||||
|
rationale=f"{band} band: score {score}",
|
||||||
|
test_hint=None,
|
||||||
|
inventory_finding_ids=inventory_finding_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _six_hotspots(*, sev5_id: str | None = None, sev5_score: float = 4.0) -> tuple[Hotspot, ...]:
|
||||||
|
return (
|
||||||
|
_hotspot("a.py", function_name="a", score=5.0),
|
||||||
|
_hotspot("b.py", function_name="b", score=4.8),
|
||||||
|
_hotspot("c.py", function_name="c", score=4.6),
|
||||||
|
_hotspot("d.py", function_name="d", score=4.4),
|
||||||
|
_hotspot("e.py", function_name="e", score=4.2),
|
||||||
|
_hotspot(
|
||||||
|
"f.py",
|
||||||
|
function_name="f",
|
||||||
|
score=sev5_score,
|
||||||
|
inventory_finding_ids=(sev5_id,) if sev5_id else (),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_six_hotspots_max_candidates_three_returns_three() -> None:
|
||||||
|
noise = _finding("f.py", "print:f.py:1", function_name="f", reachability="reachable")
|
||||||
|
selected = select_candidates(
|
||||||
|
_six_hotspots(),
|
||||||
|
[noise],
|
||||||
|
max_candidates=3,
|
||||||
|
policy_severity_by_id={noise.finding_id: 2},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(selected) == 3
|
||||||
|
assert [item.path for item in selected] == ["a.py", "b.py", "c.py"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sev5_reachable_extra_included_when_ledger_demoted_hotspot() -> None:
|
||||||
|
sev5_id = "cmd:f.py:1"
|
||||||
|
hotspots = _six_hotspots(sev5_id=sev5_id, sev5_score=5.2)
|
||||||
|
ledger = record_verdict({}, path="f.py", function_name="f", verdict="not_triggerable")
|
||||||
|
demoted = apply_overlay(hotspots, ledger)
|
||||||
|
finding = _finding(
|
||||||
|
"f.py",
|
||||||
|
sev5_id,
|
||||||
|
function_name="f",
|
||||||
|
reachability="reachable",
|
||||||
|
severity="critical",
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = select_candidates(
|
||||||
|
demoted,
|
||||||
|
[finding],
|
||||||
|
max_candidates=3,
|
||||||
|
policy_severity_by_id={sev5_id: 5},
|
||||||
|
)
|
||||||
|
|
||||||
|
demoted_f = next(item for item in demoted if item.path == "f.py")
|
||||||
|
assert demoted_f.score == round(5.2 + NOT_TRIGGERABLE_DELTA, 4)
|
||||||
|
assert [item.path for item in demoted[:3]] == ["a.py", "b.py", "c.py"]
|
||||||
|
assert demoted_f not in demoted[:3]
|
||||||
|
assert len(selected) == 4
|
||||||
|
assert [item.path for item in selected[:3]] == ["a.py", "b.py", "c.py"]
|
||||||
|
assert selected[3] is demoted_f
|
||||||
|
assert sev5_id in selected[3].inventory_finding_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_sev5_already_in_cap_is_not_duplicated() -> None:
|
||||||
|
sev5_id = "cmd:a.py:1"
|
||||||
|
hotspots = (
|
||||||
|
_hotspot("a.py", function_name="a", score=5.0, inventory_finding_ids=(sev5_id,)),
|
||||||
|
_hotspot("b.py", function_name="b", score=4.8),
|
||||||
|
_hotspot("c.py", function_name="c", score=4.6),
|
||||||
|
_hotspot("d.py", function_name="d", score=4.4),
|
||||||
|
_hotspot("e.py", function_name="e", score=4.2),
|
||||||
|
_hotspot("f.py", function_name="f", score=4.0),
|
||||||
|
)
|
||||||
|
finding = _finding("a.py", sev5_id, function_name="a", reachability="reachable", severity="critical")
|
||||||
|
|
||||||
|
selected = select_candidates(
|
||||||
|
hotspots,
|
||||||
|
[finding],
|
||||||
|
max_candidates=3,
|
||||||
|
policy_severity_by_id={sev5_id: 5},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(selected) == 3
|
||||||
|
assert [item.path for item in selected] == ["a.py", "b.py", "c.py"]
|
||||||
|
assert sev5_id in selected[0].inventory_finding_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_sev5_reachable_inventory_without_hotspot_is_still_selected() -> None:
|
||||||
|
finding = _finding(
|
||||||
|
"z.py",
|
||||||
|
"cmd:z.py:1",
|
||||||
|
function_name="z",
|
||||||
|
reachability="reachable",
|
||||||
|
severity="critical",
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = select_candidates(
|
||||||
|
_six_hotspots(),
|
||||||
|
[finding],
|
||||||
|
max_candidates=3,
|
||||||
|
policy_severity_by_id={finding.finding_id: 5},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(selected) == 4
|
||||||
|
assert [item.path for item in selected[:3]] == ["a.py", "b.py", "c.py"]
|
||||||
|
extra = selected[3]
|
||||||
|
assert extra.path == "z.py"
|
||||||
|
assert extra.function_name == "z"
|
||||||
|
assert extra.inventory_finding_ids == (finding.finding_id,)
|
||||||
Reference in New Issue
Block a user