Skip to content

fix(osv): one malware predicate (MAL- id/alias or CWE-506), with the CWE ids stored (parity decision 1) - #992

Merged
runyourempire merged 3 commits into
mainfrom
fix/malware-cwe
Oct 18, 2026
Merged

runyourempire merged 3 commits into
mainfrom
fix/malware-cwe

Conversation

@runyourempire

@runyourempire runyourempire commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

What

Parity decision 1. This adds ONE malware predicate for every reader in this app, and stores the data it needs. The MCP server (@4da/mcp-server) must implement the same three rules; the depengine tests are the contract it mirrors. This PR does not export the function to the MCP.

  • fourda_depengine::is_malware(id: &str, aliases: &[String], cwe_ids: &[String], summary: &str) -> bool. It is pure; the wasm build has zero imports and check-wasm.cjs passes. A record is malware when ANY of these holds:
    1. Its id or one of its aliases is an OSV MAL- id.
    2. It carries CWE-506 in database_specific.cwe_ids. Example: crossenv GHSA-c2m4-w5hm-vqjw ("crossenv is malware") has aliases ["CVE-2017-16074"], cwe_ids: ["CWE-506"] and no MAL- alias (live, 2026-10-18).
    3. Its summary starts with GitHub's malware title template, Malicious Package in or Malicious code in (case-insensitive, anchored at the start). Example: GHSA-28f8-hqmc-7ph8 ("Malicious Package in ember-power-timepicker") has aliases: [] and cwe_ids: []. GitHub generates this template; it is not prose. A summary that mentions "malicious" anywhere else does not match, and the tests cover that.
  • Phase 128 migration (additive): osv_advisories.cwe_ids TEXT (a JSON array), added with the add_column_if_missing helper. TARGET_VERSION is now 128 and contract/app-schema.sql is regenerated.
    • An old 127 binary opening a 128 database is refused by the existing too-new guard (SCHEMA_TOO_NEW_PHRASE, which state.rs::is_schema_newer_than_binary routes away from the corrupt-database fallback). Rebuild fourda and fourda-engine together after merge.
  • Backfill: the migration moves each ecosystem's osv_sync_status.last_synced_at back to exactly datetime('now', '-<osv_sync_max_age_hours> hours'), so needs_sync is true on the next cycle. It does not set 1970, so every surface that shows the last sync time still shows a plausible one. That sync re-upserts every row, and rows written before the migration get their CWE ids.
  • Sync: store_vulnerability stores database_specific.cwe_ids in its single upsert (Database::upsert_osv_advisory_record, which upsert_osv_advisory_with_meta now wraps). On conflict it uses COALESCE, the same as the aliases. StoredAdvisory and MatchedAdvisory carry cwe_ids, and their is_malware() calls the shared function.
  • Wired:
    • the parent-release Advisory::new (osv/parent_release.rs);
    • the release gate's AdvisoryWindow::is_malware (osv/release_advisories.rs). Its earlier unanchored summary check ("starts with malicious") is replaced by the anchored template;
    • Preemption's infer_urgency_from_summary (used only when an advisory has no severity): a record that matches the shared predicate is High. Before, only an id starting with mal- was.
    • Severity (osv/severity.rs contains_malware) belongs to fix(severity): one grade per advisory, inherited from GHSA aliases when unrated; one tier for unknown #986. Whichever PR lands second switches it to is_malware.
  • Privacy / NETWORK.md: no new requests and no new data sent, so no change.

Upgrade window (disclosed)

Until the first sync after the upgrade, a stored row has cwe_ids NULL. During that window, a malware record identified only by its CWE reads as non-malware. An example is GHSA-hxxf-q3w9-4xgw (eslint-scope), whose summary does not match the template. The migration makes that sync due at once, so the window lasts until the next refresh cycle, not 6 hours. Records with a MAL- id or alias, or with the title template, are recognised from the first launch, because those fields are already stored. A record matched only by the template (rule 3) is also read correctly from the first launch, since summaries are already stored.

Tests

  • is_malware:
    • crossenv (CWE only);
    • ember-power-timepicker (title only);
    • template case and leading whitespace;
    • a MAL- id, a MAL- alias, and a CWE written with whitespace or lowercase;
    • records that are not malware: CWE-1333, CWE-5060 and MALWARE-NOTE, plus six pieces of prose that look like the template (Malicious packages can..., foo: Malicious Package in bar, Malicious Packagein baz, ...allows malicious code in qux).
  • Release gate: the original "Malicious code in foo" test is restored. GHSA-28f8-hqmc-7ph8 is malware with no alias and no CWE, and crossenv is malware by its CWE.
  • Preemption: unrated GHSA-28f8 (title), crossenv (CWE) and a MAL- id are all High; unrelated prose is Watch.
  • Sync: a row stored before the migration gets cwe_ids on the next sync once the migration's step makes the sync due, and crossenv then reads as malware.
  • Parent release: crossenv as the installed parent gives "malicious" (remove or replace), never a named release.
  • cargo test --lib -- osv:: preemption db:: app_schema_contract evidence:: brief_facts scoring::security: 1011 passed, 0 failed. The pre-push gate passed after the rebase onto c88feb2ae.
  • depengine: the tests, clippy --lib -D warnings, and the wasm32 release build with 0 imports all pass. cargo clippy --lib -D warnings and rustfmt are clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7

runyourempire added a commit that referenced this pull request Oct 18, 2026
…depengine::is_malware); crossenv vector on

contains_malware now calls fourda_depengine::is_malware over each
match's (advisory_id, aliases, cwe_ids): a MAL- id or alias, or CWE-506 in
database_specific.cwe_ids. The local MAL-only predicate is deleted. Stacked
on fix/malware-cwe (#992), which stores cwe_ids; this lands after it.

sv-021: crossenv GHSA-c2m4-w5hm-vqjw (live OSV 2026-10-18: aliases
[CVE-2017-16074], cwe_ids [CWE-506], own label HIGH) on the installed
1.0.1 is Critical, its severity its own high. The vector checker reads
database_specific.cwe_ids as osv::sync stores it. Knowledge Gaps reads the
cwe_ids column with the rows it grades.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
runyourempire added a commit that referenced this pull request Oct 18, 2026
…t stores the CWEs (review of #992)

B1: is_malware(id, aliases, cwe_ids, summary) gains a third rule: the
summary starts with GitHub's malware advisory title template, "Malicious
Package in " or "Malicious code in " (case-insensitive, anchored). Some
GitHub malware records carry neither a MAL- alias nor a CWE:
GHSA-28f8-hqmc-7ph8 ("Malicious Package in ember-power-timepicker",
aliases [], cwe_ids []). Prose that merely mentions malice elsewhere is
not read. The release gate's summary test is restored and the record is
pinned; every caller (parent releases, the release gate, StoredAdvisory,
MatchedAdvisory, the rule vectors) passes the summary.

Preemption: an unrated advisory's urgency reads the shared predicate
(High) instead of a MAL- id prefix only.

Migration: the column is added with add_column_if_missing, and the sync is
made due by setting last_synced_at back exactly the max age
(osv_sync_max_age_hours), not to 1970. The sync stores cwe_ids in its one
upsert (upsert_osv_advisory_record; COALESCE like the aliases) instead of
a second UPDATE. malware.rs no longer claims "both engines": it names the
MCP server as the mirror of the same three rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire added this pull request to the merge queue Oct 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 18, 2026
runyourempire and others added 3 commits October 19, 2026 00:54
…WE ids stored

`fourda_depengine::is_malware(id, aliases, cwe_ids)`: a malicious-package
record is an OSV `MAL-` id or alias, or carries CWE-506 in
database_specific.cwe_ids. GitHub's malware advisories often have no
`MAL-` alias: crossenv GHSA-c2m4-w5hm-vqjw ("crossenv is malware") has
aliases ["CVE-2017-16074"] and cwe_ids ["CWE-506"] (live 2026-10-18), so
the parent-release predicate read it as an unfixed ordinary advisory. No
summary keyword is read (the release gate's "Malicious code in" prefix
check is replaced by the shared rule).

Phase 128 adds osv_advisories.cwe_ids (additive; schema 128, so a
127 binary refuses the database through the existing too-new guard) and
makes every ecosystem's advisory sync due, so rows stored before it are
re-upserted with their CWE ids on the next cycle instead of staying NULL.
The sync stores database_specific.cwe_ids; StoredAdvisory and
MatchedAdvisory carry them. Wired where MAL- was matched: the
parent-release Advisory (osv/parent_release.rs) and the release gate
(osv/release_advisories.rs). Severity is left to #986. No new egress.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…t stores the CWEs (review of #992)

B1: is_malware(id, aliases, cwe_ids, summary) gains a third rule: the
summary starts with GitHub's malware advisory title template, "Malicious
Package in " or "Malicious code in " (case-insensitive, anchored). Some
GitHub malware records carry neither a MAL- alias nor a CWE:
GHSA-28f8-hqmc-7ph8 ("Malicious Package in ember-power-timepicker",
aliases [], cwe_ids []). Prose that merely mentions malice elsewhere is
not read. The release gate's summary test is restored and the record is
pinned; every caller (parent releases, the release gate, StoredAdvisory,
MatchedAdvisory, the rule vectors) passes the summary.

Preemption: an unrated advisory's urgency reads the shared predicate
(High) instead of a MAL- id prefix only.

Migration: the column is added with add_column_if_missing, and the sync is
made due by setting last_synced_at back exactly the max age
(osv_sync_max_age_hours), not to 1970. The sync stores cwe_ids in its one
upsert (upsert_osv_advisory_record; COALESCE like the aliases) instead of
a second UPDATE. malware.rs no longer claims "both engines": it names the
MCP server as the mirror of the same three rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…ge of #988 and #992)

#988 landed a stack_change test that builds an AdvisoryWindow literal
before #992 added the cwe_ids field, so the merge-queue build failed
with E0063. Same fixture, empty CWE list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 80c8641 Oct 18, 2026
24 checks passed
@runyourempire
runyourempire deleted the fix/malware-cwe branch October 18, 2026 15:14
runyourempire added a commit that referenced this pull request Oct 18, 2026
…depengine::is_malware); crossenv vector on

contains_malware now calls fourda_depengine::is_malware over each
match's (advisory_id, aliases, cwe_ids): a MAL- id or alias, or CWE-506 in
database_specific.cwe_ids. The local MAL-only predicate is deleted. Stacked
on fix/malware-cwe (#992), which stores cwe_ids; this lands after it.

sv-021: crossenv GHSA-c2m4-w5hm-vqjw (live OSV 2026-10-18: aliases
[CVE-2017-16074], cwe_ids [CWE-506], own label HIGH) on the installed
1.0.1 is Critical, its severity its own high. The vector checker reads
database_specific.cwe_ids as osv::sync stores it. Knowledge Gaps reads the
cwe_ids column with the rows it grades.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
runyourempire added a commit that referenced this pull request Oct 18, 2026
…edAdvisory::is_malware); sv-022 title-only malware

contains_malware calls MatchedAdvisory::is_malware, the merged
fourda_depengine::is_malware(id, aliases, cwe_ids, summary): a MAL- id or
alias, CWE-506, or GitHub's "Malicious Package in" / "Malicious code in"
title. #992's unrated-malware test now pins Critical on the alert path
(most_urgent_tier) instead of the deleted summary heuristic.

sv-022: GHSA-28f8-hqmc-7ph8 "Malicious Package in ember-power-timepicker"
(live OSV 2026-10-19: no aliases, cwe_ids [], own label CRITICAL, versions
[1.0.8]) installed is Critical; a unit test pins the title arm beside a low
own grade. Restores a closing brace the rebase dropped in
briefing_deterministic_tests.rs.

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