mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
fix(scan): scope a partition census gap to its own partition, and write the rule down
A missing JavaScript file cannot make a PHP answer wrong. A census gap now carries the partition it was raised for, and reaches only that partition questions. On the measured plugin four JavaScript files -- a webpack build output, a minified select2, a build config and one source file -- had taken the admissibility of 4,425 PHP regions with them. Two controls exercise the real boundaries rather than injecting a reason, and both were confirmed to FAIL with the old classification restored, so they guard what they claim to. And the rule is now in the design, because this was the fifth instance found and fixed one at a time. Two invariants, eleven instances between them: A degradation is owned by the narrowest scope that caused it. Vendor exclusion as a global veto, line ambiguity inherited by every question, an empty scope blocking a sibling comparison, a language boundary demoting every answer, a census gap demoting every answer. The fourth and fifth cost the declared v0.1 target its entire admissibility, and neither passing subject could reveal it because both hold one language. A token matches the thing it names, not text that contains it. gets inside fgets, resolve inside resolveUrl, user inside users, a sink word inside a comment, a dotted setting on its last segment, a flow query reading the receiver. Each carries a review test: a new degradation must state the scope it reaches and justify demoting that far, and a text match must be bounded AND confined to the part of the node that names the callee. 1,419 tests pass, eighteen skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5
parent
c93669c34f
commit
29cef32bac
@@ -121,11 +121,19 @@ class PartitionGraph:
|
||||
if not named or not expected <= observed:
|
||||
self.diagnostics.append("partition_file_census_incomplete" if named else "partition_file_census_unavailable")
|
||||
reason = "partition_file_census_incomplete" if named else "partition_file_census_unavailable"
|
||||
# The partition the gap came FROM travels with it. A census gap is evidence about one
|
||||
# graph: a JavaScript file missing from the JavaScript graph says nothing about whether
|
||||
# a PHP answer is right, and without this the scan cannot tell the difference.
|
||||
rows = [
|
||||
dict(r, census_failure=reason, missing_file_names=sorted(expected - observed) if named else [])
|
||||
dict(
|
||||
r,
|
||||
census_failure=reason,
|
||||
census_language=language,
|
||||
missing_file_names=sorted(expected - observed) if named else [],
|
||||
)
|
||||
for r in normalized
|
||||
if isinstance(r, Mapping)
|
||||
] or [{"census_failure": reason, "missing_file_names": []}]
|
||||
] or [{"census_failure": reason, "census_language": language, "missing_file_names": []}]
|
||||
# Preserve any individual empty or sharded graph's degradation.
|
||||
previous = answers.get(rid, [])
|
||||
if not previous or any(
|
||||
|
||||
@@ -426,7 +426,7 @@ def _scan_repository_impl(
|
||||
{
|
||||
"stage": "model",
|
||||
"reason": "cpg_empty" if "methods" in entry and methods == 0 else entry.get("census_failure", "cpg_empty"),
|
||||
**{k: entry[k] for k in ("methods", "files", "missing_file_names") if k in entry},
|
||||
**{k: entry[k] for k in ("methods", "files", "missing_file_names", "census_language") if k in entry},
|
||||
}
|
||||
)
|
||||
# More than one graph means some files could only be built apart from the rest, and a
|
||||
@@ -519,6 +519,22 @@ def _scan_repository_impl(
|
||||
degradations.append({"stage": "model", "reason": "budget_exhausted", "regions_unasked": len(unasked)})
|
||||
|
||||
graph_gaps = tuple(str(d["reason"]) for d in degradations if d.get("reason") in GRAPH_INTEGRITY_GAPS)
|
||||
# An integrity gap reaches the questions it can actually be wrong about, which for a census gap is the
|
||||
# ONE partition it came from. A partitioned scan raises a gap per graph, and a repository-wide demotion
|
||||
# let four JavaScript files -- a webpack build output, a minified select2, a build config and one source
|
||||
# file -- take the admissibility of 4,425 PHP regions with them. Third instance of this shape in one week,
|
||||
# after vendor exclusion acting as a global veto and line ambiguity inherited by every question, so the
|
||||
# rule is now written in the design: a gap is owned by the narrowest scope that caused it.
|
||||
scoped_graph_gaps: dict[str, list[str]] = {}
|
||||
unscoped_graph_gaps: list[str] = []
|
||||
for item in degradations:
|
||||
if item.get("reason") not in GRAPH_INTEGRITY_GAPS:
|
||||
continue
|
||||
partition = str(item.get("census_language") or "")
|
||||
if partition:
|
||||
scoped_graph_gaps.setdefault(partition, []).append(str(item["reason"]))
|
||||
else:
|
||||
unscoped_graph_gaps.append(str(item["reason"]))
|
||||
# A declared exclusion is a deliberate scope choice, not a failure to read the graph. It stays
|
||||
# in coverage, and whether a particular answer needed the excluded semantics is carried by that
|
||||
# question's own unresolved call destinations, not assumed for every answer in the repository.
|
||||
@@ -535,8 +551,13 @@ def _scan_repository_impl(
|
||||
)
|
||||
if question.identity in context_gaps and outcomes[rid].status == "completed":
|
||||
outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="change_context_incomplete")
|
||||
if graph_gaps and outcomes[rid].status == "completed":
|
||||
outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="graph_incomplete")
|
||||
if outcomes[rid].status == "completed":
|
||||
# A gap with no partition is about the whole read and reaches everything. A scoped one reaches its
|
||||
# own partition, and a question whose language the scan cannot place is treated as reached: an
|
||||
# unplaceable question is not evidence that the gap missed it.
|
||||
reached = bool(unscoped_graph_gaps) or bool(scoped_graph_gaps.get(question.identity.language))
|
||||
if reached or (scoped_graph_gaps and not question.identity.language):
|
||||
outcomes[rid] = replace(outcomes[rid], status="unresolved", reason="graph_incomplete")
|
||||
scope = replace(scope, unresolved_boundaries=tuple(dict.fromkeys((*scope.unresolved_boundaries, *graph_gaps, *declared_gaps))))
|
||||
ordered_outcomes = tuple(outcomes[q.identity.question_id] for q in scope.selected)
|
||||
scanned = len(judged)
|
||||
|
||||
Reference in New Issue
Block a user