feat(model-grounded-detection): group 1, subtract the noise architecture

99 source modules -> 81. 832 tests -> 646. The whole learning/ package (16
modules, ~4,100 lines) and six semantic/ modules removed, with the three gates
byte-identical to 2026-09-06-gate-baseline.txt at every step.

The sequence is what made it safe: vocabularies ported first (1.1), importers
detached second (1.2-1.3), deletion third (1.4). Every import break during the
deletion was therefore a detachment miss in the importer and was fixed there --
no module was restored to satisfy one. Both misses were the same shape and were
fixed as a class: operations.py and shapes.py each imported trailing_name from
the deleted variants.py, so it moved to semantic/ir.py, its natural home, and
both importers changed in one edit.

Ported before deleting: the closed guard vocabulary (verbatim, nothing else
holds it) into model/specs.py, which derives TaintSpec and DominanceSpec from
the retained ruleset facts rather than copying them. Re-homed: families ->
model/taxonomy, candidates -> model/candidates, endpoint -> model/endpoint,
plus Region, labeled_family and labeled_spans. labeled_family no longer falls
back to the LLM classifier -- an unlabeled pair is honestly unknown.

Four deviations, each recorded in the spec notes with its reason. improve/
evolve.py and validator.py were stripped, not deleted: each mixes the mechanism
lever (gone) with the deterministic rule-status loop `ousast improve` still
runs, which has no LLM in it; a test now asserts the lever's absence by symbol
so it cannot creep back. The learning CLI group became `model candidates`,
because deleting it outright would have orphaned the retained enumerator by
removing its only entry point. thinking moved from [learning] to [models].

A gap named rather than papered over: no retained sink fact routes by CWE to
path, output_encoding, untrusted_destination or prototype -- the same hole the
entailment ceiling diagnosed. The test asserts the seeded set and the gap set
exactly, so task 4.2 must update it when it authors those sinks.

The audit found exactly one genuine orphan: vectorstore.py, a bakeoff harness
with no production caller and no CLI, alive only through its own test. The five
named subsystems each carry an explicit decision. Manifest committed, with a
test asserting the tree still matches it.

Group 2 (the Joern go/no-go) has not started.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-08 12:15:54 +02:00
co-authored by Claude Opus 5
parent fc4dab97fb
commit 9ed0e45c5f
86 changed files with 1710 additions and 12158 deletions
-5
View File
@@ -3,26 +3,21 @@
from .engines import joern_available, tree_sitter_available
from .extra import grammar_for, has_semantic_extra
from .facts import FactLoadError, SemanticFacts, load_facts
from .mechanisms import Mechanism, MechanismStore, append_mechanism, order_promotions
from .overlay import OverlayError, OverlayRecord, adjudicate, finding_from_coverage, write_overlay
from .prove_filter import filter_promoted_hotspots
__all__ = [
"FactLoadError",
"Mechanism",
"MechanismStore",
"OverlayError",
"OverlayRecord",
"SemanticFacts",
"adjudicate",
"append_mechanism",
"filter_promoted_hotspots",
"finding_from_coverage",
"grammar_for",
"has_semantic_extra",
"joern_available",
"load_facts",
"order_promotions",
"tree_sitter_available",
"write_overlay",
]
+12
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import ast
import re
from dataclasses import dataclass
@@ -230,3 +231,14 @@ def _unparse(node: ast.AST) -> str:
return ast.unparse(node)
except Exception:
return ""
def trailing_name(callee: str) -> str:
"""``os.path.join`` -> ``join``; ``Runtime.getRuntime().exec`` -> ``exec``; keeps the last identifier segment.
Re-homed here from the deleted ``semantic/variants.py``: it is a pure helper over ``CallSite.name`` and has
nothing to do with the mechanism search that used to surround it.
"""
tail = callee.rsplit(".", 1)[-1].rsplit("::", 1)[-1].rsplit("->", 1)[-1]
match = re.search(r"[A-Za-z_$][\w$]*\s*$", tail)
return match.group(0).strip() if match else tail
-304
View File
@@ -1,304 +0,0 @@
"""Leave-one-out: the pair corpus's own detection rate (corpus-seeded-mechanisms, Req 4).
For every scorable pair, a temporary mechanism store is seeded from every *other* trusted pair, both excerpts are
materialized and searched for variants, and the pair is scored with the pair rules: detected when a hit lies inside the
labeled function and names the labeled mechanism (or sink), silent when the fixed side yields no hit. Only ``seeded`` and
``reviewed`` pairs teach; ``advisory`` and ``title`` pairs are scored as held-out targets but never seed. Reported, never
gated.
Obligation-labeled rows (authorization-obligations, Req 7.4) run the obligation checker on both sides with the held-out
store's obligation shapes: detected when a finding tagged with the labeled obligation lies inside the labeled function *and*
carries a ``known_fix`` supplied by the store (the checker's own facts-only finding is not the corpus teaching), silent when
the fixed side carries no such finding; ``found_by`` names the shape record and the pairs that taught it.
"""
from __future__ import annotations
import tempfile
from collections.abc import Sequence
from dataclasses import dataclass, field
from pathlib import Path
from ..findings import StaticFinding
from ..pairs import PairCase, _materialize, _targets
from .facts import FactLoadError, SemanticFacts, load_facts
from .functions import named_function_ranges, spans_named
from .mechanisms import Mechanism, MechanismStore, append_from_pair, corpus_mechanisms
from .obligations import check_obligations, findings_to_static, load_obligation_facts, obligation_mechanisms
from .obligations.dominance import OrderDominance
from .seed import Lesson, _parse_language, pair_lessons
from .variant_search import search_tree
from .variants import Shape, trailing_name
@dataclass(frozen=True)
class LooOutcome:
pair: str
slice: str
provenance: str
mechanisms: tuple[str, ...] # labeled mechanism ids of the held-out pair
detected: bool
silent: bool
found_by: tuple[tuple[str, tuple[str, ...]], ...] # (mechanism record id, pairs that taught it) for detecting hits
hits_vuln: int
hits_fixed: int
teaches: bool # the pair contributed at least one shape when others were held out
leaked_by: tuple[str, ...] = () # mechanism record ids that hit the fixed side
degradations: tuple[dict[str, object], ...] = () # from variant search on either side
obligations: tuple[str, ...] = () # labeled obligation kinds of the held-out pair (Req 7.4)
def to_dict(self) -> dict[str, object]:
return {
"pair": self.pair,
"slice": self.slice,
"provenance": self.provenance,
"mechanisms": list(self.mechanisms),
"detected": self.detected,
"silent": self.silent,
"found_by": [{"mechanism_id": mechanism_id, "pairs": list(pairs)} for mechanism_id, pairs in self.found_by],
"hits_vuln": self.hits_vuln,
"hits_fixed": self.hits_fixed,
"teaches": self.teaches,
"leaked_by": list(self.leaked_by),
"obligations": list(self.obligations),
}
@dataclass(frozen=True)
class LooResult:
outcomes: tuple[LooOutcome, ...]
per_slice: dict[str, dict[str, float]]
per_profile: dict[str, dict[str, float]]
per_mechanism: dict[str, dict[str, float]]
skipped: tuple[tuple[str, str], ...] = ()
per_obligation_kind: dict[str, dict[str, float]] = field(default_factory=dict)
teaching_pairs: int = 0
degradations: tuple[dict[str, object], ...] = field(default=())
def to_dict(self) -> dict[str, object]:
return {
"per_slice": self.per_slice,
"per_profile": self.per_profile,
"per_mechanism": self.per_mechanism,
"per_obligation_kind": self.per_obligation_kind,
"outcomes": [outcome.to_dict() for outcome in self.outcomes],
"skipped": [{"pair": pair, "reason": reason} for pair, reason in self.skipped],
"teaching_pairs": self.teaching_pairs,
"degradations": list(self.degradations),
}
def evaluate_loo(cases: Sequence[PairCase], *, facts: SemanticFacts | None = None) -> LooResult:
loaded = facts if facts is not None else _facts()
targets: list[PairCase] = []
skipped: list[tuple[str, str]] = []
for case in cases:
if case.unscorable or case.known_limit:
# `PairCase.unscorable` verbatim when the loader computed one: one spelling per corpus fact, and it
# already carries the `known_limit:` prefix when the limit is a maintainer's own word rather than one
# of the vocabulary's. A case built by hand may carry only the declaration.
skipped.append((case.name, case.unscorable or f"known_limit:{case.known_limit}"))
elif not case.vuln_file.is_file() or not case.fixed_file.is_file():
skipped.append((case.name, "pointer_pair_not_cached" if not case.vendored else "excerpt_missing"))
else:
targets.append(case)
from ..learning.split import GATING_TIERS, holdout_names, is_teacher, refuse_if_holdout
# learning-harness Req 5.1: every pair is still scored, but only a train-split teacher may seed a store.
lessons = {case.name: pair_lessons(case, loaded) for case in targets if is_teacher(case)}
outcomes = [_hold_out(case, targets, lessons, loaded) for case in targets]
# Req 5.2: the holdout pairs kept out of the teaching set are named, not silently dropped. Non-teachers held
# back for their tier or their unscorable reason are already in `skipped` with that reason.
excluded = [case.name for case in targets if case.name not in lessons]
refusal = refuse_if_holdout(excluded, targets)
refusals = (refusal.degradation(),) if refusal is not None else ()
withheld = tuple(
(case.name, "tier" if case.review_tier not in GATING_TIERS else "unscorable")
for case in targets
if case.name in set(excluded) - set(holdout_names(targets))
)
skipped.extend((name, f"not_a_teacher:{why}") for name, why in withheld)
return LooResult(
outcomes=tuple(outcomes),
per_slice=_group(outcomes, lambda item: (item.slice,)),
per_profile=_group(outcomes, lambda item: (item.provenance,)),
per_mechanism=_group(outcomes, lambda item: item.mechanisms),
per_obligation_kind=_group(outcomes, lambda item: item.obligations),
skipped=tuple(skipped),
teaching_pairs=sum(1 for shapes in lessons.values() if shapes),
degradations=(*refusals, *_unique_degradations(outcomes)),
)
def _unique_degradations(outcomes: Sequence[LooOutcome]) -> tuple[dict[str, object], ...]:
seen: set[str] = set()
unique: list[dict[str, object]] = []
for outcome in outcomes:
for item in outcome.degradations:
key = repr(sorted(item.items()))
if key not in seen:
seen.add(key)
unique.append(item)
return tuple(unique)
def _hold_out(case: PairCase, targets: Sequence[PairCase], lessons: dict[str, list[Lesson]], facts: SemanticFacts) -> LooOutcome:
with tempfile.TemporaryDirectory(prefix="ousast-loo-") as scratch:
root = Path(scratch)
store = MechanismStore(root / "mechanisms.jsonl")
for teacher in targets:
if teacher.name == case.name:
continue
for lesson in lessons.get(teacher.name, ()):
append_from_pair(
store,
lesson.shape,
summary=lesson.summary,
cwe=lesson.cwe,
pair=teacher.name,
provenance=teacher.provenance,
tier=teacher.review_tier,
)
scored = score_pair_with_store(case, store, facts, root=root)
return LooOutcome(**{**scored.__dict__, "teaches": bool(lessons.get(case.name))})
def score_pair_with_store(case: PairCase, store: MechanismStore, facts: SemanticFacts, *, root: Path | None = None) -> LooOutcome:
"""Search both sides of one pair with a fixed store and score it with the pair rules (shared by LOO and the lever)."""
labeled_mechanisms = tuple(sorted({row.mechanism for row in case.expected if row.mechanism}))
labeled_obligations = tuple(sorted({str(row.obligation) for row in case.expected if getattr(row, "obligation", None)}))
loaded = store.load()
records = {record.id: record for record in corpus_mechanisms(loaded)}
obligation_vuln: list[StaticFinding] = []
obligation_fix: list[StaticFinding] = []
with tempfile.TemporaryDirectory(prefix="ousast-loo-pair-") as scratch:
base = root if root is not None else Path(scratch)
vuln_root = _materialize(base / "vuln", case.vuln_file, case.relpath)
fix_root = _materialize(base / "fixed", case.fixed_file, case.relpath)
vuln_search = search_tree(vuln_root, _targets(vuln_root), store, facts, max_mechanisms=len(records) or 1)
fix_search = search_tree(fix_root, _targets(fix_root), store, facts, max_mechanisms=len(records) or 1)
vuln_hits, fix_hits = vuln_search.hits, fix_search.hits
if labeled_obligations:
shapes = obligation_mechanisms(loaded)
obligation_vuln = _obligation_findings(vuln_root, shapes)
obligation_fix = _obligation_findings(fix_root, shapes)
text = case.vuln_file.read_text(errors="ignore")
ranges = named_function_ranges(case.relpath, text, _parse_language(case)) or ()
found: list[tuple[str, tuple[str, ...]]] = []
for finding in obligation_vuln:
record = _known_fix_record(finding, records)
if record is None or finding.line is None or not _inside_label(case, finding.line, ranges):
continue
if any(f"obligation:{kind}" in finding.tags for kind in labeled_obligations):
found.append((record.id, tuple(record.pairs)))
obligation_leaks = tuple(
sorted({record.id for finding in obligation_fix if (record := _known_fix_record(finding, records)) is not None})
)
for hit in vuln_hits:
record = records.get(hit.mechanism_id)
if record is None or not _inside_label(case, hit.line, ranges):
continue
if _names_label(case, record, hit.sink_name):
found.append((record.id, tuple(record.pairs)))
return LooOutcome(
pair=case.name,
slice=case.slice,
provenance=case.provenance,
mechanisms=labeled_mechanisms,
detected=bool(found),
silent=not fix_hits and not obligation_leaks,
found_by=tuple(found),
hits_vuln=len(vuln_hits) + len(obligation_vuln),
hits_fixed=len(fix_hits) + len(obligation_fix),
teaches=False,
leaked_by=tuple(sorted({hit.mechanism_id for hit in fix_hits})) + obligation_leaks,
degradations=tuple(vuln_search.degradations) + tuple(fix_search.degradations),
obligations=labeled_obligations,
)
def _obligation_findings(root: Path, shapes: Sequence[object]) -> list[StaticFinding]:
"""Function-local obligation findings for one materialized side, with the held-out store's obligation shapes as known fixes."""
from ..config import ObligationsConfig
from ..mapping import analyze_entry_points
from .ir import parse_file
targets = _targets(root)
irs: dict[str, tuple[object, str]] = {}
texts: dict[str, str] = {}
for target in targets:
try:
text = (root / target.path).read_text(errors="ignore")
except OSError:
continue
texts[target.path] = text
irs[target.path] = (parse_file(target.path, text, target.language), text)
result = check_obligations(
irs=irs, # type: ignore[arg-type]
entries=analyze_entry_points(root, targets),
facts=load_obligation_facts(),
flow_facts=_facts(),
policy=None,
paths=(),
dominance=OrderDominance(texts=texts),
store_shapes=shapes,
min_siblings=ObligationsConfig().min_siblings,
)
return findings_to_static(result)
def _known_fix_record(finding: StaticFinding, records: dict[str, Mechanism]) -> Mechanism | None:
for tag in finding.tags:
if tag.startswith("mechanism:"):
return records.get(tag.split(":", 1)[1])
return None
def _inside_label(case: PairCase, line: int, ranges: Sequence[tuple[str, int, int]]) -> bool:
functions = {row.function for row in case.expected if row.function}
if not functions:
return True
return any(start <= line <= end for function in functions for start, end in spans_named(ranges, function))
def _names_label(case: PairCase, record: Mechanism, sink_name: str) -> bool:
"""The hit names the pair's labeled mechanism, or its labeled sink when the row carries one."""
shape = Shape.from_dict(record.shape or {})
for row in case.expected:
if row.mechanism and row.mechanism == shape.mechanism:
return True
if row.sink and trailing_name(row.sink) == sink_name:
return True
return False
def _group(outcomes: Sequence[LooOutcome], keys: object) -> dict[str, dict[str, float]]:
buckets: dict[str, list[LooOutcome]] = {}
for outcome in outcomes:
for key in keys(outcome): # type: ignore[operator]
buckets.setdefault(str(key), []).append(outcome)
return {name: _metrics(items) for name, items in sorted(buckets.items())}
def _metrics(outcomes: Sequence[LooOutcome]) -> dict[str, float]:
pairs = len(outcomes)
detected = sum(1 for item in outcomes if item.detected)
silent = sum(1 for item in outcomes if item.silent)
recall = detected / pairs if pairs else 0.0
silence = silent / pairs if pairs else 0.0
return {
"pairs": pairs,
"detected": detected,
"silent": silent,
"recall": recall,
"silence": silence,
"youden": recall + silence - 1.0 if pairs else 0.0,
"teaching": sum(1 for item in outcomes if item.teaches),
}
def _facts() -> SemanticFacts:
try:
return load_facts()
except FactLoadError:
return SemanticFacts(version="", sources=(), sinks=(), sanitizers=())
-363
View File
@@ -1,363 +0,0 @@
"""Append-only mechanism memory. OpenRouter embeddings rank prove budget; never demote."""
from __future__ import annotations
import hashlib
import json
import math
import os
import uuid
from collections.abc import Sequence
from dataclasses import asdict, dataclass
from pathlib import Path
from ..complexity.map import Hotspot
from ..findings import StaticFinding
from ..index import CodeChunk, EmbeddingClient, VectorIndex, VectorRecord, write_vector_index
from .obligations.shapes import ObligationShape
from .overlay import OverlayRecord
from .variants import Shape
DEFAULT_MECHANISM_LOG = Path(".openultrasast/mechanisms.jsonl")
@dataclass(frozen=True)
class Mechanism:
id: str
summary: str
cwe: str
language: str
tags: tuple[str, ...]
keywords: tuple[str, ...]
what_made_it_exploitable: str
source_finding_id: str = ""
source_repo: str = ""
# corpus-seeded-mechanisms (additive; old rows load with these defaults)
origin: str = "sandbox" # "sandbox" (proven finding) | "corpus" (derived from a trusted pair)
review_tier: str = ""
pairs: tuple[str, ...] = ()
shape: dict[str, object] | None = None # Shape.to_dict(); None on sandbox rows (not searchable)
guard: str = "none"
retracted: bool = False # tombstone row appended by the improve lever; load() folds the record away
class MechanismStore:
def __init__(self, log_path: Path, cache_path: Path | None = None) -> None:
self.log_path = log_path
self.cache_path = cache_path if cache_path is not None else log_path.with_suffix(".index.json")
def load(self) -> tuple[Mechanism, ...]:
if not self.log_path.is_file():
return ()
by_id: dict[str, Mechanism] = {} # append-only log; the latest row for an id is its current state
for line in self.log_path.read_text().splitlines():
if not line.strip():
continue
payload = json.loads(line)
record = _mechanism_from_payload(payload)
if record.retracted:
by_id.pop(record.id, None)
else:
by_id[record.id] = record
return tuple(by_id.values())
def append(self, mechanism: Mechanism) -> None:
self.log_path.parent.mkdir(parents=True, exist_ok=True)
with self.log_path.open("a") as handle:
handle.write(json.dumps(asdict(mechanism), sort_keys=True) + "\n")
def append_mechanism(
store: MechanismStore,
*,
summary: str,
cwe: str,
language: str,
tags: Sequence[str],
what_made_it_exploitable: str,
source_finding_id: str = "",
source_repo: str = "",
keywords: Sequence[str] = (),
) -> Mechanism:
mechanism = Mechanism(
id=str(uuid.uuid4()),
summary=summary,
cwe=cwe,
language=language,
tags=tuple(tags),
keywords=tuple(keywords),
what_made_it_exploitable=what_made_it_exploitable,
source_finding_id=source_finding_id,
source_repo=source_repo,
)
store.append(mechanism)
return mechanism
_TIER_RANK = {"seeded": 1, "reviewed": 2}
def corpus_mechanism_id(shape: Shape | ObligationShape) -> str:
"""Deterministic id from the shape key, so the same shape gets the same record on every machine (lever-addressable)."""
return "corpus:" + hashlib.sha1(shape.key().encode()).hexdigest()[:16]
def append_from_pair(
store: MechanismStore,
shape: Shape | ObligationShape,
*,
summary: str,
cwe: str,
pair: str,
provenance: str,
tier: str,
) -> Mechanism:
"""Second writer (Req 1.3, 1.4): one record per shape, ``origin = "corpus"``, provenance lists every pair that taught it."""
mechanism_id = corpus_mechanism_id(shape)
existing = next((item for item in store.load() if item.id == mechanism_id), None)
pairs = tuple(existing.pairs) if existing is not None else ()
if pair not in pairs:
pairs = (*pairs, pair)
if existing is not None and _TIER_RANK.get(existing.review_tier, 0) > _TIER_RANK.get(tier, 0):
tier = existing.review_tier # a shape keeps the strongest review it ever had
inherited = tuple(tag for tag in (existing.tags if existing is not None else ()) if tag.startswith(("provenance:", "tier:")))
if isinstance(shape, Shape):
positions = ", ".join(f"arg{p} ({k})" for p, k in zip(shape.source_positions, shape.source_kinds, strict=True))
exploitable = f"{positions} reaches {shape.sink_name}/{shape.arity}; the fix added {shape.guard}"
keywords: tuple[str, ...] = (shape.sink_name, shape.guard, *shape.source_kinds)
guard = shape.guard
else: # obligation shape: the discharger the fix carries is the 'guard' the reports show
exploitable = f"{shape.operation_kind} reached without {shape.discharger_kind}; the fix binds {shape.provenance}"
keywords = (shape.operation_kind, shape.discharger_kind, shape.provenance, shape.resource_class)
guard = shape.discharger_kind
record = Mechanism(
id=mechanism_id,
summary=summary,
cwe=cwe,
language=shape.language,
tags=(
shape.mechanism,
f"mechanism:{shape.mechanism}",
f"guard:{guard}",
*sorted({*inherited, f"provenance:{provenance}", f"tier:{tier}"}),
),
keywords=keywords,
what_made_it_exploitable=exploitable,
source_finding_id="",
source_repo="",
origin="corpus",
review_tier=tier,
pairs=pairs,
shape=shape.to_dict(),
guard=guard,
)
store.append(record)
return record
def corpus_mechanisms(records: Sequence[Mechanism]) -> tuple[Mechanism, ...]:
"""Records that variant search may use: corpus origin with a shape."""
return tuple(item for item in records if item.origin == "corpus" and item.shape is not None)
def order_promotions(
hotspots: Sequence[Hotspot],
records: Sequence[OverlayRecord],
findings: Sequence[StaticFinding],
*,
store: MechanismStore | None = None,
client: EmbeddingClient | None = None,
model: str | None = None,
) -> tuple[tuple[Hotspot, ...], str | None]:
"""Heuristic order, then optional OpenRouter cosine boost. Never drops a promotion."""
heuristic = tuple(sorted(hotspots, key=lambda item: (-item.score, item.path, item.function_name or "")))
if store is None:
return heuristic, None
mechanisms = store.load()
if not mechanisms:
return heuristic, None
if client is None or not model:
return heuristic, "embeddings_unavailable"
overlay_by_id = {record.proposal_id: record for record in records}
finding_by_id = {finding.finding_id: finding for finding in findings}
try:
ranked = _rank_with_embeddings(heuristic, overlay_by_id, finding_by_id, mechanisms, store, client, model)
except Exception:
return heuristic, "embed_failed"
return ranked, None
def overlay_text(record: OverlayRecord) -> str:
parts = [record.cwe, record.reason, *record.sources, *record.sinks, *record.sanitizers]
return " ".join(part for part in parts if part)
def mechanism_text(mechanism: Mechanism) -> str:
return " ".join(
part
for part in (mechanism.summary, mechanism.what_made_it_exploitable, mechanism.cwe, *mechanism.tags, *mechanism.keywords)
if part
)
def _rank_with_embeddings(
hotspots: tuple[Hotspot, ...],
overlay_by_id: dict[str, OverlayRecord],
finding_by_id: dict[str, StaticFinding],
mechanisms: tuple[Mechanism, ...],
store: MechanismStore,
client: EmbeddingClient,
model: str,
) -> tuple[Hotspot, ...]:
vectors = _mechanism_vectors(mechanisms, store, client, model)
scored: list[tuple[float, Hotspot]] = []
for hotspot in hotspots:
record = _overlay_for_hotspot(hotspot, overlay_by_id)
finding = _finding_for_hotspot(hotspot, finding_by_id)
language = record.language if record is not None else ""
cwe = record.cwe if record is not None else ""
tags = tuple(finding.tags) if finding is not None else ()
filtered = [item for item in mechanisms if _hard_filter(item, language, cwe, tags)]
boost = 0.0
if record is not None and filtered:
query = client.embed(model=model, inputs=[overlay_text(record)])[0]
for mechanism in filtered:
vector = vectors.get(mechanism.id)
if vector is None:
continue
boost = max(boost, _cosine(query, vector))
scored.append((hotspot.score + boost, hotspot))
scored.sort(key=lambda item: (-item[0], item[1].path, item[1].function_name or ""))
return tuple(item[1] for item in scored)
def _mechanism_vectors(
mechanisms: tuple[Mechanism, ...],
store: MechanismStore,
client: EmbeddingClient,
model: str,
) -> dict[str, list[float]]:
vectors: dict[str, list[float]] = {}
missing: list[Mechanism] = []
cached = _read_cache(store.cache_path, model)
for mechanism in mechanisms:
key = f"{mechanism.id}:{model}"
if key in cached:
vectors[mechanism.id] = cached[key]
else:
missing.append(mechanism)
if missing:
embeddings = client.embed(model=model, inputs=[mechanism_text(item) for item in missing])
for mechanism, embedding in zip(missing, embeddings, strict=True):
vectors[mechanism.id] = embedding
records = [
VectorRecord(
chunk=CodeChunk(
chunk_id=f"{mechanism.id}:{model}",
namespace="mechanisms",
path=mechanism.id,
language=mechanism.language or "text",
start_line=1,
end_line=1,
text=mechanism_text(mechanism),
metadata={"mechanism_id": mechanism.id, "model": model, "cwe": mechanism.cwe, "language": mechanism.language},
),
embedding=vectors[mechanism.id],
)
for mechanism in mechanisms
if mechanism.id in vectors
]
index = VectorIndex(
store="json-local",
embedding_model=model,
repo_root=str(store.log_path.parent),
repo_commit=None,
records=records,
)
write_vector_index(index, store.cache_path)
return vectors
def _read_cache(path: Path, model: str) -> dict[str, list[float]]:
if not path.is_file():
return {}
try:
payload = json.loads(path.read_text())
except json.JSONDecodeError:
return {}
if str(payload.get("embedding_model")) != model:
return {}
cached: dict[str, list[float]] = {}
for item in payload.get("records", []):
chunk = item.get("chunk", {})
metadata = chunk.get("metadata", {})
mechanism_id = str(metadata.get("mechanism_id", ""))
if mechanism_id:
cached[f"{mechanism_id}:{model}"] = [float(value) for value in item.get("embedding", [])]
return cached
def _hard_filter(mechanism: Mechanism, language: str, cwe: str, tags: Sequence[str]) -> bool:
if mechanism.language and language and mechanism.language != language:
return False
if mechanism.cwe and cwe and mechanism.cwe != cwe:
return False
tag_ok = not mechanism.tags or not tags or bool(set(mechanism.tags) & set(tags))
return tag_ok
def _overlay_for_hotspot(hotspot: Hotspot, overlay_by_id: dict[str, OverlayRecord]) -> OverlayRecord | None:
for finding_id in hotspot.inventory_finding_ids:
record = overlay_by_id.get(finding_id)
if record is not None:
return record
return None
def _finding_for_hotspot(hotspot: Hotspot, finding_by_id: dict[str, StaticFinding]) -> StaticFinding | None:
for finding_id in hotspot.inventory_finding_ids:
finding = finding_by_id.get(finding_id)
if finding is not None:
return finding
return None
def _cosine(left: list[float], right: list[float]) -> float:
if len(left) != len(right):
return 0.0
numerator = sum(a * b for a, b in zip(left, right, strict=True))
left_norm = math.sqrt(sum(a * a for a in left))
right_norm = math.sqrt(sum(b * b for b in right))
if left_norm == 0 or right_norm == 0:
return 0.0
return numerator / (left_norm * right_norm)
def _mechanism_from_payload(payload: dict[str, object]) -> Mechanism:
tags = payload.get("tags") or []
keywords = payload.get("keywords") or []
pairs = payload.get("pairs") or []
shape = payload.get("shape")
if not isinstance(tags, list) or not isinstance(keywords, list) or not isinstance(pairs, list):
raise ValueError("mechanism tags, keywords and pairs must be lists")
return Mechanism(
id=str(payload["id"]),
summary=str(payload.get("summary", "")),
cwe=str(payload.get("cwe", "")),
language=str(payload.get("language", "")),
tags=tuple(str(item) for item in tags),
keywords=tuple(str(item) for item in keywords),
what_made_it_exploitable=str(payload.get("what_made_it_exploitable", "")),
source_finding_id=str(payload.get("source_finding_id", "")),
source_repo=str(payload.get("source_repo", "")),
origin=str(payload.get("origin", "sandbox")),
review_tier=str(payload.get("review_tier", "")),
pairs=tuple(str(item) for item in pairs),
shape=dict(shape) if isinstance(shape, dict) else None, # validated where it is used (search_tree, the lever)
guard=str(payload.get("guard", "none")),
retracted=bool(payload.get("retracted", False)),
)
def embeddings_configured(model: str | None) -> bool:
return bool(model) and bool(os.environ.get("OPENROUTER_API_KEY"))
@@ -14,8 +14,7 @@ from collections.abc import Sequence
from dataclasses import dataclass
from ..facts import SemanticFacts
from ..ir import FileIR, FunctionIR
from ..variants import trailing_name
from ..ir import FileIR, FunctionIR, trailing_name
from .facts import DischargerFact, ObligationFacts
from .shapes import _call_matches, _constraint_provenance, _is_literal, _operations, _permissive
@@ -17,8 +17,7 @@ from dataclasses import dataclass
from ..facts import SemanticFacts
from ..functions import named_ranges_from_ir
from ..ir import CallSite, FileIR, FunctionIR
from ..variants import trailing_name
from ..ir import CallSite, FileIR, FunctionIR, trailing_name
from .facts import DISCHARGER_KINDS, OPERATION_KINDS, PROVENANCE_KINDS, DischargerFact, ObligationFacts, OperationFact
FAMILY = "obligation"
@@ -1,12 +0,0 @@
"""Prove-budget ranker: heuristic order plus optional mechanism recall."""
from .mechanisms import MechanismStore, append_mechanism, order_promotions
from .prove_filter import filter_promoted_hotspots, promoted_findings
__all__ = [
"MechanismStore",
"append_mechanism",
"filter_promoted_hotspots",
"order_promotions",
"promoted_findings",
]
-231
View File
@@ -1,231 +0,0 @@
"""Export trusted pairs as mechanism records (corpus-seeded-mechanisms, Req 1).
Only ``seeded`` and ``reviewed`` pairs seed; ``advisory`` and ``title`` pairs, ``known_limit`` pairs, pointer pairs
without a cached excerpt, and excerpts that fail to parse are skipped with a reason. Runs offline over the vendored
catalog; nothing here fetches, and no code text enters the store.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from pathlib import Path
from ..pairs import GATING_TIERS, PairCase
from ..preprocess import LANGUAGE_BY_EXTENSION
from .facts import FactLoadError, SemanticFacts, load_facts
from .ir import FileIR, parse_file
from .mechanisms import MechanismStore, append_from_pair
from .obligations.facts import ObligationFacts, load_obligation_facts
from .obligations.shapes import ABSENCE_MECHANISMS, ObligationShape, derive_obligation, explain_skip
from .variants import Shape, derive_shape
LANGUAGE_UNSUPPORTED = "variants_language_unsupported"
@dataclass(frozen=True)
class ExportReport:
seeded: int # pairs that produced at least one record
records: int # distinct records written (deduped by shape)
skipped: tuple[tuple[str, str], ...] = () # (pair, reason)
degradations: tuple[dict[str, object], ...] = ()
seeded_pairs: tuple[str, ...] = field(default=())
def to_dict(self) -> dict[str, object]:
return {
"seeded": self.seeded,
"records": self.records,
"skipped": [{"pair": pair, "reason": reason} for pair, reason in self.skipped],
"degradations": list(self.degradations),
"seeded_pairs": list(self.seeded_pairs),
}
def export_mechanisms(cases: Sequence[PairCase], store: MechanismStore, *, facts: SemanticFacts | None = None) -> ExportReport:
"""Derive one shape per labeled expected row of every trusted pair and append it to ``store`` (deduped by shape)."""
loaded = facts if facts is not None else _facts()
skipped: list[tuple[str, str]] = []
degradations: list[dict[str, object]] = []
seeded: list[str] = []
record_ids: set[str] = set()
unsupported_languages: set[str] = set()
for case in cases: # catalog order; the store lists pairs in the order they taught
reason = _skip_reason(case)
if reason is not None:
skipped.append((case.name, reason))
continue
vuln_text = _read(case.vuln_file)
fixed_text = _read(case.fixed_file)
language = _parse_language(case)
vuln_ir = parse_file(case.relpath, vuln_text, language)
fixed_ir = parse_file(case.relpath, fixed_text, language)
if not vuln_ir.parse_ok or not fixed_ir.parse_ok:
bad = vuln_ir if not vuln_ir.parse_ok else fixed_ir
if bad.reason == "language_unsupported":
skipped.append((case.name, LANGUAGE_UNSUPPORTED))
if language not in unsupported_languages:
unsupported_languages.add(language)
degradations.append({"stage": "mechanisms", "reason": LANGUAGE_UNSUPPORTED, "language": language})
else:
skipped.append((case.name, "parse_failed"))
continue
if not any(row.function and row.mechanism for row in case.expected):
skipped.append((case.name, "no_labeled_function"))
continue
lessons = pair_lessons(case, loaded, vuln_ir=vuln_ir, fixed_ir=fixed_ir, vuln_text=vuln_text, fixed_text=fixed_text)
for lesson in lessons:
record = append_from_pair(
store,
lesson.shape,
summary=lesson.summary,
cwe=lesson.cwe,
pair=case.name,
provenance=case.provenance,
tier=case.review_tier,
)
record_ids.add(record.id)
if lessons:
seeded.append(case.name)
else:
skipped.append((case.name, _skip_detail(case, vuln_ir, fixed_ir, vuln_text, fixed_text, loaded)))
return ExportReport(
seeded=len(seeded),
records=len(record_ids),
skipped=tuple(skipped),
degradations=tuple(degradations),
seeded_pairs=tuple(seeded),
)
@dataclass(frozen=True)
class Lesson:
"""One shape a trusted pair teaches, with the CWE and a text-free summary (a sink shape or an obligation shape)."""
shape: Shape | ObligationShape
cwe: str
summary: str
def pair_lessons(
case: PairCase,
facts: SemanticFacts,
*,
vuln_ir: FileIR | None = None,
fixed_ir: FileIR | None = None,
vuln_text: str | None = None,
fixed_text: str | None = None,
) -> list[Lesson]:
"""Shapes derived from every labeled row of a pair (shared by the exporter and leave-one-out). Empty when a side fails to parse."""
vuln_text = vuln_text if vuln_text is not None else _read(case.vuln_file)
fixed_text = fixed_text if fixed_text is not None else _read(case.fixed_file)
language = _parse_language(case)
vuln_ir = vuln_ir if vuln_ir is not None else parse_file(case.relpath, vuln_text, language)
fixed_ir = fixed_ir if fixed_ir is not None else parse_file(case.relpath, fixed_text, language)
if not vuln_ir.parse_ok or not fixed_ir.parse_ok:
return []
lessons: list[Lesson] = []
for row in case.expected:
if not row.function or not row.mechanism:
continue
obligation_kind = getattr(row, "obligation", None)
if obligation_kind or row.mechanism in ABSENCE_MECHANISMS:
obligation = derive_obligation(
vuln_ir,
fixed_ir,
function=row.function,
obligation=obligation_kind,
mechanism=row.mechanism,
facts=_obligation_facts(),
flow_facts=facts,
vuln_text=vuln_text,
fixed_text=fixed_text,
)
if obligation is not None:
summary = (
f"{obligation.mechanism}: {obligation.operation_kind} without {obligation.discharger_kind} ({row.cwe}); "
f"fix binds {obligation.provenance}"
)
lessons.append(Lesson(shape=obligation, cwe=row.cwe, summary=summary))
continue
# an absence row that teaches no obligation may still teach a sink shape (a weak literal at a sink, say)
shape = derive_shape(
vuln_ir,
fixed_ir,
function=row.function,
sink=row.sink,
line=row.line,
mechanism=row.mechanism,
facts=facts,
vuln_text=vuln_text,
fixed_text=fixed_text,
cwe=row.cwe or None,
)
if shape is not None:
summary = f"{shape.mechanism}: {'/'.join(shape.source_kinds)} into {shape.sink_name} ({row.cwe}); fix adds {shape.guard}"
lessons.append(Lesson(shape=shape, cwe=row.cwe, summary=summary))
return lessons
def _skip_detail(case: PairCase, vuln_ir: FileIR, fixed_ir: FileIR, vuln_text: str, fixed_text: str, flow_facts: SemanticFacts) -> str:
"""Why a parsing, labeled pair taught nothing: the obligation derivation's reason for absence rows, else the sink reason."""
for row in case.expected:
if row.function and row.mechanism and (getattr(row, "obligation", None) or row.mechanism in ABSENCE_MECHANISMS):
return explain_skip(
vuln_ir,
fixed_ir,
function=row.function,
vuln_text=vuln_text,
fixed_text=fixed_text,
obligation=getattr(row, "obligation", None),
facts=_obligation_facts(),
flow_facts=flow_facts,
)
return "no_labeled_sink_call_site"
_OBLIGATION_FACTS: list[ObligationFacts] = []
def _obligation_facts() -> ObligationFacts:
if not _OBLIGATION_FACTS:
_OBLIGATION_FACTS.append(load_obligation_facts())
return _OBLIGATION_FACTS[0]
def _skip_reason(case: PairCase) -> str | None:
from ..learning.split import is_teacher
if case.review_tier not in GATING_TIERS:
return f"tier:{case.review_tier}"
if case.unscorable or case.known_limit:
# `PairCase.unscorable` verbatim when the loader computed one, the same string every other path reports:
# `identical_twin` declared in the catalog and `identical_twin` computed from the bytes are one fact, and
# calling either of them `split:train` is simply untrue. A case built by hand may carry only the
# declaration (Req 5.1, 10.3).
return case.unscorable or f"known_limit:{case.known_limit}"
if not is_teacher(case) and case.vendored:
return f"split:{case.split}"
if not case.vuln_file.is_file() or not case.fixed_file.is_file():
return "pointer_pair_not_cached" if not case.vendored else "excerpt_missing"
return None
def _parse_language(case: PairCase) -> str:
by_ext = LANGUAGE_BY_EXTENSION.get(case.vuln_file.suffix.lower())
if by_ext:
return str(by_ext)
return {"c_cpp": "c"}.get(case.language, case.language)
def _read(path: Path) -> str:
try:
return path.read_text(errors="ignore")
except OSError:
return ""
def _facts() -> SemanticFacts:
try:
return load_facts()
except FactLoadError:
return SemanticFacts(version="", sources=(), sinks=(), sanitizers=())
@@ -1,125 +0,0 @@
"""Variant search over a scanned tree and its merge with overlay flows (corpus-seeded-mechanisms, Req 3).
Depends on the shapes (``variants``), the store (``mechanisms``), the overlay records and ``StaticFinding``; the shape
module itself stays free of those imports.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, replace
from pathlib import Path
from ..findings import StaticFinding
from ..preprocess import FileTarget
from .facts import SemanticFacts
from .ir import parse_file
from .mechanisms import MechanismStore, corpus_mechanisms
from .overlay import OverlayRecord
from .variants import Shape, VariantHit, match_shapes
LANGUAGE_UNSUPPORTED = "variants_language_unsupported"
@dataclass(frozen=True)
class SearchResult:
hits: tuple[VariantHit, ...]
mechanisms_searched: int
files_searched: int
degradations: tuple[dict[str, object], ...] = ()
def search_tree(
root: Path,
targets: Sequence[FileTarget],
store: MechanismStore,
facts: SemanticFacts,
*,
max_mechanisms: int,
) -> SearchResult:
"""Variant search over the parsed files of a tree with the store's corpus shapes (bounded by ``max_mechanisms``)."""
records = [record for record in corpus_mechanisms(store.load()) if (record.shape or {}).get("family", "sink") == "sink"]
records = records[: max(max_mechanisms, 0)]
shapes: list[Shape] = []
ids: dict[str, str] = {}
degradations: list[dict[str, object]] = []
for record in records:
assert record.shape is not None
try:
shape = Shape.from_dict(record.shape)
except (ValueError, TypeError) as exc: # one malformed operator-visible row must not abort the scan
degradations.append(
{"stage": "variants", "reason": "variants_store_row_invalid", "mechanism_id": record.id, "detail": str(exc)[:120]}
)
continue
shapes.append(shape)
ids[shape.key()] = record.id
if not shapes:
return SearchResult(hits=(), mechanisms_searched=0, files_searched=0, degradations=tuple(degradations))
hits: list[VariantHit] = []
files = 0
unsupported: set[str] = set()
for target in targets:
try:
text = (root / target.path).read_text(errors="ignore")
except OSError:
continue
ir = parse_file(target.path, text, target.language)
if not ir.parse_ok:
if ir.reason == "language_unsupported" and target.language not in unsupported:
unsupported.add(target.language)
degradations.append({"stage": "variants", "reason": "variants_language_unsupported", "language": target.language})
continue
files += 1
hits.extend(match_shapes(ir, shapes, facts, mechanism_ids=ids))
return SearchResult(hits=tuple(hits), mechanisms_searched=len(shapes), files_searched=files, degradations=tuple(degradations))
def hits_to_findings(
hits: Sequence[VariantHit],
records: Sequence[OverlayRecord],
summaries: Mapping[str, tuple[str, tuple[str, ...], str]],
) -> tuple[list[StaticFinding], list[OverlayRecord]]:
"""Merge hits into overlay records at the same call site (Req 3.3); the rest become ``suspicion`` findings (Req 3.2)."""
by_site: dict[tuple[str, int | None], list[int]] = {}
for index, record in enumerate(records):
if record.disposition in {"promote", "coverage"} and record.sources and record.sinks:
by_site.setdefault((record.path, record.line), []).append(index)
updated = list(records)
findings: list[StaticFinding] = []
emitted: set[str] = set()
for hit in hits:
indexes = by_site.get((hit.path, hit.line))
if indexes:
for index in indexes:
if updated[index].mechanism_id is None:
updated[index] = replace(updated[index], mechanism_id=hit.mechanism_id)
continue
finding_id = f"variant:{hit.mechanism_id}:{hit.path}:{hit.line}"
if finding_id in emitted:
continue
emitted.add(finding_id)
summary, pairs, cwe = summaries.get(hit.mechanism_id, (hit.mechanism_id, (), ""))
taught_by = ", ".join(pairs) if pairs else "the mechanism store"
findings.append(
StaticFinding(
finding_id=finding_id,
path=hit.path,
title=f"Variant of known mechanism: {hit.sink_name}",
severity="medium",
confidence="low",
evidence_level="suspicion",
rationale=(
f"{summary}. Structural variant ({hit.source_kind} reaches {hit.sink_name}) of a mechanism learned from "
f"{taught_by}; suspicion until the overlay or the sandbox agrees."
),
line=hit.line,
function_name=None,
reachability_status="unknown",
reachability_evidence=[],
reachability_conditions=[],
tags=["variant", f"mechanism:{hit.mechanism_id}", *([f"cwe:{cwe}"] if cwe else [])],
ranking_priority=0.0,
)
)
return findings, updated
-347
View File
@@ -1,347 +0,0 @@
"""Structural mechanism shapes derived from labeled pairs (corpus-seeded-mechanisms, Req 2).
A shape is the closed, text-free description of how a known vulnerable call looked: the
sink's trailing callee name, its arity, which argument positions carried source-derived
values and of what kind, and the kind of guard the fix added. Shapes are derived from
``FileIR`` (never from regex over code) and compared structurally, so a variant in another
project matches even when every identifier differs. Stdlib plus ``.ir``, ``.facts`` and
``.functions`` only; never imports the overlay, the scorer, or the store.
"""
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from .facts import SemanticFacts, SinkFact
from .functions import named_ranges_from_ir
from .ir import Bind, CallSite, FileIR, FunctionIR
GUARD_KINDS = ("null_test", "bounds_test", "allowlist_test", "auth_check", "parameterized_call", "type_change", "none")
SOURCE_KINDS = ("fact_source", "parameter", "container_read")
_MAX_BIND_DEPTH = 8
# Closed guard classification over the statements the fix added inside the labeled function.
# Order matters: the first matching kind wins. These classify the *fix*, they never detect.
_GUARD_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
("parameterized_call", re.compile(r"execute\w*\([^,]+,\s*[\(\[]|prepareStatement\(|\?\s*['\"]?\s*[,)]|\$\d\b|%s['\"]\s*,\s*[\(\[]")),
(
"auth_check",
re.compile(
r"\b(request\.user|req\.user|current_user|request\.state|session\[|is_authenticated|authoriz|require_auth"
r"|login_required|verify_token|check_permission|owner_id\s*[!=]=)"
),
),
(
"allowlist_test",
re.compile(
r"\bnot in\b|\bin\s*[\(\{\[]|\.includes\(|\.has\(|\bstartswith\(|\bstartsWith\(|\bendswith\(|\bendsWith\("
r"|re\.(?:match|fullmatch)\(|\.test\(|allow_?list|whitelist|\bALLOWED\b|\bsafe_join\(|secure_filename\("
r"|\brealpath\(|\bnormpath\(|\bresolve\("
),
),
("bounds_test", re.compile(r"\blen\(|\.length\b|\bsizeof\(|\bstrn?len\(|\bsnprintf\(|\bstrlcpy\(|\bmin\(|\bmax\(|\s(?:<|>|<=|>=)\s")),
(
"null_test",
re.compile(
r"\bis None\b|\bis not None\b|[!=]= *NULL\b|[!=]==? *null\b|\bnullptr\b|\bif\s*\(\s*!\s*\w|\bif not \w+\s*:"
r"|\bif\s+\w+\s+is\s+None|\?\?|\?\."
),
),
(
"type_change",
re.compile(r"\bint\(|\bparseInt\(|\bNumber\(|\(int\)|\(size_t\)|\(unsigned\b|\bstr\(|\.toString\(|\bstatic_cast<|\bBigInt\("),
),
)
# Design §Shape: a container read is a subscript or a `.get(` read; source names themselves live in facts, never here.
_CONTAINER_READ = re.compile(r"\[[^\]]+\]|\.get\(")
@dataclass(frozen=True)
class Shape:
language: str
sink_name: str # trailing callee segment
arity: int
source_positions: tuple[int, ...]
source_kinds: tuple[str, ...]
guard: str # GUARD_KINDS
mechanism: str # closed vocabulary id from the pair label
def key(self) -> str:
"""Stable identity of the shape; carries no path, line, or literal."""
positions = ",".join(str(p) for p in self.source_positions)
kinds = ",".join(self.source_kinds)
return f"{self.language}|{self.sink_name}/{self.arity}|pos={positions}|kinds={kinds}|guard={self.guard}|{self.mechanism}"
def to_dict(self) -> dict[str, object]:
return {
"language": self.language,
"sink_name": self.sink_name,
"arity": self.arity,
"source_positions": list(self.source_positions),
"source_kinds": list(self.source_kinds),
"guard": self.guard,
"mechanism": self.mechanism,
}
@classmethod
def from_dict(cls, payload: dict[str, object]) -> Shape:
positions = payload.get("source_positions") or []
kinds = payload.get("source_kinds") or []
if not isinstance(positions, list) or not isinstance(kinds, list):
raise ValueError("shape positions and kinds must be lists")
guard = str(payload.get("guard", "none"))
if guard not in GUARD_KINDS:
raise ValueError(f"unknown guard kind {guard!r}")
return cls(
language=str(payload.get("language", "")),
sink_name=str(payload.get("sink_name", "")),
arity=int(str(payload.get("arity", 0))),
source_positions=tuple(int(p) for p in positions),
source_kinds=tuple(str(k) for k in kinds),
guard=guard,
mechanism=str(payload.get("mechanism", "other")),
)
def trailing_name(callee: str) -> str:
"""``os.path.join`` -> ``join``; ``Runtime.getRuntime().exec`` -> ``exec``; keeps the last identifier segment."""
tail = callee.rsplit(".", 1)[-1].rsplit("::", 1)[-1].rsplit("->", 1)[-1]
match = re.search(r"[A-Za-z_$][\w$]*\s*$", tail)
return match.group(0).strip() if match else tail
def derive_shape(
vuln: FileIR,
fixed: FileIR,
*,
function: str,
sink: str | None,
line: int | None,
mechanism: str,
facts: SemanticFacts,
vuln_text: str = "",
fixed_text: str = "",
cwe: str | None = None,
) -> Shape | None:
"""The shape of the labeled sink call in ``function`` on the vulnerable side, with the guard the fixed side added.
None when either side failed to parse, the function or the labeled call site is not found, or no argument of the
call carries a source (a constant-only call teaches nothing).
"""
if not vuln.parse_ok or not fixed.parse_ok:
return None
target = _function_named(vuln, function, vuln_text)
if target is None:
return None
scoped = facts.for_language(vuln.language)
call = _labeled_call(target, sink, line, scoped, cwe)
if call is None:
return None
kinds = [_source_kind_of_argument(target, call, index, scoped) for index in range(len(call.arg_texts))]
positions = tuple(index for index, kind in enumerate(kinds) if kind is not None)
if not positions:
return None
guard = classify_guard(vuln, fixed, function=function, vuln_text=vuln_text, fixed_text=fixed_text)
return Shape(
language=vuln.language,
sink_name=trailing_name(call.name),
arity=len(call.arg_texts),
source_positions=positions,
source_kinds=tuple(kind for kind in kinds if kind is not None),
guard=guard,
mechanism=mechanism,
)
def classify_guard(vuln: FileIR, fixed: FileIR, *, function: str, vuln_text: str, fixed_text: str) -> str:
"""Closed guard kind from the statements inside ``function`` that exist only on the fixed side (``none`` when unknown)."""
before = _function_lines(vuln, function, vuln_text)
after = _function_lines(fixed, function, fixed_text)
added = [line for line in after if line not in set(before)]
for kind, pattern in _GUARD_PATTERNS:
if any(pattern.search(line) for line in added):
return kind
return "none"
def source_kind_of_name(function: FunctionIR, name: str, facts: SemanticFacts, *, before_line: int, depth: int = 0) -> str | None:
"""How ``name`` came to carry attacker data inside ``function``: a fact source, a parameter, or a container read."""
if depth > _MAX_BIND_DEPTH:
return None
bind = _last_bind(function, name, before_line)
if bind is not None:
if _source_in(bind.value_text, facts):
return "fact_source"
if _CONTAINER_READ.search(bind.value_text):
return "container_read"
for inner in bind.names:
if inner == name:
continue
kind = source_kind_of_name(function, inner, facts, before_line=bind.line, depth=depth + 1)
if kind is not None:
return kind
if name in function.params:
return "parameter"
return None
def _source_kind_of_argument(function: FunctionIR, call: CallSite, index: int, facts: SemanticFacts) -> str | None:
text = call.arg_texts[index]
if _source_in(text, facts):
return "fact_source"
if call.arg_is_constant[index] and not call.arg_names[index]:
return None
if _CONTAINER_READ.search(text) and any(name in function.params for name in call.arg_names[index]):
return "container_read"
found: list[str] = []
for name in call.arg_names[index]:
kind = source_kind_of_name(function, name, facts, before_line=call.line + 1)
if kind is not None:
found.append(kind)
for preferred in SOURCE_KINDS:
if preferred in found:
return preferred
return None
def _source_in(text: str, facts: SemanticFacts) -> bool:
return any(pattern and pattern in text for source in facts.sources for pattern in source.patterns)
def _last_bind(function: FunctionIR, name: str, before_line: int) -> Bind | None:
candidates = [bind for bind in function.binds if bind.name == name and bind.line < before_line]
return max(candidates, key=lambda bind: bind.line) if candidates else None
def _labeled_call(function: FunctionIR, sink: str | None, line: int | None, facts: SemanticFacts, cwe: str | None) -> CallSite | None:
"""The call the label names: line and sink intersected when both exist, else either; else the fact sink in the function.
Labels that carry only a function and a mechanism (Real-Vuln rows) fall back to the fact-sink call inside the labeled
function, preferring the sink whose CWE matches the row and a call with a non-constant argument.
"""
wanted = trailing_name(sink) if sink else None
on_line = [call for call in function.calls if line is not None and call.line == line]
if on_line and wanted:
exact = [call for call in on_line if trailing_name(call.name) == wanted]
if exact:
return exact[0]
if on_line:
return on_line[0]
if wanted:
for call in function.calls:
if trailing_name(call.name) == wanted:
return call
return None
ranked: list[tuple[int, int, CallSite]] = []
for order, call in enumerate(function.calls):
fact = _sink_fact_for(call.name, facts)
if fact is None:
continue
cwe_match = 0 if (cwe and fact.cwe == cwe) else 1
non_constant = 0 if any(not const or names for const, names in zip(call.arg_is_constant, call.arg_names, strict=True)) else 1
ranked.append((cwe_match + non_constant, order, call))
ranked.sort(key=lambda item: (item[0], item[1]))
return ranked[0][2] if ranked else None
def _sink_fact_for(name: str, facts: SemanticFacts) -> SinkFact | None:
for fact in facts.sinks:
if any(_call_matches(name, call) for call in fact.calls):
return fact
return None
def _call_matches(name: str, fact_call: str) -> bool:
"""Same rule as the taint walker: exact, dotted suffix, or a bare name equal to the fact's last segment."""
if name == fact_call or name.endswith("." + fact_call):
return True
return "." not in name and name == fact_call.split(".")[-1]
def _function_named(ir: FileIR, function: str, text: str) -> FunctionIR | None:
for item in ir.functions:
if item.name == function:
return item
if text: # labels use declarator names; the parser may name the function differently (or <anon>)
for name, start, _end in named_ranges_from_ir(ir, text):
if name == function:
for item in ir.functions:
if item.start_line == start:
return item
return None
def _function_lines(ir: FileIR, function: str, text: str) -> list[str]:
target = _function_named(ir, function, text)
if target is None or not text:
return []
lines = text.splitlines()
body = lines[target.start_line - 1 : target.end_line]
return [line.strip() for line in body if line.strip()]
def shapes_by_sink(shapes: Sequence[Shape]) -> dict[tuple[str, str], list[Shape]]:
"""Index for matching: (language, sink_name) -> shapes."""
index: dict[tuple[str, str], list[Shape]] = {}
for shape in shapes:
index.setdefault((shape.language, shape.sink_name), []).append(shape)
return index
@dataclass(frozen=True)
class VariantHit:
path: str
line: int
mechanism_id: str
sink_name: str
source_kind: str
def match_shapes(
ir: FileIR,
shapes: Sequence[Shape],
facts: SemanticFacts,
*,
mechanism_ids: Mapping[str, str],
) -> list[VariantHit]:
"""Call sites of ``ir`` whose trailing callee name, arity and source-carrying positions match a shape (Req 3.1).
Any source kind at the shape's positions matches: a variant learned from ``request.args`` still fires when the same
sink takes a parameter. Constant-only arguments never match. ``mechanism_ids`` maps ``Shape.key()`` to the store id.
"""
if not ir.parse_ok:
return []
index = shapes_by_sink([shape for shape in shapes if shape.language == ir.language])
if not index:
return []
scoped = facts.for_language(ir.language) if hasattr(facts, "for_language") else facts
hits: list[VariantHit] = []
seen: set[tuple[int, str]] = set()
for function in ir.functions:
for call in function.calls:
candidates = index.get((ir.language, trailing_name(call.name)))
if not candidates:
continue
for shape in candidates:
if shape.arity != len(call.arg_texts) or (call.line, shape.key()) in seen:
continue
kinds = [
_source_kind_of_argument(function, call, position, scoped)
for position in shape.source_positions
if position < len(call.arg_texts)
]
if len(kinds) != len(shape.source_positions) or any(kind is None for kind in kinds):
continue
seen.add((call.line, shape.key()))
hits.append(
VariantHit(
path=ir.path,
line=call.line,
mechanism_id=mechanism_ids.get(shape.key(), shape.key()),
sink_name=shape.sink_name,
source_kind=str(kinds[0]),
)
)
return hits