You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the Stage-1 consistency pass corrects a unit's verdict it writes result["verdict"] and does not
write result["finding"]. Ingestion has already set a lowercase finding, and the canonical
downstream read is finding-first — str(r.get("finding") or r.get("verdict", "")).lower() — so the
stale finding short-circuits the or and the correction is invisible.
A safe → VULNERABLE correction leaves {finding: "safe", verdict: "VULNERABLE"}. The unit is counted
safe, filtered out of Stage 2 entirely, and absent from the disclosure document.
There is a second edge, and it points the other way. Because the stale finding also survives a downgrade to a verdict the block-list does not cover, it is currently acting as an accidental safety
net. The obvious one-line fix removes that net and introduces a different false negative. The fix
below is gated accordingly.
/usr/bin/grep -nE '\["(verdict|finding)"\] *=' libs/openant-core/utilities/stage1_consistency.py
returns one line — :298. The file writes verdict once and finding never.
Ingestion guarantees the shadowing value. libs/openant-core/core/analyzer.py:149:
Upstream of that, libs/openant-core/core/analysis_core.py:33-56 (_normalize_result) synthesises verdict from finding, so both keys are present and agreeing before the consistency pass runs — and
that pass runs later, at libs/openant-core/core/analyzer.py:686.
_count_verdicts is worth separating: its expression is r.get("finding", r.get("verdict", "error").lower()) — a default-on-absent, not an or-on-falsy.
It reaches the same wrong answer here by a different route, so a fix targeting only the or idiom
would miss it.
Executed at b501962
Harness attached in the comment below; it reads only and makes no API calls.
A. _count_verdicts (core/analyzer.py:320)
corrected-only -> vulnerable=0 safe=1
CONTROL vuln -> vulnerable=1 safe=0
CONTROL safe -> vulnerable=0 safe=1
mixed -> vulnerable=1 safe=2
B. canonical finding-first read (core/reporter.py:315)
corrected (finding=safe, verdict=VULNERABLE) -> 'safe' in confirmed? False
CONTROL (finding=vulnerable) -> 'vulnerable' in confirmed? True
C. writes in utilities/stage1_consistency.py
result["verdict"] : 1
result["finding"] : 0
CONTROL vuln is the discriminating control: the same function, on a row whose finding agrees with
its verdict, counts correctly. The zero in A is an effect of the disagreement, not a broken harness.
An independent reviewer drove the realrun_stage1_consistency_check with a stubbed model call
and confirmed the emitted shape is {'finding': 'safe', 'verdict': 'VULNERABLE'} — i.e. the rows
above are what the pass actually produces, not only what it is read to produce.
The negation, executed
The claim fails if anything re-derives finding after the pass. Enumerated (unedited output):
Two are comments (:169, :434). The analyzer.py pair runs at ingestion, before the pass. The finding_verifier.py writes are Stage 2 — which the corrected unit does not reach, per libs/openant-core/core/verifier.py:107. experiment.py is not the production pipeline. No production
write re-derives finding between the pass and the consumers.
The naive fix is unsafe — measured
The obvious fix is to write both keys at :298. Do not ship that. The downgrade guard at libs/openant-core/utilities/stage1_consistency.py:287 blocks only old ∈ {VULNERABLE, BYPASSABLE} AND new ∈ DISCLOSURE_DROPPED, and DISCLOSURE_DROPPED is {inconclusive, protected, rejected, safe} (libs/openant-core/core/verdict_taxonomy.py:86-91). new_verdict is unvalidated model output — the code's own comment at :281 says so, and _resolve_stage1_inconsistency (:334-342) json.loads it with no enum check.
Executed:
DISCLOSURE_DROPPED = ['inconclusive', 'protected', 'rejected', 'safe']
VULNERABLE -> INSUFFICIENT_CONTEXT blocked_at_287=False
today canonical='vulnerable' in confirmed? True
UNDER FIX canonical='insufficient_context' in confirmed? False <-- REMOVED
VULNERABLE -> NOT_VULNERABLE blocked_at_287=False
today canonical='vulnerable' in confirmed? True
UNDER FIX canonical='not_vulnerable' in confirmed? False <-- REMOVED
VULNERABLE -> SAFE blocked_at_287=True
INSUFFICIENT_CONTEXT is not hypothetical — it is a first-class verdict in _normalize_result's map
at libs/openant-core/core/analysis_core.py:55, and it is not in DISCLOSURE_DROPPED. So the stale finding is presently masking unrecognised-verdict downgrades, and an ungated write of finding
would drop a disclosed vulnerability in the two cases measured above.
Suggested fix
Gate the write on the new verdict being disclosure-eligible, so the correction lands for upgrades
without removing the accidental protection for unrecognised downgrades:
ifold_verdict_norm!=new_verdict:
result["verdict"] =new_verdict# Canonical downstream reads are finding-first, and ingestion# always sets `finding` (core/analyzer.py:149-152), so writing# only `verdict` leaves the correction shadowed. Gate on# disclosure-eligibility: `new_verdict` is unvalidated model# output (see the comment at :281), and writing an unrecognised# value here would drop a disclosed finding.ifstr(new_verdict).lower() inDISCLOSURE_ELIGIBLE:
result["finding"] =str(new_verdict).lower()
The alternative — widen :287's block-set from DISCLOSURE_DROPPED to "anything not
disclosure-eligible", then write both keys without the gate — is the more complete fix, and it is the
open question PR #245 deferred. Either way _count_verdicts at core/analyzer.py:331 should be
brought onto the same canonical read.
Regression tests should assert (1) after a safe → VULNERABLE correction the unit is in _count_verdicts' vulnerable bucket and in confirmed_findings, and (2) after a VULNERABLE → INSUFFICIENT_CONTEXT correction it is still disclosed. I found no existing test
covering a correction whose finding and verdict disagree.
Prior art, checked
libs/openant-core/tests/test_verifier_verdictonly_confirmed_drop.py exists and passes at b501962
(3 passed). Its subject is a result with no finding key, fixed by adding or r.get("verdict")
fallbacks. Those are not reached here: the or short-circuits on a present-but-stale value, as the executed
runs above show. _resolve_stage1_finding's docstring states the absent-key intent explicitly at libs/openant-core/utilities/finding_verifier.py:209-211, and the same assumption appears at libs/openant-core/experiment.py:557 (if finding is None).
PRs #193, #195, #243 and #245 all touch this function and all left :298 writing one key.
Not that a specific vulnerability was missed in a real scan. The evidence is the production sinks
and the canonical reads executed at b501962, plus an independent drive of the real consistency
pass with a stubbed model call. No end-to-end CLI run against a real repository was performed.
Not that every unit is affected — only units the consistency pass actually corrects. I did not
measure how often it fires on a real corpus.
Summary
When the Stage-1 consistency pass corrects a unit's verdict it writes
result["verdict"]and does notwrite
result["finding"]. Ingestion has already set a lowercasefinding, and the canonicaldownstream read is finding-first —
str(r.get("finding") or r.get("verdict", "")).lower()— so thestale
findingshort-circuits theorand the correction is invisible.A
safe → VULNERABLEcorrection leaves{finding: "safe", verdict: "VULNERABLE"}. The unit is countedsafe, filtered out of Stage 2 entirely, and absent from the disclosure document.
There is a second edge, and it points the other way. Because the stale
findingalso survives adowngrade to a verdict the block-list does not cover, it is currently acting as an accidental safety
net. The obvious one-line fix removes that net and introduces a different false negative. The fix
below is gated accordingly.
The lost write
libs/openant-core/utilities/stage1_consistency.py:297:/usr/bin/grep -nE '\["(verdict|finding)"\] *=' libs/openant-core/utilities/stage1_consistency.pyreturns one line —
:298. The file writesverdictonce andfindingnever.Ingestion guarantees the shadowing value.
libs/openant-core/core/analyzer.py:149:Upstream of that,
libs/openant-core/core/analysis_core.py:33-56(_normalize_result) synthesisesverdictfromfinding, so both keys are present and agreeing before the consistency pass runs — andthat pass runs later, at
libs/openant-core/core/analyzer.py:686.Where the stale value is read
libs/openant-core/core/verifier.py:107— Stage-2 input gatevulnerable_results, so Stage 2 does not verify it at alllibs/openant-core/core/verifier.py:351-356—confirmed_findingslibs/openant-core/core/reporter.py:313-316confirmed_findingsis absent (:307-308) — same outcome when Stage 2 did not runlibs/openant-core/core/analyzer.py:331—_count_verdictssafe_count_verdictsis worth separating: its expression isr.get("finding", r.get("verdict", "error").lower())— a default-on-absent, not anor-on-falsy.It reaches the same wrong answer here by a different route, so a fix targeting only the
oridiomwould miss it.
Executed at
b501962Harness attached in the comment below; it reads only and makes no API calls.
CONTROL vulnis the discriminating control: the same function, on a row whosefindingagrees withits verdict, counts correctly. The zero in A is an effect of the disagreement, not a broken harness.
An independent reviewer drove the real
run_stage1_consistency_checkwith a stubbed model calland confirmed the emitted shape is
{'finding': 'safe', 'verdict': 'VULNERABLE'}— i.e. the rowsabove are what the pass actually produces, not only what it is read to produce.
The negation, executed
The claim fails if anything re-derives
findingafter the pass. Enumerated (unedited output):Two are comments (
:169,:434). Theanalyzer.pypair runs at ingestion, before the pass. Thefinding_verifier.pywrites are Stage 2 — which the corrected unit does not reach, perlibs/openant-core/core/verifier.py:107.experiment.pyis not the production pipeline. No productionwrite re-derives
findingbetween the pass and the consumers.The naive fix is unsafe — measured
The obvious fix is to write both keys at
:298. Do not ship that. The downgrade guard atlibs/openant-core/utilities/stage1_consistency.py:287blocks onlyold ∈ {VULNERABLE, BYPASSABLE} AND new ∈ DISCLOSURE_DROPPED, andDISCLOSURE_DROPPEDis{inconclusive, protected, rejected, safe}(libs/openant-core/core/verdict_taxonomy.py:86-91).new_verdictis unvalidated model output — the code's own comment at:281says so, and_resolve_stage1_inconsistency(:334-342)json.loadsit with no enum check.Executed:
INSUFFICIENT_CONTEXTis not hypothetical — it is a first-class verdict in_normalize_result's mapat
libs/openant-core/core/analysis_core.py:55, and it is not inDISCLOSURE_DROPPED. So the stalefindingis presently masking unrecognised-verdict downgrades, and an ungated write offindingwould drop a disclosed vulnerability in the two cases measured above.
Suggested fix
Gate the write on the new verdict being disclosure-eligible, so the correction lands for upgrades
without removing the accidental protection for unrecognised downgrades:
The alternative — widen
:287's block-set fromDISCLOSURE_DROPPEDto "anything notdisclosure-eligible", then write both keys without the gate — is the more complete fix, and it is the
open question PR #245 deferred. Either way
_count_verdictsatcore/analyzer.py:331should bebrought onto the same canonical read.
Regression tests should assert (1) after a
safe → VULNERABLEcorrection the unit is in_count_verdicts'vulnerablebucket and inconfirmed_findings, and (2) after aVULNERABLE → INSUFFICIENT_CONTEXTcorrection it is still disclosed. I found no existing testcovering a correction whose
findingandverdictdisagree.Prior art, checked
libs/openant-core/tests/test_verifier_verdictonly_confirmed_drop.pyexists and passes atb501962(3 passed). Its subject is a result with no
findingkey, fixed by addingor r.get("verdict")fallbacks. Those are not reached here: the
orshort-circuits on a present-but-stale value, as the executedruns above show.
_resolve_stage1_finding's docstring states the absent-key intent explicitly atlibs/openant-core/utilities/finding_verifier.py:209-211, and the same assumption appears atlibs/openant-core/experiment.py:557(if finding is None).PRs #193, #195, #243 and #245 all touch this function and all left
:298writing one key.Relation to existing issues
verdictnorfindingis counted in no bucket and adopted as complete on resume, sounits_analyzedoverstates and the unit is never retried #324 — a Stage-1 result with neither key; this is one with both, disagreeing.matters here: JSON corrector synthesises a verdict from free text and reports it as a successful correction, producing rows no bucket counts #316's mechanism is what makes the naive fix above dangerous.
What I am not claiming
and the canonical reads executed at
b501962, plus an independent drive of the real consistencypass with a stubbed model call. No end-to-end CLI run against a real repository was performed.
measure how often it fires on a real corpus.
findingis present only for units arriving with either key;core/analyzer.py:149-152sets nothing when both are absent, which is A Stage-1 result with neither
verdictnorfindingis counted in no bucket and adopted as complete on resume, sounits_analyzedoverstates and the unit is never retried #324's case and where this mechanism does not fire.variant against the full suite.