Skip to content

fix(advisories): unversioned matches are never grounded; uncovered ecosystems say so (audit A2/E4) - #974

Merged
runyourempire merged 5 commits into
mainfrom
fix/e4-unversioned-not-covered
Oct 18, 2026
Merged

runyourempire merged 5 commits into
mainfrom
fix/e4-unversioned-not-covered

Conversation

@runyourempire

@runyourempire runyourempire commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

Adversarial audit 2026-10-12, findings E4 and A2 (app side). A fresh profile with one Maven pom.xml showed 15 GHSA advisories under Signal's "Affects You: Grounded in your dependencies", marked CORE and red. 14 of the 15 did not apply. Maven, Gradle and .NET projects also read as clean, because nothing reported that their versions were never checked.

1. Unversioned matches are never grounded (scoring v42, scoped epoch)

A registry advisory (cve / osv row) that names a dependency with no known installed version (manifest-only, no lockfile, or an ecosystem 4DA does not resolve) now gets the same treatment as a confirmed not-affected row (version_unknown in pipeline_v2::score_item):

  • no critical fast-path floor, so no CORE;
  • the registry-ungrounded score ceiling, and the item is never feed-relevant;
  • never strongly_grounded and never a dependency_event;
  • applicability = "version_unknown";
  • the necessity reason and the explanation say "Advisory names X in P — unconfirmed: no installed version known" instead of "affects" or "Names your dependency".

On the frontend, evidence-pool::isGrounded refuses version_unknown even if a stale breakdown still claims grounding. The expanded row labels it "Unconfirmed: no installed version known".

Display choice: drop from the promoted surfaces, label it everywhere else. I did not show these as a low-confidence "unconfirmed" card.

  • Doctrine principle 5 (accurate first): without an installed version, 4DA cannot stand behind "affects you" or even "probably affects you". The live case was wrong 14 times out of 15.
  • This is the existing v30/v31 rule for registry rows. Their only relevance evidence is a version check, so an unchecked one leaves Signal.
  • The user is not left with silence. Preemption now names the file 4DA could not check (section 3), which is the honest and actionable statement.

PIPELINE_VERSION 41 → 42, registered in epochs::SCOPED_EPOCHS as source_type IN ('osv','cve') (AD-034). version_unknown requires registry_advisory, which is exactly that predicate.

2. Two bypasses closed

  • Preemption Tier 2 (llm_judged_to_alerts) matched cve/osv items to direct dependencies by name only. Two checks now apply:
    • The dependency needs a resolved installed version. It comes from the same resolver every dependency reader uses, temporal::included_dependencies_resolved, extracted from get_inventory_dependencies: the same project and ecosystem family, then the same active root. Dormant projects are still admitted. project_dependencies.version alone is NULL on all 593 live rows, and a name-only join could give a Cargo crate an npm row's version.
    • The scorer's stored version verdict (scoring_explanations) must not be not-affected or version-unknown. An unscored row fails closed; Tier 1 still lists every version-confirmed match.
  • The background CVE scan (monitoring_jobs) no longer passes unversioned dependencies to cross_reference_advisories. That function "conservatively assumes affected" without a version, which would have sent Critical/High security-alert notifications for a pom.xml. version_is_affected itself is unchanged, because dependency_health legitimately relies on it.

3. "Not covered" is reported, never treated as clean

This reuses the MCP server's not_covered concept (project-tree.ts, {path, ecosystem}) rather than adding a new type.

  • AdvisorySync.not_covered?: NotCovered is built from the lockfile walk's saved report (ace.lockfile_report.v1) and has two parts:
    • unsupported files as {path, ecosystem};
    • unreadable lockfiles as {path, reason}. An all-clear must not rest on rows a failed read left stale (recovery-lead's parity finding).
  • .NET: a project with only *.csproj / *.fsproj / *.vbproj (no packages.lock.json) is now reported as unsupported. Before this it was not reported at all.
  • Preemption UI: PreemptionNotCoveredNote names the files (5 listed, the rest counted). While any file is not covered, the empty state reads "No known vulnerabilities in the dependencies 4DA could check" instead of "Your stack is clean". The copy uses the coverage qualifier from CLAUDE.md verbatim.
  • Conformance:
    • new synthetic case synth-dotnet-csproj;
    • new test every_declared_unsupported_file_is_reported_not_covered, which requires every declared-unsupported corpus file (Maven pom.xml, bun.lockb, .csproj) to come back from the reader as an explicit Unsupported;
    • is_lockfile_like is updated in both the Rust harness and scripts/conformance-corpus.mjs.

4. Review fixes (second commit)

  • The scorer looks for a lockfile pin before declaring "version unknown", through the bounded lookup temporal::installed_versions_nearby (temporal_versions.rs, round 3). The scope is, in order:

    1. the project's own lockfile;
    2. else the nearest ancestor inside the project's active repo root, never above it, so a stray lockfile in a home directory or monorepo parent can never answer;
    3. else one nested project's pin, only when the project has no lockfile of its own and exactly one nested project pins the package.

    Every distinct version in scope is returned, and the advisory is affected if ANY copy is; the affected copy's version is the one shown. The name lookup uses idx_user_deps_package (exact and lowercase forms). Tests:

    • a_stray_parent_lockfile_never_overrides_the_projects_own_pin;
    • a_nested_sub_project_pin_never_replaces_the_own_pin;
    • any_affected_copy_makes_the_advisory_affected;
    • a_lockfile_pin_rescues_a_versionless_grounding_row;
    • 5 unit tests of the scope rules.
  • Preemption Tier 2 fills a missing installed version from the same lookup, so Tier 2 and the scorer cannot disagree. tier2_and_the_scorer_agree_on_the_installed_version asserts both see 4.17.1 on one fixture.

  • The 9 new UI keys are translated into all 13 locales.

  • When the only files are ones 4DA cannot read (for example an .fsproj-only repo with total_tracked == 0), Preemption shows the not-covered note instead of "No dependencies are known yet, scan your projects".

  • The CVE scan logs how many unversioned dependencies it skipped.

  • The NotCovered doc now says it is the MCP shape plus unreadable. The MCP side has not adopted unreadable yet.

Not done / follow-ups

  • ace/scanner.rs (t2's file) still stores Maven coordinates with no version and offers them to the OSV sync. That is harmless now that unversioned matches are unconfirmed everywhere, but readiness can still say "ready" for the Maven ecosystem. Suggested change for t2: skip versionless manifest rows in osv/sync::auditable_packages_by_ecosystem, or have the scanner not register Maven deps at all.
  • The Brief's "Nothing new touches your code today" line (brief_facts_copies.rs, t2's file) does not yet mention not-covered files. AdvisorySync.not_covered is available for it.

Evidence

  • Rust, round-3 run (preemption, temporal, pipeline_v2, readiness): 326 passed, 0 failed.
  • Rust, review-fix run (preemption, temporal, readiness, pipeline_v2, monitoring_jobs, conformance): 328 passed, 0 failed. New tests: tier2_version_never_crosses_ecosystems, tier2_respects_the_stored_version_verdict (an installed version past the fix is rejected; an unscored row fails closed) and a_lockfile_pin_rescues_a_versionless_grounding_row. Frontend: PreemptionWorklist.emptyState gains the partial all-clear and the not-covered-only project; 95 preemption tests pass.
  • Rust, first run: 366 passed, 0 failed. Covers preemption, readiness, conformance, lockfile, pipeline_v2, explanation_chain and epochs, built in an isolated target dir. New tests:
    • advisory_against_an_unknown_installed_version_is_unconfirmed, end to end through score_item, with a pinned control that still confirms;
    • an_advisory_against_an_unknown_version_is_unconfirmed_not_affecting;
    • tier2_never_matches_a_registry_advisory_to_an_unversioned_dep;
    • readiness_names_the_files_4da_does_not_read;
    • readiness_has_no_not_covered_when_every_file_was_read, which includes back-compat with an older snapshot;
    • read_dir_reports_a_csproj_only_project_as_unsupported;
    • every_declared_unsupported_file_is_reported_not_covered.
  • Frontend: evidence-pool (version_unknown is never grounded) and PreemptionNotCoveredNote (2 tests).
  • Live corpus check (read-only):
    • Of 2,019 scored cve/osv rows on the founder DB, 0 have matched deps with an unknown installed version. Every grounded advisory there carries a version, so this change leaves the founder's Signal feed exactly as it is.
    • The founder's lockfile report has 0 unsupported and 0 failed files, so no not-covered note appears.
    • The behaviour change is confined to manifest-only and unsupported projects such as the audit's fresh profile, whose scratch data no longer exists. The end-to-end unit test reproduces that shape.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7

@runyourempire
runyourempire force-pushed the fix/e4-unversioned-not-covered branch 3 times, most recently from 76c0b20 to f77a2f9 Compare October 17, 2026 16:50
@runyourempire
runyourempire added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 17, 2026
@runyourempire
runyourempire force-pushed the fix/e4-unversioned-not-covered branch from f77a2f9 to c5df05c Compare October 17, 2026 18:15
@runyourempire
runyourempire added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 17, 2026
runyourempire and others added 5 commits October 18, 2026 04:56
…osystems say so (audit A2/E4)

A fresh profile with one Maven pom.xml showed 15 GHSA advisories as grounded
"Affects You" items (14 did not apply), and Maven/Gradle/.NET projects read
as clean because nothing reported them as unchecked.

Signal (scoring v42, scoped epoch on source_type cve/osv):
- A registry advisory naming a dependency with no known installed version
  (manifest-only, no lockfile, or an unresolved ecosystem) takes the
  confirmed-not-affected tier: no critical fast path, the registry-ungrounded
  ceiling, never feed-relevant, never strongly grounded or a dependency
  event. Applicability is `version_unknown`; the reason and explanation say
  "unconfirmed: no installed version known" instead of "affects".
- The evidence pool never puts `version_unknown` in Affects You.

Bypasses closed:
- Preemption Tier 2 (LLM-judged) no longer matches a cve/osv item to a
  direct dependency without a resolved version.
- The background CVE scan no longer notifies on unversioned dependencies
  (the matcher "conservatively assumes affected" without a version).

Not covered (reuses the MCP server's `not_covered` concept):
- AdvisorySync carries `not_covered`: the lockfile walk's unsupported files
  ({path, ecosystem}) and its unreadable lockfiles ({path, reason}).
- A .NET project with only a .csproj/.fsproj/.vbproj is now reported as
  unsupported (it was not reported at all).
- Preemption names those files, and its empty state no longer says "Your
  stack is clean" while they exist.
- Conformance: new synthetic case synth-dotnet-csproj, and a test that every
  declared-unsupported corpus file comes back from the reader as Unsupported.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
… stored verdict, lockfile pin before "unknown"

- SF1: Tier 2's grounding set reads installed versions from the shared
  resolver (temporal::included_dependencies_resolved, extracted from
  get_inventory_dependencies): same project + ecosystem family, then the
  same active root. The name-only join could give a Cargo crate an npm
  row's version. Test: tier2_version_never_crosses_ecosystems.
- SF2: a cve/osv row becomes a Tier 2 alert only when the scorer's stored
  verdict saw an installed version and did not find it outside the range
  (not_affected and version_unknown are rejected; unscored rows fail closed).
  Test: tier2_respects_the_stored_version_verdict.
- SF3: before an advisory is "version unknown", the scorer looks for a
  lockfile pin of the package (same family, same or enclosing project) via
  temporal::installed_version_nearby. Test:
  a_lockfile_pin_rescues_a_versionless_grounding_row.
- Nits: empty-state tests for the partial all-clear; a not-covered-only
  project shows the not-covered note instead of "scan your projects"; the
  CVE scan logs how many unversioned deps it skipped; NotCovered's doc says
  it extends the MCP shape with `unreadable`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…ier 2; locale keys (round-3 review)

- SF1: temporal::installed_versions_nearby (temporal_versions.rs) replaces
  the first-row lookup that could take a stray parent lockfile or a nested
  sub-project's pin and suppress a real advisory. Scope: the project's own
  lockfile, else the NEAREST ancestor inside its active repo root (never
  above it), else a unique nested pin only when the project has no lockfile
  of its own. Every distinct version in scope is returned and the scorer
  treats the advisory as affected if ANY copy is (the affected copy is
  shown). Name lookup uses the package index (exact + lowercase forms).
- SF2: Preemption Tier 2 fills a missing version from the same lookup, so
  Tier 2 and the scorer agree; a test proves it on one fixture.
- SF3: the 9 new UI keys are translated into all 13 locales.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…merge-queue ejection)

Both fixtures modelled lockfile dependencies with `version: None` and no
`user_dependencies` rows, i.e. genuinely unversioned, which v42 (audit E4)
correctly treats as "version unknown". Neither test is about that case:

- direct_dep_cve_clears_the_direct_dep_floor: the direct `hyper` dep now
  carries a pin (1.4.0); the test still proves the direct-dep floor.
- serde_derive_advisory_engages_fast_path_for_serde_user: the bench
  profile's lockfile-only family children carry their Cargo.lock pins
  (serde_derive 1.0.200, below the 1.0.205 fix; tokio-util 0.7.11); the
  test still proves the family-child fast path.

The unversioned case stays covered by
advisory_against_an_unknown_installed_version_is_unconfirmed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…ts (v42, real-embedding ratchet)

The real-embedding calibration ratchet (queue run 39057765993) failed at
93.0%: four security scenarios (sec_hyper_vuln, sec_react_xss,
sec_pytorch_rce, reg_cve_reqwest) scored exactly 0.370, the v42
version-unknown ceiling. The benchmark profiles install every dependency
with `version: None` and the bench DB has no lockfile rows, so these were
genuinely unversioned fixtures, not a resolver regression.

The profiles now carry lockfile pins inside each advisory's affected range,
so the scenarios still test the confirmed-exposure security path:
hyper 1.4.0 (< 1.4.2), reqwest 0.12.10 (< 0.12.15), react 19.1.0
(< 19.1.2), pytorch/torch 2.4.0 (< 2.4.1).

Local run, `FOURDA_REQUIRE_REAL_EMBEDDINGS=1 cargo test --lib --features
calibrated-sim scoring::` (committed real-model embedding fixtures):
score-range 97.0% (97/100), security 12/12, TP 17/20, TN 20/20, quality gate
PASSED. The three remaining failures (tp_systems_programming,
reg_tauri_title_boost, reg_desktop_packaging) are the documented residuals in
quality_gate.rs and fail identically on main. No floor was changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire force-pushed the fix/e4-unversioned-not-covered branch from c5df05c to afb5767 Compare October 18, 2026 04:52
@runyourempire
runyourempire added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 4d257b9 Oct 18, 2026
25 checks passed
@runyourempire
runyourempire deleted the fix/e4-unversioned-not-covered branch October 18, 2026 09:10
runyourempire added a commit that referenced this pull request Oct 18, 2026
… what it did not check (W5a, follow-ups to #974)

- Readiness counts an ecosystem only when an auditable row carries an
  installed version. The manifest scanner registers pom.xml / build.gradle
  dependencies with no version, a versionless dependency is never matched
  (#974), and those rows made "Maven" a required ecosystem: its sync
  succeeded and readiness said Ready for a project nothing was checked in.
  Now a Maven-only project reads NoInventory with its pom.xml named, and an
  npm + Maven machine reads Ready for npm with the Maven files named. The
  scanner keeps those rows for ACE context; the sync is unchanged. (Live
  DB: every versioned row is lockfile-backed npm/Rust, so the operator's
  readiness does not change.)
- BriefFacts carries #974's not-covered report. The deterministic brief's
  all-clear reads "Nothing new touches the code 4DA checked today." and a
  "Not checked: ..." line names the files 4DA does not read and the
  lockfiles it could not read (three named, then "and N more"). A narrated
  brief's all-clear is rewritten the same way (idempotent), and the prompt
  tells the model the facts are complete only for the files read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant