From 12580050c089d772177ea9b6464cf24a71816f33 Mon Sep 17 00:00:00 2001 From: emmanuelgjr Date: Sun, 30 Aug 2026 10:10:52 -0400 Subject: [PATCH 1/5] T-ENG03: make the webapp data bundles deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every docs/*.js bundle carried a `// Generated: ` header. Nothing else in the generator output varies between runs, so that one line was the whole reason a regenerate on a different day showed four dirty files — and, since #30, the reason the Generator reproducibility job and the `committed entries match a fresh generation` test could only pass on the day the bundles were last committed. - generate.js: drop the run-date lines; the Source header now reads the version from package.json instead of a hard-coded, stale `v1.5.2`. - generate.test.mjs: assert no bundle header names a run or carries a date. - validate.yml: the reproducibility job now diffs every generated artefact (backlinks.json, backlinks.js, frameworks-registry.js too). - CONTRIBUTING.md: document the build contract — generated files, the determinism requirement, and why the bundles are committed (Pages serves docs/ from main; no deploy workflow exists). Determinism only. No structural, route, layout or logo change (C2). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0147wBugcuzLkswKPqgofcke --- .github/workflows/validate.yml | 7 ++++++- CONTRIBUTING.md | 22 ++++++++++++++++++++++ docs/backlinks.js | 1 - docs/data.js | 3 +-- docs/frameworks-registry.js | 1 - docs/incidents.js | 1 - scripts/generate.js | 12 ++++++------ scripts/generate.test.mjs | 14 ++++++++++++++ 8 files changed, 49 insertions(+), 12 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fca25d8..b1d1e74 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -114,8 +114,13 @@ jobs: # And its output must match what is committed, so a hand-edit to a # generated file, or a source change that was never regenerated, fails # here rather than shipping. + # Every generated artefact is listed, and the generator is timestamp-free + # by design (T-ENG03) — a run-date in any header would fail this on day two. - name: Assert generated output is current - run: git diff --exit-code -- data/entries docs/data.js docs/incidents.js + run: >- + git diff --exit-code -- + data/entries data/backlinks.json + docs/data.js docs/backlinks.js docs/frameworks-registry.js docs/incidents.js unit-tests: name: Unit tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2311bf..37350d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,6 +122,28 @@ the corresponding JSON file in `/data/`. The schema is in [`data/schema.json`](data/schema.json). This keeps the machine-readable layer in sync with the markdown. +### Generated files + +`data/entries/*.json`, `data/backlinks.json` and the webapp bundles +(`docs/data.js`, `docs/backlinks.js`, `docs/frameworks-registry.js`, +`docs/incidents.js`) are **written by `scripts/generate.js`, never by hand**. +`data/stats.json` and the README badges come from `npm run stats`. + +The build is deterministic: running `npm run build` twice on the same +sources produces byte-identical output, and the generated files carry no +run date, machine name or other build-time value. CI regenerates everything +and fails on any diff, so after changing a mapping file run: + +```bash +npm run build # generate → validate → stats:check +npm test # includes the determinism and bundle checks +git diff --exit-code # must be clean apart from your intended change +``` + +GitHub Pages serves `docs/` straight from `main`, which is why the bundles +are committed rather than built on deploy. Keep it that way unless the +Pages source is deliberately switched to a workflow-based deploy. + --- ## Code of conduct diff --git a/docs/backlinks.js b/docs/backlinks.js index f323dd8..1c4a46e 100644 --- a/docs/backlinks.js +++ b/docs/backlinks.js @@ -1,5 +1,4 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Generated: 2026-08-28 // Backlinks: 1159 window.CROSSWALK_BACKLINKS = [ { diff --git a/docs/data.js b/docs/data.js index 8442fd5..ab2b5c1 100644 --- a/docs/data.js +++ b/docs/data.js @@ -1,6 +1,5 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Source: OWASP GenAI Crosswalk v1.5.2 -// Generated: 2026-08-28 +// Source: OWASP GenAI Crosswalk v4.0.0 // Entries: 51 window.CROSSWALK_DATA = [ { diff --git a/docs/frameworks-registry.js b/docs/frameworks-registry.js index c6abd6c..efa9a1b 100644 --- a/docs/frameworks-registry.js +++ b/docs/frameworks-registry.js @@ -1,5 +1,4 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Generated: 2026-08-28 // Frameworks: 25 window.CROSSWALK_FRAMEWORKS = [ { diff --git a/docs/incidents.js b/docs/incidents.js index e329a5d..f8884a0 100644 --- a/docs/incidents.js +++ b/docs/incidents.js @@ -1,5 +1,4 @@ // Auto-generated by scripts/generate.js — do not edit manually -// Generated: 2026-08-28 // Incidents: 131 window.CROSSWALK_INCIDENTS = [ { diff --git a/scripts/generate.js b/scripts/generate.js index d1804f1..f73a06a 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -841,15 +841,18 @@ function main() { allEntries.push(entry); } - // Write bundled site data for GitHub Pages query interface + // Write bundled site data for GitHub Pages query interface. + // Bundle headers are deliberately timestamp-free: docs/ is served straight + // from main and CI asserts `git diff --exit-code` on these files, so a + // generated-on date would make every checkout dirty the next day. if (!DRY_RUN && !SINGLE_ID) { + const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version; const docsDir = path.join(ROOT, 'docs'); fs.mkdirSync(docsDir, { recursive: true }); const siteDataPath = path.join(docsDir, 'data.js'); const siteData = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Source: OWASP GenAI Crosswalk v1.5.2`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, + `// Source: OWASP GenAI Crosswalk v${PKG_VERSION}`, `// Entries: ${allEntries.length}`, `window.CROSSWALK_DATA = ${JSON.stringify(allEntries, null, 2)};`, ].join('\n'); @@ -895,7 +898,6 @@ function main() { const siteBacklinksPath = path.join(docsDir, 'backlinks.js'); const siteBacklinks = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, `// Backlinks: ${backlinksArray.length}`, `window.CROSSWALK_BACKLINKS = ${JSON.stringify(backlinksArray, null, 2)};`, ].join('\n'); @@ -920,7 +922,6 @@ function main() { const fwRegistryPath = path.join(docsDir, 'frameworks-registry.js'); const fwRegistryData = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, `// Frameworks: ${fwRegistry.length}`, `window.CROSSWALK_FRAMEWORKS = ${JSON.stringify(fwRegistry, null, 2)};`, ].join('\n'); @@ -934,7 +935,6 @@ function main() { const incPath = path.join(docsDir, 'incidents.js'); const incData = [ `// Auto-generated by scripts/generate.js — do not edit manually`, - `// Generated: ${new Date().toISOString().split('T')[0]}`, `// Incidents: ${incDb.incidents.length}`, `window.CROSSWALK_INCIDENTS = ${JSON.stringify(incDb.incidents, null, 2)};`, ].join('\n'); diff --git a/scripts/generate.test.mjs b/scripts/generate.test.mjs index 8c31dce..c531ae4 100644 --- a/scripts/generate.test.mjs +++ b/scripts/generate.test.mjs @@ -117,6 +117,20 @@ test('DRAFT never survives into a stored enum field', () => { assert.deepEqual(leaked.slice(0, 5), [], `${leaked.length} field(s) stored the literal "DRAFT"`); }); +test('webapp bundles carry no build timestamp', () => { + // docs/ is served straight from main and CI diffs these files against a + // fresh generation. A `// Generated: ` header made that diff fail on + // every day but the one the bundle was committed — so the header must + // describe the data, never the run. + for (const f of BUNDLES.filter((b) => fs.existsSync(b))) { + const header = fs.readFileSync(f, 'utf8').split(/\r?\n/).filter((l) => l.startsWith('//')); + for (const line of header) { + assert.doesNotMatch(line, /Generated:/i, `${path.basename(f)} header names a run: ${line}`); + assert.doesNotMatch(line, /\d{4}-\d{2}-\d{2}/, `${path.basename(f)} header carries a date: ${line}`); + } + } +}); + test('webapp bundles stay in step with the entry files', () => { const src = fs.readFileSync(path.join(ROOT, 'docs', 'data.js'), 'utf8'); const start = src.indexOf('['); From cdd09b6712116d3553fde066d410c91cfefd321e Mon Sep 17 00:00:00 2001 From: emmanuelgjr Date: Mon, 14 Sep 2026 10:08:11 -0400 Subject: [PATCH 2/5] T-STRAT03: derive evidence_count from incident control failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A mapping says a control addresses a risk; an incident's control_failures[] says that control failed in the wild. This joins the two and surfaces the result, without ever counting a claim nobody reviewed. - scripts/evidence.js — the one place the join is defined. Rules (DRAFT, for ratification in docs/EVIDENCE_METHODOLOGY.md): a failure supports a mapping only when the incident exemplifies that entry and names the same framework + control; only confirmed failures (non-empty confirmed_by) count; the unit is the incident. Unabsorbed failures are reported as orphans (possible missing mapping — a human call, C4). - stats.js — new `evidence` block in data/stats.json. - generate.js — `evidence_count` + `evidence.{confirmed,drafted}` on mapping rows that have a linked failure; every other row is byte-identical. Currently 6 rows, all drafted, all evidence_count 0. - validate.js — checkEvidence(): framework and control must resolve in the registry, basis must be quotable, a confirmed failure needs a source_url; orphans warn. Negative-tested with three planted records (5 errors). - compliance-report.js — "Controls that failed in the wild" per framework and in the summary; evidence in the JSON output. - src/index.ts — ControlFailure/ExternalRef types, evidenceFor(), controlFailures({confirmedOnly}). Reads generated fields, so the package cannot disagree with the data it ships. - data/schema.json — documents the two generated fields. Also fixes a pre-existing test race this ticket made more likely: generate.js truncated and rewrote every output even when unchanged, so a suite reading data/entries in parallel could see an empty file ("Unexpected end of JSON input", observed 2 in ~15 runs). It now skips identical writes; 12 consecutive full runs after the change were clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR --- data/entries/AST01.json | 9 ++- data/entries/AST02.json | 18 ++++- data/entries/AST04.json | 9 ++- data/entries/AST06.json | 9 ++- data/entries/AST07.json | 9 ++- data/schema.json | 15 ++++ data/stats.json | 9 +++ docs/EVIDENCE_METHODOLOGY.md | 133 +++++++++++++++++++++++++++++++++++ docs/data.js | 54 ++++++++++++-- scripts/compliance-report.js | 87 +++++++++++++++++++++++ scripts/evidence.js | Bin 0 -> 7397 bytes scripts/evidence.test.mjs | 107 ++++++++++++++++++++++++++++ scripts/generate.js | 50 +++++++++++-- scripts/stats.js | 5 ++ scripts/validate.js | 65 +++++++++++++++++ src/index.test.ts | 38 +++++++++- src/index.ts | 89 +++++++++++++++++++++++ 17 files changed, 686 insertions(+), 20 deletions(-) create mode 100644 docs/EVIDENCE_METHODOLOGY.md create mode 100644 scripts/evidence.js create mode 100644 scripts/evidence.test.mjs diff --git a/data/entries/AST01.json b/data/entries/AST01.json index a8ca487..3c55233 100644 --- a/data/entries/AST01.json +++ b/data/entries/AST01.json @@ -32,7 +32,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST02.json b/data/entries/AST02.json index 7908cdb..632a1ef 100644 --- a/data/entries/AST02.json +++ b/data/entries/AST02.json @@ -32,7 +32,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } }, { "framework": "MAESTRO", @@ -43,7 +50,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-123" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST04.json b/data/entries/AST04.json index 5ee5802..150a0c8 100644 --- a/data/entries/AST04.json +++ b/data/entries/AST04.json @@ -54,7 +54,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-123" + ] + } } ], "tools": [], diff --git a/data/entries/AST06.json b/data/entries/AST06.json index a02ff01..c76958f 100644 --- a/data/entries/AST06.json +++ b/data/entries/AST06.json @@ -32,7 +32,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-124" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST07.json b/data/entries/AST07.json index 390909d..f2c878b 100644 --- a/data/entries/AST07.json +++ b/data/entries/AST07.json @@ -32,7 +32,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-124" + ] + } }, { "framework": "MAESTRO", diff --git a/data/schema.json b/data/schema.json index 2669851..63f7840 100644 --- a/data/schema.json +++ b/data/schema.json @@ -175,6 +175,21 @@ "type": "string", "format": "date", "description": "Date of the most recent human review." + }, + "evidence_count": { + "type": "integer", + "minimum": 0, + "description": "GENERATED by scripts/generate.js from data/incidents.json — do not author. Number of distinct incidents that exemplify this entry and record a CONFIRMED control_failures[] item for this framework + control_id. Drafted failures are never counted. Present only on rows with at least one linked failure; absent means no incident evidence. Method: docs/EVIDENCE_METHODOLOGY.md." + }, + "evidence": { + "type": "object", + "description": "GENERATED — the incident ids behind evidence_count. `confirmed` are counted; `drafted` await human confirmation and are not.", + "additionalProperties": false, + "required": ["confirmed", "drafted"], + "properties": { + "confirmed": { "type": "array", "items": { "type": "string", "pattern": "^INC-\\d{3}$" } }, + "drafted": { "type": "array", "items": { "type": "string", "pattern": "^INC-\\d{3}$" } } + } } } } diff --git a/data/stats.json b/data/stats.json index 9f0abb2..7c10b12 100644 --- a/data/stats.json +++ b/data/stats.json @@ -63,6 +63,15 @@ "incidents": { "total": 131 }, + "evidence": { + "incidents_annotated": 3, + "control_failures": 3, + "confirmed": 0, + "drafted": 3, + "mappings_with_confirmed_evidence": 0, + "mappings_with_drafted_evidence_only": 6, + "orphan_failures": 0 + }, "freshness": { "checked": 3, "current": 2, diff --git a/docs/EVIDENCE_METHODOLOGY.md b/docs/EVIDENCE_METHODOLOGY.md new file mode 100644 index 0000000..d34a6e0 --- /dev/null +++ b/docs/EVIDENCE_METHODOLOGY.md @@ -0,0 +1,133 @@ + + +# Evidence methodology + +> **DRAFT — maintainer ratification required.** The plumbing described here is +> built and running. The *rules* it applies are proposals, marked +> `TODO(maintainer)` where a decision is still open. Until they are ratified, +> no evidence count should be quoted outside this repository. + +Most crosswalks say that a control addresses a risk. Few can show that the +control mattered: that its absence, or its failure, is what let a real incident +happen. This page describes how the crosswalk records that, and — just as +important — what it refuses to count. + +--- + +## The idea in one paragraph + +A **mapping** says *control C addresses risk R*. An **incident** in +`data/incidents.json` can record, in `control_failures[]`, that *control C was +absent, bypassed, misconfigured or failed*. When that incident exemplifies R, +the failure is evidence for the mapping. The number of such incidents is the +mapping's `evidence_count`. + +## Counting rules + +These are implemented in exactly one place, `scripts/evidence.js`, and every +consumer (stats, generator, compliance report, npm package) goes through it. + +| # | Rule | Why | +|---|---|---| +| 1 | A failure supports a mapping only if the incident lists the mapping's entry in `owasp_entries` **and** names the same framework and `control_id`. | A control failing in an unrelated incident says nothing about this risk. | +| 2 | Only a **confirmed** failure — a non-empty `confirmed_by` — counts toward `evidence_count`. | A draft is a claim awaiting review. Counting it would turn a backlog into a statistic. | +| 3 | The unit is the **incident**. One incident naming the same control twice is one piece of evidence. | Otherwise a detailed write-up would outweigh a terse one. | + +Drafted failures are still shown — in `evidence.drafted` on the mapping row, and +in reports — so the review backlog is visible. They are never added to a count. + +`TODO(maintainer)`: ratify rules 1–3, or amend them. + +## What a failure record must contain + +Schema: `data/incidents-schema.json` → `control_failures[]`. + +```json +{ + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "absent", + "basis": "", + "source_url": "https://…", + "confirmed_by": [] +} +``` + +| Field | Rule | +|---|---| +| `framework` | Must be a registry `name` in `data/frameworks/`. | +| `control_id` | Must exist in that registry. | +| `outcome` | `absent` · `present-but-bypassed` · `present-but-misconfigured` · `failed` | +| `basis` | A **quotation** from the source that states the failure. Not an inference from the attack description. | +| `source_url` | Where the quotation can be read. Prefer the primary disclosure over a summary of it. | +| `confirmed_by` | Empty while drafted. A human adds their name on confirmation. | + +`npm run validate` enforces the first four rows and fails a **confirmed** record +that has no `source_url`; a drafted one without it is a warning. + +## Drafting discipline + +Drafting is the part an agent or contributor may do. It is bounded by one rule: +**never assert a failure without a source that says so.** + +- The source must *state* that a control was missing or defeated — "no rate + limiting", "before any trust dialog", "without signature verification". A + description of what the attacker did is not, by itself, a statement about any + control. +- Quote; do not paraphrase into a quote. If the source cannot be fetched, the + failure is not drafted. +- Choosing *which* control failed is a judgment. That is why every draft waits + for confirmation, and why rule 2 exists. +- Drafts are reviewed in **batches of at most 20 incidents**, and incidents with + drafted failures carry the `draft-evidence` tag. + +## Confirmation + +`TODO(maintainer)` — not yet defined. Open questions: + +- **Who may confirm.** A maintainer only, or any reviewer recorded in + `reviewed_by` for that framework? +- **What they check.** Proposed minimum: the quotation appears at `source_url`; + it states a failure rather than an attack; the named control is the one that + failed; the outcome is right. +- **Layer-level controls.** MAESTRO layers (`L1`–`L7`) are `kind: layer` — + architecture context, not controls. A failure recorded against a whole layer + is coarse. Should such records count, count separately, or be re-homed to a + sub-control once issue #31 settles the MAESTRO sub-control ids? +- **Disagreement.** What happens to a draft a reviewer rejects — deleted, or + kept with a rejection note so it is not re-drafted? + +## Orphan failures + +A failure whose incident's entries do not map that control is an **orphan**. +`npm run validate` warns on each one. It is not an error: it may mean a mapping +is missing. Adding that mapping is expert work, not something to do because a +warning asked for it. + +## Where evidence appears + +| Surface | What it shows | +|---|---| +| `data/stats.json` → `evidence` | Corpus totals: annotated incidents, confirmed and drafted failures, mappings with evidence, orphans. | +| `data/entries/*.json` → mapping rows | `evidence_count` and `evidence.{confirmed, drafted}`, only on rows with a linked failure. An absent field means no incident evidence. | +| `npm run compliance` | Per framework: a *Controls that failed in the wild* section, an evidence line per control, and a summary row. | +| npm package | `evidenceFor(entry, framework, control)` and `controlFailures(framework?, control?, { confirmedOnly })`. | +| Webapp | Not yet. The data ships in `docs/data.js`; rendering it is a content change that needs maintainer approval. | + +## What evidence does not mean + +- **No evidence is not weak evidence.** Most mappings will never have an + incident on record. A mapping with `evidence_count` absent is unevidenced, + not disproven. +- **Evidence is not a relationship.** An incident can show a control mattered; + it cannot show the OLIR relationship is `subset-of` rather than + `intersects-with`. That stays expert work. +- **The count reflects the corpus, not the world.** It measures the incidents + this project has recorded and reviewed. It will be biased toward what gets + publicly disclosed. diff --git a/docs/data.js b/docs/data.js index ab2b5c1..a85274e 100644 --- a/docs/data.js +++ b/docs/data.js @@ -10877,7 +10877,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } }, { "framework": "MAESTRO", @@ -11002,7 +11009,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } }, { "framework": "MAESTRO", @@ -11013,7 +11027,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-123" + ] + } }, { "framework": "MAESTRO", @@ -11210,7 +11231,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-123" + ] + } } ], "tools": [], @@ -11386,7 +11414,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-124" + ] + } }, { "framework": "MAESTRO", @@ -11477,7 +11512,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-124" + ] + } }, { "framework": "MAESTRO", diff --git a/scripts/compliance-report.js b/scripts/compliance-report.js index 395d7f3..48acd59 100644 --- a/scripts/compliance-report.js +++ b/scripts/compliance-report.js @@ -34,6 +34,7 @@ const fs = require('fs'); const path = require('path'); +const { deriveEvidence, readEntries, readIncidents } = require('./evidence'); // ── Configuration ──────────────────────────────────────────────────────────── @@ -272,6 +273,38 @@ function getFrameworkNames(entries) { return [...set].sort(); } +// ── Incident evidence (T-STRAT03) ─────────────────────────────────────────── + +let _failedControls; +/** + * Controls that failed in the wild, derived once from data/incidents.json by + * the shared evidence module. Always derived over every entry, not the + * --severity subset: which controls failed is a fact about the incidents, and a + * severity filter must not make a failure disappear from the record. + */ +function failedControls() { + if (!_failedControls) { + _failedControls = deriveEvidence(readEntries(REPO_ROOT), readIncidents(REPO_ROOT)).failedControls; + } + return _failedControls; +} + +/** Failed controls for one framework, keyed by the report's normalised control id. */ +function failuresFor(framework) { + const out = new Map(); + for (const fc of failedControls()) { + if (fc.framework === framework) out.set(normalise(fc.control_id), fc); + } + return out; +} + +/** The evidence block for one control; empty lists when nothing failed. */ +function evidenceOf(fc) { + return fc + ? { confirmed: fc.confirmed, drafted: fc.drafted, outcomes: fc.outcomes } + : { confirmed: [], drafted: [], outcomes: [] }; +} + // ── Framework data extraction ───────────────────────────────────────────────── /** @@ -409,6 +442,9 @@ function renderMarkdown(fw, allEntries, opts) { lines.push(`| Coverage rate | ${(r.coverageRate * 100).toFixed(0)}% |`); lines.push(`| Unique controls referenced | ${r.controls.size} |`); lines.push(`| Registry inventory | ${inventoryLabel(fw)} |`); + const failures = failuresFor(fw); + const confirmedCtl = [...failures.values()].filter(fc => fc.confirmed.length).length; + lines.push(`| Controls with a confirmed in-the-wild failure | ${confirmedCtl} (${failures.size - confirmedCtl} more drafted, unconfirmed) |`); lines.push(`| Critical-severity gaps | ${criticalUncovered.length} |`); lines.push(`| High-severity gaps | ${highUncovered.length} |`); lines.push(''); @@ -506,6 +542,12 @@ function renderMarkdown(fw, allEntries, opts) { if (ctrl.tier) lines.push(`_Tier: ${ctrl.tier}_`); lines.push(''); lines.push(`Addresses: ${entryIds.join(' · ')}`); + const fc = failures.get(ctrl.control_id); + if (fc) { + lines.push(''); + lines.push(`Evidence: **${fc.confirmed.length}** confirmed incident(s)` + + (fc.drafted.length ? ` · ${fc.drafted.length} drafted, awaiting confirmation (${fc.drafted.join(', ')})` : '')); + } if (ctrl.notes.length > 0) { lines.push(''); // Show first two distinct notes @@ -517,6 +559,27 @@ function renderMarkdown(fw, allEntries, opts) { lines.push('---'); lines.push(''); + // Controls that failed in the wild + lines.push('## Controls that failed in the wild'); + lines.push(''); + lines.push('Controls recorded in `data/incidents.json` as absent, bypassed, misconfigured or failed during a real incident. ' + + 'Only **confirmed** failures are evidence; drafted ones are listed so the review backlog is visible, and are not counted. ' + + 'Method: [`docs/EVIDENCE_METHODOLOGY.md`](../docs/EVIDENCE_METHODOLOGY.md).'); + lines.push(''); + if (failures.size === 0) { + lines.push(`No incident yet records a failure of a ${fw} control.`); + } else { + lines.push('| Control | Confirmed incidents | Drafted — not counted | How it failed | Risks |'); + lines.push('|---|---|---|---|---|'); + for (const [cid, f] of failures) { + lines.push(`| **${cid}** | ${f.confirmed.length ? f.confirmed.join(', ') : '0'} | ${f.drafted.length ? f.drafted.join(', ') : '—'} ` + + `| ${f.outcomes.join(', ')} | ${f.entries.join(', ')} |`); + } + } + lines.push(''); + lines.push('---'); + lines.push(''); + // Action plan lines.push('## Action plan'); lines.push(''); @@ -640,6 +703,10 @@ function renderJSON(fw, allEntries) { unique_controls: r.controls.size, critical_gaps: r.uncovered.filter(e => e.severity === 'Critical').length, high_gaps: r.uncovered.filter(e => e.severity === 'High').length, + controls_failed_in_wild: { + confirmed: [...failuresFor(fw).values()].filter(fc => fc.confirmed.length).length, + drafted_only: [...failuresFor(fw).values()].filter(fc => !fc.confirmed.length).length, + }, }, coverage: allEntries.map(e => { const fwMappings = e.mappings.filter(m => m.framework === fw); @@ -655,6 +722,7 @@ function renderJSON(fw, allEntries) { control_name: normalise(m.control_name), tier: m.tier || null, notes: normalise(m.notes), + evidence_count: m.evidence_count || 0, })), }; }), @@ -663,6 +731,7 @@ function renderJSON(fw, allEntries) { control_name: ctrl.control_name, tier: ctrl.tier, entry_ids: ctrl.entries.map(e => e.id), + evidence: evidenceOf(failuresFor(fw).get(ctrl.control_id)), })), }; @@ -713,6 +782,24 @@ function renderSummaryMarkdown(frameworks, allEntries, opts) { lines.push(''); lines.push('---'); lines.push(''); + lines.push('## Controls that failed in the wild'); + lines.push(''); + const inWild = failedControls().filter(fc => frameworks.includes(fc.framework)); + const confirmedN = inWild.filter(fc => fc.confirmed.length).length; + lines.push(`**${confirmedN}** control(s) carry a confirmed in-the-wild failure; ${inWild.length - confirmedN} more are drafted and await human confirmation. ` + + 'Drafts are shown so the backlog is visible — they are not evidence until confirmed. ' + + 'Method: [`docs/EVIDENCE_METHODOLOGY.md`](../docs/EVIDENCE_METHODOLOGY.md).'); + lines.push(''); + if (inWild.length) { + lines.push('| Framework | Control | Confirmed | Drafted — not counted | Risks |'); + lines.push('|---|---|---|---|---|'); + for (const fc of inWild) { + lines.push(`| ${fc.framework} | **${fc.control_id}** | ${fc.confirmed.length} | ${fc.drafted.length} | ${fc.entries.join(', ')} |`); + } + lines.push(''); + } + lines.push('---'); + lines.push(''); lines.push('## Eval coverage'); lines.push(''); lines.push('Run these profiles to validate controls are effective, not just documented:'); diff --git a/scripts/evidence.js b/scripts/evidence.js new file mode 100644 index 0000000000000000000000000000000000000000..214bc527ac5bad4c3f26ea2a5ffc51d2510c0b2d GIT binary patch literal 7397 zcmbVR>u%e~74C08#YwtYQHerlH~mp4b+(2tTenSI*hYiGFys+Aw3tw&mP1()90Prb zK4G7v-#K&PNOFLnUaTC=oH=tY-?@!{|7?G0+Wl!+@9X-W>L%BF`!8PX(F@Y|Wv=T? zr=JY{_uv1aW8Iebn&xV;DC@hhO-0+xsjM@m?kH>Ou5BtRR9P)sZ78{Vb$NAhc=h_t zFa=xYd>EJ*wKnwJpE)9x=Qv|B?^tE$e0 zZM~S&LS>)TohDUPl<`LiJDEoIvu0U$2FrEbbj&Sp%c3K-P;Dm!E47y2GjM!0S7qJd zf7o-TRbJs=R4?aKRx>YydPmHZfz(1li!iqITz9i3XRTIk3ER}PT(EWWCNujd|2#WB z`EYbHd4F>Cm-FNEzn}m5uXLV^hOCUf*X>#u;%c}KTofH&9G+fL(X{Y}ldL7!22RZI z+06!9I1XE4Ug)^Fau` z;i(>tth@vUM-}4>?0!z^oYhfubpGM=?Be~&aZo*}oB9=i-mN9zin5*Sd@^0%4k3eJ zp@Tkc9`wBRz@NzeZA!i@d})ecV;{9Wek4TeCV&UYr)j4Mbq+jw-n0vZ3$o0a2+S&BS+{M|juCt~wq{x8 z8mm15aTqDfK+qAbW@R?BcKU6p4MSOAu4uNL<5Y6=hoRM7QQNGT8v}v)Aw3{+8_u^Z z`2ODBU}=Cupfl?RZ};|~v_WPwbO2-hwk%tn3<@(CzI8hb)y?8P{?5B@`{Mlk3fsJz zBB3|cy-p^RylffsBO0V>M0|Pj56}*NhkNa{&f8-PyDJ@fOmcltmE@<6+yP8D0eVS2H9SWYS72sLPIJ0g<@GfTq0TECy#ZMUk?Z zie;UNolA4%IVBENLweXFSgTvMH66CCTBoIv-<~23wT=BiTz{PqFFg{P?pfZSl&6@MHI zAqLU@KAlfL>8yJN?9C~8K-?Q3wJ%Y~2DUsd*s`M#kmma4arhP}t)!A62&SAvlvws# zmWCQb`bD%E^Jf~Qqpx4-`exTMCpIO2(=KHTyL?l5m(bbd!S~K`K)VFMw77W(O)Ad=-A*Ij)i3}>O z8dQ6Ig!TZ&O{Sps{oh%g>o2aP;&|NEp@Dj;T=^XMXBz~D9^R|PpDa{I)CX$_WYo9F z-(%V!ccv9$8?z&7mK{17Z64p@KS^ADM6$T2uIo&K+hdHEqR|10&x)@~#A%!o6KftE z;*`F177^N$6pnzSl`RtbPzx`68%MR}_*qhzqeVVTiZrAwxG8WcbCHD)qP<9-iKOXF znFJh$Jy&eA4yn-{@4%?NAI9JpG4}$NsB`f}8wL-SAcUHLln8c}AFF|>Bwg5R@9ee1 zKC1j7ON_b-c=3^xNnywonz3mf98=p8p#>V#b(*F^ve6njJKmtDb3b5h-48ufWpuye z(J&T|UgE}~-+c6fA^%)(2mNMDhIM^RpbnxkVgseQK($=My+-iPTnzr$`Dbtbro$-MF@YB*r!z6KI; z0>i{QVZ^N6COeq_ccn^PD|yikekX453kE_Cl93Nh#BPW`Y_@*zh4lJS*uwHst*&p7 z>1|usFFkIBvFCQ$vL+&7#nK-EW!r*~1#Zgg3WX{sga zV8RPX(+GN&yR4V`Ef-OqlISN~%|8jB>}CBWYHLvStci!|VrgdVelWAq;2j@MqRl|1 z;i@1M?&Q|0gULfcv+eQ6{iSK8>$`3ixjZs|cTh~8@ z?;#<87Ca{FFPJt7PZ`c0Y%5k3i@btv)s{C&hU|5_CuZ{CjnHaG7#IpVcyLDBb4IXE z{asJjIDL;hy-S}6Ta*C2v&5xo{#4zw;!8F|W3O>)yQk*>{x+Y-TZeKRT>8Eb5Qk3| zAIl!)7PMY#4lA~jjT(TR!*C4cS^AK1#+!;bjG>L;jsDw+P}^Xa?V@S}(E9u+#-|{V zM~xVqj72wF@I3Eb&aYTJNcNs*BAo|@-p`PShZ=!64{|njMedE7hma4qTQcMiLyxxW z;m+e;RhTuC(399p@VS*YJ@U3Qr(gHwYa6eA-{r+ijR9>7(i;ugcD`kKd!PQFiIQt$ zRAFJV6P3-9q>-lCi!NAgZ21uQMCm*nDebHpsUyd*9N=nAXuT={9M=Uq2s$nA7I5K< zZM-SZ*YZ&6PDH5?ve+DF!5>lx7$S#e1^9p@r1+4Q-J=4do5LzAT&)?RlfaW6-}dsQ z?Y_d9mj__z=} zd-p~6hpvQGv=YYs?St=?(IRd1AilHa*5P1F0x%BOV_+9)!u4n@eAkM304d`xk!LSE XL6*fj`JOxR>b + ({ framework, control_id, outcome: 'absent', basis: BASIS, confirmed_by }); + +const entries = [ + { id: 'R01', mappings: [{ framework: 'FW', control_id: 'C1' }, { framework: 'FW', control_id: 'C2' }] }, + { id: 'R02', mappings: [{ framework: 'FW', control_id: 'C1' }] }, +]; + +test('a draft is reported but never counted', () => { + const idx = indexFailures([{ id: 'INC-001', owasp_entries: ['R01'], control_failures: [failure('C1')] }]); + const ev = evidenceForMapping(idx, 'R01', 'FW', 'C1'); + assert.equal(ev.evidence_count, 0); + assert.deepEqual(ev.drafted, ['INC-001']); + assert.deepEqual(ev.confirmed, []); +}); + +test('confirmation needs a named human, not an empty or blank list', () => { + assert.equal(isConfirmed(failure('C1', [])), false); + assert.equal(isConfirmed(failure('C1', [' '])), false); + assert.equal(isConfirmed({ framework: 'FW', control_id: 'C1' }), false); + assert.equal(isConfirmed(failure('C1', ['reviewer'])), true); +}); + +test('a failure only supports mappings for the entries its incident exemplifies', () => { + const idx = indexFailures([ + { id: 'INC-001', owasp_entries: ['R01'], control_failures: [failure('C1', ['reviewer'])] }, + ]); + assert.equal(evidenceForMapping(idx, 'R01', 'FW', 'C1').evidence_count, 1); + // R02 also maps C1, but INC-001 is not an R02 incident. + assert.equal(evidenceForMapping(idx, 'R02', 'FW', 'C1').evidence_count, 0); +}); + +test('framework and control must both match', () => { + const idx = indexFailures([ + { id: 'INC-001', owasp_entries: ['R01'], control_failures: [failure('C1', ['reviewer'], 'OTHER')] }, + ]); + assert.equal(evidenceForMapping(idx, 'R01', 'FW', 'C1').evidence_count, 0); +}); + +test('one incident naming a control twice is one piece of evidence', () => { + const idx = indexFailures([ + { id: 'INC-001', owasp_entries: ['R01'], control_failures: [failure('C1', ['a']), failure('C1', ['b'])] }, + ]); + assert.equal(evidenceForMapping(idx, 'R01', 'FW', 'C1').evidence_count, 1); +}); + +test('a confirmed record outranks a draft for the same incident and control', () => { + const idx = indexFailures([ + { id: 'INC-001', owasp_entries: ['R01'], control_failures: [failure('C1'), failure('C1', ['reviewer'])] }, + ]); + const ev = evidenceForMapping(idx, 'R01', 'FW', 'C1'); + assert.deepEqual([ev.confirmed, ev.drafted], [['INC-001'], []]); +}); + +test('a failure no mapping absorbs is an orphan, and adds no count', () => { + const { orphans, rows, summary } = deriveEvidence(entries, [ + { id: 'INC-002', owasp_entries: ['R02'], control_failures: [failure('C2', ['reviewer'])] }, + ]); + assert.deepEqual(orphans, [{ incident: 'INC-002', framework: 'FW', control_id: 'C2', entries: ['R02'] }]); + assert.equal(rows.length, 0); + assert.equal(summary.mappings_with_confirmed_evidence, 0); +}); + +test('summary counts reconcile with the corpus', () => { + const { summary, rows, failedControls } = deriveEvidence(entries, [ + { id: 'INC-001', owasp_entries: ['R01', 'R02'], control_failures: [failure('C1', ['reviewer']), failure('C2')] }, + { id: 'INC-002', owasp_entries: ['R02'], control_failures: [] }, + ]); + assert.equal(summary.incidents_annotated, 1); + assert.equal(summary.control_failures, 2); + assert.equal(summary.confirmed + summary.drafted, summary.control_failures); + // C1 confirmed on R01 and R02; C2 drafted on R01. + assert.equal(summary.mappings_with_confirmed_evidence, 2); + assert.equal(summary.mappings_with_drafted_evidence_only, 1); + assert.equal(rows.length, 3); + assert.equal(failedControls[0].control_id, 'C1', 'confirmed failures sort first'); +}); + +test('the real corpus: every row count is backed by a confirmed failure it can name', () => { + const { rows, summary } = deriveEvidence(readEntries(ROOT), readIncidents(ROOT)); + for (const r of rows) { + assert.equal(r.evidence_count, r.confirmed.length, `${r.entry} ${r.framework} ${r.control_id}`); + } + const counted = rows.reduce((n, r) => n + r.evidence_count, 0); + assert.ok(summary.confirmed > 0 || counted === 0, 'evidence counted with no confirmed failure in the corpus'); +}); diff --git a/scripts/generate.js b/scripts/generate.js index f73a06a..b9a4372 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -14,6 +14,7 @@ const fs = require('fs'); const path = require('path'); +const { indexFailures, evidenceForMapping } = require('./evidence'); const ROOT = path.resolve(__dirname, '..'); const ENTRIES_DIR = path.join(ROOT, 'data', 'entries'); @@ -661,6 +662,39 @@ function mergeAudiences(existing, incoming) { return [...s]; } +/** + * Write a generated file only when its content changed. + * + * Regenerating an unchanged corpus used to truncate and rewrite every output, + * so anything reading data/entries at the same moment — the parallel test + * suites, a report script — could read an empty file and fail with + * "Unexpected end of JSON input". Skipping identical writes removes that + * window for the common case and leaves mtimes meaningful. Compared + * EOL-normalised: a CRLF checkout of identical content is not a change. + */ +function writeIfChanged(file, text) { + const norm = (t) => t.replace(/\r\n/g, '\n'); + if (fs.existsSync(file) && norm(fs.readFileSync(file, 'utf8')) === norm(text)) return false; + fs.writeFileSync(file, text, 'utf8'); + return true; +} + +/** + * Attach incident evidence to the mapping rows that have any (T-STRAT03). + * + * Sparse on purpose: a row with no linked control failure is left exactly as + * parsed, so an absent field means "no incident evidence", not "not computed". + * `evidence_count` counts confirmed failures only; drafted ones are listed so + * a reader can see the review backlog without it inflating the count. + */ +function withEvidence(entryId, mappings, failureIndex) { + return mappings.map((m) => { + const ev = evidenceForMapping(failureIndex, entryId, m.framework, m.control_id); + if (!ev.confirmed.length && !ev.drafted.length) return m; + return { ...m, evidence_count: ev.evidence_count, evidence: { confirmed: ev.confirmed, drafted: ev.drafted } }; + }); +} + // ─── Main ───────────────────────────────────────────────────────────────────── function main() { @@ -670,8 +704,10 @@ function main() { // Load incidents index from data/incidents.json if available const incidentsFile = path.join(ROOT, 'data', 'incidents.json'); const incidentsByEntry = {}; // entryId -> [{name, url, year, incident_id}] + let failureIndex = new Map(); // framework+control -> incidents whose control failed if (fs.existsSync(incidentsFile)) { const incDb = JSON.parse(fs.readFileSync(incidentsFile, 'utf8')); + failureIndex = indexFailures(incDb.incidents); for (const inc of incDb.incidents) { for (const eid of (inc.owasp_entries || [])) { if (!incidentsByEntry[eid]) incidentsByEntry[eid] = []; @@ -787,7 +823,7 @@ function main() { severity: vuln.severity, aivss_score: AIVSS_SCORES[id] ?? null, audience: data.audiences.length ? data.audiences : defaultAudience(vuln.source_list), - mappings: data.mappings, + mappings: withEvidence(id, data.mappings, failureIndex), tools: mergeTools(data.tools, toolsSupplement[id] || []), incidents: incidentsByEntry[id] || [], crossrefs: data.crossrefs, @@ -834,7 +870,7 @@ function main() { if (DRY_RUN) { console.log(` [dry-run] Would write ${outPath} (${data.mappings.length} mappings, ${data.tools.length} tools)`); } else { - fs.writeFileSync(outPath, json, 'utf8'); + writeIfChanged(outPath, json); written++; } @@ -856,7 +892,7 @@ function main() { `// Entries: ${allEntries.length}`, `window.CROSSWALK_DATA = ${JSON.stringify(allEntries, null, 2)};`, ].join('\n'); - fs.writeFileSync(siteDataPath, siteData, 'utf8'); + writeIfChanged(siteDataPath, siteData); console.log(`Written docs/data.js (${allEntries.length} entries bundled for site)`); // ── Build backlink index: framework control_id → OWASP entries ── @@ -891,7 +927,7 @@ function main() { return a.control_id.localeCompare(b.control_id); }); const backlinksPath = path.join(ROOT, 'data', 'backlinks.json'); - fs.writeFileSync(backlinksPath, JSON.stringify(backlinksArray, null, 2), 'utf8'); + writeIfChanged(backlinksPath, JSON.stringify(backlinksArray, null, 2)); console.log(`Written data/backlinks.json (${backlinksArray.length} control backlinks)`); // ── Bundle backlinks for site ── @@ -901,7 +937,7 @@ function main() { `// Backlinks: ${backlinksArray.length}`, `window.CROSSWALK_BACKLINKS = ${JSON.stringify(backlinksArray, null, 2)};`, ].join('\n'); - fs.writeFileSync(siteBacklinksPath, siteBacklinks, 'utf8'); + writeIfChanged(siteBacklinksPath, siteBacklinks); console.log(`Written docs/backlinks.js (${backlinksArray.length} backlinks bundled for site)`); // ── Bundle framework registry for site ── @@ -925,7 +961,7 @@ function main() { `// Frameworks: ${fwRegistry.length}`, `window.CROSSWALK_FRAMEWORKS = ${JSON.stringify(fwRegistry, null, 2)};`, ].join('\n'); - fs.writeFileSync(fwRegistryPath, fwRegistryData, 'utf8'); + writeIfChanged(fwRegistryPath, fwRegistryData); console.log(`Written docs/frameworks-registry.js (${fwRegistry.length} frameworks)`); } @@ -938,7 +974,7 @@ function main() { `// Incidents: ${incDb.incidents.length}`, `window.CROSSWALK_INCIDENTS = ${JSON.stringify(incDb.incidents, null, 2)};`, ].join('\n'); - fs.writeFileSync(incPath, incData, 'utf8'); + writeIfChanged(incPath, incData); console.log(`Written docs/incidents.js (${incDb.incidents.length} incidents)`); } } diff --git a/scripts/stats.js b/scripts/stats.js index 56b3249..037aae1 100644 --- a/scripts/stats.js +++ b/scripts/stats.js @@ -30,6 +30,8 @@ const fs = require('fs'); const path = require('path'); +const { deriveEvidence } = require('./evidence'); + const ROOT = path.resolve(__dirname, '..'); const OUT = path.join(ROOT, 'data', 'stats.json'); @@ -191,6 +193,9 @@ function computeStats() { }, mapping_files: { total: mappingFilesTotal, by_list: mappingFilesByList }, incidents: { total: incidents.length }, + // Evidence from incident control_failures (T-STRAT03). `confirmed` is the + // only figure that may be quoted as evidence; `drafted` is review backlog. + evidence: deriveEvidence(entries, incidents).summary, freshness: computeFreshness(), controls: { // `total` is the honest control count: only kind=control. `registry_items` diff --git a/scripts/validate.js b/scripts/validate.js index dce3942..fe53e5e 100644 --- a/scripts/validate.js +++ b/scripts/validate.js @@ -807,6 +807,70 @@ function checkMaestroLayers() { return bad === 0; } +/** + * Evidence guard (T-STRAT03). + * + * `control_failures[]` feed `evidence_count` on mapping rows, so a bad record + * does not stay in incidents.json — it becomes a number beside a mapping. Each + * record must point at a real registry control, carry a basis long enough to be + * a quotation, and, once confirmed, a source a reader can open. + * + * A failure that no mapping absorbs is only a warning: it may mean a mapping is + * missing, and deciding that is expert work (C4), not a validator's. + */ +function checkEvidence() { + const incPath = path.join(ROOT, 'data', 'incidents.json'); + const fwDir = path.join(ROOT, 'data', 'frameworks'); + if (!fs.existsSync(incPath) || !fs.existsSync(fwDir)) return true; + + const { deriveEvidence, isConfirmed, readEntries, readIncidents } = require('./evidence'); + const registries = new Map(); + for (const f of fs.readdirSync(fwDir).filter((f) => f.endsWith('.json'))) { + const r = JSON.parse(fs.readFileSync(path.join(fwDir, f), 'utf8')); + registries.set(r.name, new Set((r.controls || []).map((c) => c.control_id))); + } + + const incidents = readIncidents(ROOT); + let bad = 0; + let total = 0; + for (const inc of incidents) { + (inc.control_failures || []).forEach((cf, i) => { + total++; + const at = `${inc.id} control_failures[${i}]`; + const ids = registries.get(cf.framework); + if (!ids) { + fail('Evidence', `${at}: framework "${cf.framework}" is not a registry name in data/frameworks/`); + bad++; + } else if (!ids.has(cf.control_id)) { + fail('Evidence', `${at}: "${cf.control_id}" is not a control in the ${cf.framework} registry`); + bad++; + } + if (typeof cf.basis !== 'string' || cf.basis.trim().length < 20) { + fail('Evidence', `${at}: no quotable basis — a failure without a source quote is not evidence`); + bad++; + } + if (!cf.source_url) { + if (isConfirmed(cf)) { + fail('Evidence', `${at}: confirmed, but no source_url — a reader cannot check the quote`); + bad++; + } else { + warn('Evidence', `${at}: drafted without a source_url — add one before confirmation`); + } + } + }); + } + + const { orphans, summary } = deriveEvidence(readEntries(ROOT), incidents); + for (const o of orphans) { + warn('Evidence', `${o.incident}: ${o.framework} ${o.control_id} failed, but none of ${o.entries.join(', ')} maps it — missing mapping? (human call, C4)`); + } + + if (!bad) { + pass('Evidence', `${total} control failure(s) resolve to registry controls with a basis — ${summary.confirmed} confirmed, ${summary.drafted} drafted`); + } + return bad === 0; +} + function run() { const args = process.argv.slice(2); const quickMode = args.includes('--quick'); @@ -856,6 +920,7 @@ function run() { checkCrossRefFrameworks(); checkFrameworkVersions(); checkMaestroLayers(); + checkEvidence(); } // Encoding guard — mapping files plus the shared/root markdown they link to diff --git a/src/index.test.ts b/src/index.test.ts index 07d38d8..8d69815 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -2,7 +2,7 @@ import { describe, it } from 'node:test'; import * as assert from 'node:assert/strict'; import { entries, incidents, getEntry, getFramework, searchEntries, frameworks, getBySeverity, getIncidentsForEntry, registries, stats, version, getControl, controlsFor, entriesFor, - coverage } from './index'; + coverage, evidenceFor, controlFailures } from './index'; describe('@owasp/genai-crosswalk', () => { // Asserted against the generated count rather than a literal. This test said @@ -162,4 +162,40 @@ describe('@owasp/genai-crosswalk', () => { `${unresolved}/${total} mappings unresolvable — worse than the known baseline`, ); }); + + // ── Incident evidence ───────────────────────────────────────────────────── + it('evidenceFor never counts a draft', () => { + // Every incident counted for a mapping must hold a confirmed failure of that control. + for (const e of entries) { + for (const m of e.mappings) { + const confirmedHere = new Set( + controlFailures(m.framework, m.control_id, { confirmedOnly: true }).map((f) => f.incident_id), + ); + for (const id of evidenceFor(e.id, m.framework, m.control_id)!.confirmed) { + assert.ok(confirmedHere.has(id), `${e.id} ${m.framework} ${m.control_id} counts ${id}, which is not confirmed`); + } + } + } + }); + + it('evidence_count equals the confirmed incidents it names, and agrees with stats', () => { + let withConfirmed = 0; + for (const e of entries) { + for (const m of e.mappings) { + const ev = evidenceFor(e.id, m.framework, m.control_id)!; + assert.equal(ev.evidence_count, ev.confirmed.length); + if (ev.evidence_count > 0) withConfirmed++; + } + } + assert.equal(withConfirmed, stats.evidence.mappings_with_confirmed_evidence); + }); + + it('evidenceFor is undefined for a control the entry does not map', () => { + assert.equal(evidenceFor('LLM01', 'MAESTRO', 'L99'), undefined); + }); + + it('controlFailures confirmedOnly returns only confirmed records', () => { + assert.ok(controlFailures(undefined, undefined, { confirmedOnly: true }).every((f) => f.confirmed)); + assert.equal(controlFailures().length, stats.evidence.control_failures); + }); }); diff --git a/src/index.ts b/src/index.ts index 33a2b6f..84989cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,14 @@ export interface Mapping { control_name: string; tier?: string; notes?: string; + /** + * Distinct incidents that exemplify this entry and record a CONFIRMED failure + * of this control. Generated; present only on rows with linked failures. + * Drafted failures are never counted — see `evidence.drafted`. + */ + evidence_count?: number; + /** Incident ids behind `evidence_count`, and drafts awaiting confirmation. */ + evidence?: { confirmed: string[]; drafted: string[] }; } export interface Tool { @@ -69,6 +77,27 @@ export interface Incident { mitigations: string[]; references: Reference[]; tags: string[]; + external_refs?: ExternalRef[]; + control_failures?: ControlFailure[]; +} + +/** A stable identifier that lets a reader check an incident independently. */ +export interface ExternalRef { + source: 'CVE' | 'AIID' | 'AIAAIC' | 'MITRE-ATLAS' | 'GHSA' | 'campaign' | 'vendor-advisory' | 'research'; + id: string; + url?: string; +} + +/** A control that was absent, bypassed, misconfigured or failed in an incident. */ +export interface ControlFailure { + framework: string; + control_id: string; + outcome: 'absent' | 'present-but-bypassed' | 'present-but-misconfigured' | 'failed'; + /** Quotation from the cited source. */ + basis: string; + source_url?: string; + /** Who confirmed the claim. Empty means drafted, not evidence. */ + confirmed_by?: string[]; } export interface CrosswalkDB { @@ -237,6 +266,15 @@ export interface Stats { }; mapping_files: { total: number; by_list: Record }; incidents: { total: number }; + evidence: { + incidents_annotated: number; + control_failures: number; + confirmed: number; + drafted: number; + mappings_with_confirmed_evidence: number; + mappings_with_drafted_evidence_only: number; + orphan_failures: number; + }; controls: { total: number; registry_items: number; by_kind: Record }; } @@ -346,3 +384,54 @@ export function coverage(framework: string): { unresolved: [...cited].filter((id) => !known.has(id)).length, }; } + +// ── Incident evidence ─────────────────────────────────────────────────────── +// +// Which controls failed in real incidents, and how much that supports each +// mapping. The counting rules live in the generator (scripts/evidence.js); the +// package reads the generated fields rather than re-deriving them, so the npm +// API cannot disagree with the data files it ships. + +/** + * Incident evidence for one mapping row. + * + * `evidence_count` counts confirmed failures only. `drafted` lists failures + * still awaiting human confirmation — useful to see, not to cite. + * Returns undefined when the entry does not map that control. + */ +export function evidenceFor( + entryId: string, + framework: string, + controlId: string, +): { evidence_count: number; confirmed: string[]; drafted: string[] } | undefined { + const row = getEntry(entryId)?.mappings.find((m) => m.framework === framework && m.control_id === controlId); + if (!row) return undefined; + return { + evidence_count: row.evidence_count ?? 0, + confirmed: row.evidence?.confirmed ?? [], + drafted: row.evidence?.drafted ?? [], + }; +} + +/** + * Every recorded control failure, flattened with its incident id. + * + * Filter by framework, and optionally control. Pass `{ confirmedOnly: true }` + * to exclude drafts — do this for anything presented as evidence. + */ +export function controlFailures( + framework?: string, + controlId?: string, + opts: { confirmedOnly?: boolean } = {}, +): Array { + return incidents.flatMap((inc) => + (inc.control_failures ?? []) + .filter((f) => (!framework || f.framework === framework) && (!controlId || f.control_id === controlId)) + .map((f) => ({ + ...f, + incident_id: inc.id, + confirmed: (f.confirmed_by ?? []).some((n) => n.trim() !== ''), + })) + .filter((f) => !opts.confirmedOnly || f.confirmed), + ); +} From 1f08139d00e19ec2e93f9ca0a17ac098d595f4a2 Mon Sep 17 00:00:00 2001 From: emmanuelgjr Date: Mon, 14 Sep 2026 10:08:50 -0400 Subject: [PATCH 3/5] T-STRAT03: key the evidence index on JSON text, not a NUL separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composite-key separator in evidence.js was written to disk as a literal NUL byte, so git classified the file as binary: no diff, no review, and a surprise for anyone opening it. Keys are now JSON.stringify([parts]) — plain text, and no separator a framework name such as "ISO/IEC 27001:2022" could collide with. No output changes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR --- scripts/evidence.js | Bin 7397 -> 7492 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/evidence.js b/scripts/evidence.js index 214bc527ac5bad4c3f26ea2a5ffc51d2510c0b2d..ec7e59defb1c799a399c55177611da86dfdc10b4 100644 GIT binary patch delta 222 zcmaEAdBkc%9b3J=mX?BZer`d2ab`)XLUwAULSnIkSFpdILP=^xiNd3XDGGV{3dN}f ziA9Me`9%sP8Hptdi3({&iMgre`9;|Zc|e9ja(-z}3XsezNzBaCQ_#}q0oV3f43EoTwPHXNuK{G#kym1>|QkOV3$ zDaz0BOsP%S9L~OzTN Date: Mon, 14 Sep 2026 10:15:21 -0400 Subject: [PATCH 4/5] =?UTF-8?q?T-STRAT03:=20pilot=20batch=20=E2=80=94=2018?= =?UTF-8?q?=20drafted=20control=20failures=20across=2013=20AST=20incidents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DRAFT — human confirm. Every confirmed_by is empty; evidence_count stays 0 on every row until a reviewer signs a record off. Each basis is an exact quotation, cut programmatically from the fetched source page and re-checked as a substring; 16 of them were re-fetched and re-checked independently before this commit. Primary disclosures were preferred (GitHub security advisories, Oasis, Check Point-era GHSAs, Snyk, Trail of Bits, Air Security, BlueRock, Antiy CERT) over the AST10 summary pages. Layer and outcome are suggestions for SME review. Selection rules: - the source must state a control was missing or defeated; a description of the attack alone does not qualify - the layer must be mapped by the incident's own entries (no orphans) - hedged quotes ("if any exists") excluded; at most two per incident The three earlier drafts are replaced, not kept alongside: - INC-115: its quote was verbatim but described the attacker writing to MEMORY.md/SOUL.md, not a missing control — it failed the drafting rule - INC-123, INC-124: quotes were not verbatim (re-capitalised; truncated with a full stop for a comma); now quoted from the primary disclosures Not drafted: INC-117 and INC-125 (sources state no control failure); INC-122, INC-130 and one INC-131 candidate would be orphans (MAESTRO L5, not mapped by their entries) — listed in the PR for a human. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR --- data/entries/AST01.json | 39 ++++++--- data/entries/AST02.json | 18 ++-- data/entries/AST03.json | 27 +++++- data/entries/AST04.json | 19 +++- data/entries/AST05.json | 9 +- data/entries/AST06.json | 10 ++- data/entries/AST07.json | 9 +- data/entries/AST08.json | 11 ++- data/entries/AST10.json | 9 +- data/incidents.json | 186 ++++++++++++++++++++++++++++++++++++---- data/stats.json | 8 +- docs/data.js | 151 +++++++++++++++++++++++++------- docs/incidents.js | 186 ++++++++++++++++++++++++++++++++++++---- 13 files changed, 582 insertions(+), 100 deletions(-) diff --git a/data/entries/AST01.json b/data/entries/AST01.json index 3c55233..22c7aff 100644 --- a/data/entries/AST01.json +++ b/data/entries/AST01.json @@ -21,7 +21,17 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115", + "INC-116", + "INC-118", + "INC-120" + ] + } }, { "framework": "MAESTRO", @@ -32,14 +42,7 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [], - "evidence_count": 0, - "evidence": { - "confirmed": [], - "drafted": [ - "INC-115" - ] - } + "reviewed_by": [] }, { "framework": "MAESTRO", @@ -61,7 +64,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-116" + ] + } }, { "framework": "MAESTRO", @@ -72,7 +82,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } } ], "tools": [], diff --git a/data/entries/AST02.json b/data/entries/AST02.json index 632a1ef..10d4b8d 100644 --- a/data/entries/AST02.json +++ b/data/entries/AST02.json @@ -21,7 +21,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } }, { "framework": "MAESTRO", @@ -32,14 +39,7 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [], - "evidence_count": 0, - "evidence": { - "confirmed": [], - "drafted": [ - "INC-115" - ] - } + "reviewed_by": [] }, { "framework": "MAESTRO", diff --git a/data/entries/AST03.json b/data/entries/AST03.json index b746c88..8f422b1 100644 --- a/data/entries/AST03.json +++ b/data/entries/AST03.json @@ -21,7 +21,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-119" + ] + } }, { "framework": "MAESTRO", @@ -32,7 +39,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-126" + ] + } }, { "framework": "MAESTRO", @@ -54,7 +68,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-118" + ] + } } ], "tools": [], diff --git a/data/entries/AST04.json b/data/entries/AST04.json index 150a0c8..cf26742 100644 --- a/data/entries/AST04.json +++ b/data/entries/AST04.json @@ -21,7 +21,15 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-116", + "INC-120" + ] + } }, { "framework": "MAESTRO", @@ -43,7 +51,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-116" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST05.json b/data/entries/AST05.json index d0af778..ae9d427 100644 --- a/data/entries/AST05.json +++ b/data/entries/AST05.json @@ -43,7 +43,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-129" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST06.json b/data/entries/AST06.json index c76958f..cb44f59 100644 --- a/data/entries/AST06.json +++ b/data/entries/AST06.json @@ -21,7 +21,15 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-126", + "INC-127" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST07.json b/data/entries/AST07.json index f2c878b..86cc0a0 100644 --- a/data/entries/AST07.json +++ b/data/entries/AST07.json @@ -50,7 +50,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-131" + ] + } } ], "tools": [], diff --git a/data/entries/AST08.json b/data/entries/AST08.json index 7c4b320..4b4e366 100644 --- a/data/entries/AST08.json +++ b/data/entries/AST08.json @@ -21,7 +21,16 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-121", + "INC-128", + "INC-129" + ] + } }, { "framework": "MAESTRO", diff --git a/data/entries/AST10.json b/data/entries/AST10.json index 32d1080..755b7a2 100644 --- a/data/entries/AST10.json +++ b/data/entries/AST10.json @@ -21,7 +21,14 @@ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-131" + ] + } }, { "framework": "MAESTRO", diff --git a/data/incidents.json b/data/incidents.json index 36df0ad..7fa8509 100644 --- a/data/incidents.json +++ b/data/incidents.json @@ -6964,10 +6964,18 @@ "control_failures": [ { "framework": "MAESTRO", - "control_id": "L3", + "control_id": "L7", "outcome": "absent", - "basis": "Skills also write malicious instructions directly into MEMORY.md and SOUL.md for session-persistent backdooring.", - "source_url": "https://owasp.org/www-project-agentic-skills-top-10/", + "basis": "Instead, it exploited the absence of detection, analysis, and risk control capabilities and systems that should have been inherent in its open-source ecosystem.", + "source_url": "https://www.antiy.net/p/clawhavoc-analysis-of-large-scale-poisoning-campaign-targeting-the-openclaw-skill-market-for-ai-agents/", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "present-but-bypassed", + "basis": "It's to evade antivirus scanning - password-protected archives bypass automated analysis because the scanner can't see inside.", + "source_url": "https://web.archive.org/web/20260811020443/https://www.koi.ai/blog/clawhavoc-341-malicious-clawedbot-skills-found-by-the-bot-they-were-targeting", "confirmed_by": [] } ], @@ -7047,7 +7055,26 @@ "ast01", "prompt-injection", "credential-theft", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "absent", + "basis": "No cryptographic signing or verification exists: the official guidance: \"treat third-party skills as trusted code. Read them before enabling.\"", + "source_url": "https://snyk.io/articles/skill-md-shell-access/", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L4", + "outcome": "absent", + "basis": "Default execution runs without sandboxing: OpenClaw documentation explicitly states: \"tools run on the host for the main session, so the agent has full access when it's just you.\"", + "source_url": "https://snyk.io/articles/skill-md-shell-access/", + "confirmed_by": [] + } ] }, { @@ -7166,7 +7193,18 @@ "ast03", "ecosystem-audit", "snyk", - "toxicskills" + "toxicskills", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "absent", + "basis": "No code signing. No security review. No sandbox by default.", + "source_url": "https://snyk.io/blog/toxicskills-malicious-ai-agent-skills-clawhub/", + "confirmed_by": [] + } ] }, { @@ -7224,7 +7262,18 @@ "ast03", "over-privilege", "credential-leak", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "absent", + "basis": "The flaw: It blindly extracts and outputs .jsonl session files without redaction.", + "source_url": "https://snyk.io/blog/openclaw-skills-credential-leaks-research/", + "confirmed_by": [] + } ] }, { @@ -7285,7 +7334,18 @@ "ast04", "typosquatting", "impersonation", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "present-but-bypassed", + "basis": "While ClawHub has recently introduced stronger controls, such as requiring accounts to be one week old and hiding skills with more than three reports, attackers are adapting faster than the platform can police itself.", + "source_url": "https://snyk.io/blog/clawhub-malicious-google-skill-openclaw-malware/", + "confirmed_by": [] + } ] }, { @@ -7336,7 +7396,18 @@ "agentic-skills", "ast08", "scanner-bypass", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "failed", + "basis": "The scanner failed to catch the actual threat because our exfiltration code in the fake Vercel skill didn't match its hardcoded list of \"bad\" strings.", + "source_url": "https://snyk.io/blog/skill-scanner-false-security/", + "confirmed_by": [] + } ] }, { @@ -7471,8 +7542,16 @@ "framework": "MAESTRO", "control_id": "L6", "outcome": "present-but-bypassed", - "basis": "Repository-controlled configuration files can silently execute arbitrary shell commands and exfiltrate API keys at project open time, before any trust dialog.", - "source_url": "https://owasp.org/www-project-agentic-skills-top-10/", + "basis": "Due to a bug in the startup trust dialog implementation, Claude Code could be tricked to execute code contained in a project before the user accepted the startup trust dialog.", + "source_url": "https://github.com/anthropics/claude-code/security/advisories/GHSA-4fgq-fpq9-mr3g", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "present-but-bypassed", + "basis": "Claude Code would issue API requests before showing the trust prompt, including potentially leaking the user's API keys.", + "source_url": "https://github.com/anthropics/claude-code/security/advisories/GHSA-jh7p-qr78-84p7", "confirmed_by": [] } ], @@ -7557,9 +7636,17 @@ { "framework": "MAESTRO", "control_id": "L6", - "outcome": "absent", - "basis": "Malicious websites can brute-force localhost WebSocket connections with no rate limiting to silently hijack local OpenClaw instances, register new devices without user prompts.", - "source_url": "https://owasp.org/www-project-agentic-skills-top-10/", + "outcome": "present-but-misconfigured", + "basis": "The gateway's rate limiter completely exempts loopback connections—failed attempts are not counted, not throttled, and not logged.", + "source_url": "https://www.oasis.security/blog/openclaw-vulnerability", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "present-but-misconfigured", + "basis": "The gateway auto-approves device pairings from localhost with no user prompt.", + "source_url": "https://www.oasis.security/blog/openclaw-vulnerability", "confirmed_by": [] } ], @@ -7700,7 +7787,18 @@ "ast06", "mcp", "ssrf", - "cloud-credentials" + "cloud-credentials", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L4", + "outcome": "absent", + "basis": "MarkItDownMCP does not validate the urls provided to it.", + "source_url": "https://www.bluerock.io/post/mcp-furi-microsoft-markitdown-vulnerabilities", + "confirmed_by": [] + } ] }, { @@ -7765,7 +7863,18 @@ "ast09", "shadow-ai", "exposure", - "openclaw" + "openclaw", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L4", + "outcome": "present-but-misconfigured", + "basis": "Out of the box, OpenClaw binds by default to 0.0.0.0:18789, meaning it listens on all interfaces unless an operator explicitly restricts it.", + "source_url": "https://www.bitdefender.com/en-us/blog/hotforsecurity/135k-openclaw-ai-agents-exposed-online", + "confirmed_by": [] + } ] }, { @@ -7824,7 +7933,18 @@ "ast08", "scanner-bypass", "prompt-injection", - "trail-of-bits" + "trail-of-bits", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "present-but-bypassed", + "basis": "We recently bypassed ClawHub’s malicious skill detector, Cisco’s agent skill scanner, and all three of the scanners integrated into skills.sh.", + "source_url": "https://blog.trailofbits.com/2026/06/03/the-sorry-state-of-skill-distribution/", + "confirmed_by": [] + } ] }, { @@ -7891,7 +8011,26 @@ "ast08", "external-instructions", "scanner-bypass", - "air-security" + "air-security", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "failed", + "basis": "We started by running our skill locally against Cisco's and Nvidia's scanners, and all of skills.sh's scanners: All vetted brand-landingpage as safe.", + "source_url": "https://www.air.security/blog-posts/the-story-of-skills", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "failed", + "basis": "The bottom line: every signal people use to judge a skill cleared it - scanners, stars, reputation. Every one of them failed, and 26,000 agents were compromised.", + "source_url": "https://www.air.security/blog-posts/the-story-of-skills", + "confirmed_by": [] + } ] }, { @@ -8015,7 +8154,18 @@ "ast10", "dependency-hijack", "supply-chain", - "air-security" + "air-security", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "present-but-misconfigured", + "basis": "GitHub does block re-creating previously very popular repositories, but the bar to qualify is high enough that most of the dangling repos we found weren't covered by it.", + "source_url": "https://www.air.security/blog-posts/skilljacking", + "confirmed_by": [] + } ] } ] diff --git a/data/stats.json b/data/stats.json index 7c10b12..8eb3a68 100644 --- a/data/stats.json +++ b/data/stats.json @@ -64,12 +64,12 @@ "total": 131 }, "evidence": { - "incidents_annotated": 3, - "control_failures": 3, + "incidents_annotated": 13, + "control_failures": 18, "confirmed": 0, - "drafted": 3, + "drafted": 18, "mappings_with_confirmed_evidence": 0, - "mappings_with_drafted_evidence_only": 6, + "mappings_with_drafted_evidence_only": 18, "orphan_failures": 0 }, "freshness": { diff --git a/docs/data.js b/docs/data.js index a85274e..2d4a204 100644 --- a/docs/data.js +++ b/docs/data.js @@ -10866,7 +10866,17 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115", + "INC-116", + "INC-118", + "INC-120" + ] + } }, { "framework": "MAESTRO", @@ -10877,14 +10887,7 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [], - "evidence_count": 0, - "evidence": { - "confirmed": [], - "drafted": [ - "INC-115" - ] - } + "reviewed_by": [] }, { "framework": "MAESTRO", @@ -10906,7 +10909,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-116" + ] + } }, { "framework": "MAESTRO", @@ -10917,7 +10927,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } } ], "tools": [], @@ -10998,7 +11015,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-115" + ] + } }, { "framework": "MAESTRO", @@ -11009,14 +11033,7 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [], - "evidence_count": 0, - "evidence": { - "confirmed": [], - "drafted": [ - "INC-115" - ] - } + "reviewed_by": [] }, { "framework": "MAESTRO", @@ -11102,7 +11119,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-119" + ] + } }, { "framework": "MAESTRO", @@ -11113,7 +11137,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-126" + ] + } }, { "framework": "MAESTRO", @@ -11135,7 +11166,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-118" + ] + } } ], "tools": [], @@ -11198,7 +11236,15 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-116", + "INC-120" + ] + } }, { "framework": "MAESTRO", @@ -11220,7 +11266,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-116" + ] + } }, { "framework": "MAESTRO", @@ -11329,7 +11382,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-129" + ] + } }, { "framework": "MAESTRO", @@ -11403,7 +11463,15 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-126", + "INC-127" + ] + } }, { "framework": "MAESTRO", @@ -11530,7 +11598,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-131" + ] + } } ], "tools": [], @@ -11587,7 +11662,16 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-121", + "INC-128", + "INC-129" + ] + } }, { "framework": "MAESTRO", @@ -11751,7 +11835,14 @@ window.CROSSWALK_DATA = [ "notes": "DRAFT — SME review required", "framework_version": "MAESTRO 1.0", "confidence": "unreviewed", - "reviewed_by": [] + "reviewed_by": [], + "evidence_count": 0, + "evidence": { + "confirmed": [], + "drafted": [ + "INC-131" + ] + } }, { "framework": "MAESTRO", diff --git a/docs/incidents.js b/docs/incidents.js index f8884a0..1853c6d 100644 --- a/docs/incidents.js +++ b/docs/incidents.js @@ -6962,10 +6962,18 @@ window.CROSSWALK_INCIDENTS = [ "control_failures": [ { "framework": "MAESTRO", - "control_id": "L3", + "control_id": "L7", "outcome": "absent", - "basis": "Skills also write malicious instructions directly into MEMORY.md and SOUL.md for session-persistent backdooring.", - "source_url": "https://owasp.org/www-project-agentic-skills-top-10/", + "basis": "Instead, it exploited the absence of detection, analysis, and risk control capabilities and systems that should have been inherent in its open-source ecosystem.", + "source_url": "https://www.antiy.net/p/clawhavoc-analysis-of-large-scale-poisoning-campaign-targeting-the-openclaw-skill-market-for-ai-agents/", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "present-but-bypassed", + "basis": "It's to evade antivirus scanning - password-protected archives bypass automated analysis because the scanner can't see inside.", + "source_url": "https://web.archive.org/web/20260811020443/https://www.koi.ai/blog/clawhavoc-341-malicious-clawedbot-skills-found-by-the-bot-they-were-targeting", "confirmed_by": [] } ], @@ -7045,7 +7053,26 @@ window.CROSSWALK_INCIDENTS = [ "ast01", "prompt-injection", "credential-theft", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "absent", + "basis": "No cryptographic signing or verification exists: the official guidance: \"treat third-party skills as trusted code. Read them before enabling.\"", + "source_url": "https://snyk.io/articles/skill-md-shell-access/", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L4", + "outcome": "absent", + "basis": "Default execution runs without sandboxing: OpenClaw documentation explicitly states: \"tools run on the host for the main session, so the agent has full access when it's just you.\"", + "source_url": "https://snyk.io/articles/skill-md-shell-access/", + "confirmed_by": [] + } ] }, { @@ -7164,7 +7191,18 @@ window.CROSSWALK_INCIDENTS = [ "ast03", "ecosystem-audit", "snyk", - "toxicskills" + "toxicskills", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "absent", + "basis": "No code signing. No security review. No sandbox by default.", + "source_url": "https://snyk.io/blog/toxicskills-malicious-ai-agent-skills-clawhub/", + "confirmed_by": [] + } ] }, { @@ -7222,7 +7260,18 @@ window.CROSSWALK_INCIDENTS = [ "ast03", "over-privilege", "credential-leak", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "absent", + "basis": "The flaw: It blindly extracts and outputs .jsonl session files without redaction.", + "source_url": "https://snyk.io/blog/openclaw-skills-credential-leaks-research/", + "confirmed_by": [] + } ] }, { @@ -7283,7 +7332,18 @@ window.CROSSWALK_INCIDENTS = [ "ast04", "typosquatting", "impersonation", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "present-but-bypassed", + "basis": "While ClawHub has recently introduced stronger controls, such as requiring accounts to be one week old and hiding skills with more than three reports, attackers are adapting faster than the platform can police itself.", + "source_url": "https://snyk.io/blog/clawhub-malicious-google-skill-openclaw-malware/", + "confirmed_by": [] + } ] }, { @@ -7334,7 +7394,18 @@ window.CROSSWALK_INCIDENTS = [ "agentic-skills", "ast08", "scanner-bypass", - "snyk" + "snyk", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "failed", + "basis": "The scanner failed to catch the actual threat because our exfiltration code in the fake Vercel skill didn't match its hardcoded list of \"bad\" strings.", + "source_url": "https://snyk.io/blog/skill-scanner-false-security/", + "confirmed_by": [] + } ] }, { @@ -7469,8 +7540,16 @@ window.CROSSWALK_INCIDENTS = [ "framework": "MAESTRO", "control_id": "L6", "outcome": "present-but-bypassed", - "basis": "Repository-controlled configuration files can silently execute arbitrary shell commands and exfiltrate API keys at project open time, before any trust dialog.", - "source_url": "https://owasp.org/www-project-agentic-skills-top-10/", + "basis": "Due to a bug in the startup trust dialog implementation, Claude Code could be tricked to execute code contained in a project before the user accepted the startup trust dialog.", + "source_url": "https://github.com/anthropics/claude-code/security/advisories/GHSA-4fgq-fpq9-mr3g", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "present-but-bypassed", + "basis": "Claude Code would issue API requests before showing the trust prompt, including potentially leaking the user's API keys.", + "source_url": "https://github.com/anthropics/claude-code/security/advisories/GHSA-jh7p-qr78-84p7", "confirmed_by": [] } ], @@ -7555,9 +7634,17 @@ window.CROSSWALK_INCIDENTS = [ { "framework": "MAESTRO", "control_id": "L6", - "outcome": "absent", - "basis": "Malicious websites can brute-force localhost WebSocket connections with no rate limiting to silently hijack local OpenClaw instances, register new devices without user prompts.", - "source_url": "https://owasp.org/www-project-agentic-skills-top-10/", + "outcome": "present-but-misconfigured", + "basis": "The gateway's rate limiter completely exempts loopback connections—failed attempts are not counted, not throttled, and not logged.", + "source_url": "https://www.oasis.security/blog/openclaw-vulnerability", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L6", + "outcome": "present-but-misconfigured", + "basis": "The gateway auto-approves device pairings from localhost with no user prompt.", + "source_url": "https://www.oasis.security/blog/openclaw-vulnerability", "confirmed_by": [] } ], @@ -7698,7 +7785,18 @@ window.CROSSWALK_INCIDENTS = [ "ast06", "mcp", "ssrf", - "cloud-credentials" + "cloud-credentials", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L4", + "outcome": "absent", + "basis": "MarkItDownMCP does not validate the urls provided to it.", + "source_url": "https://www.bluerock.io/post/mcp-furi-microsoft-markitdown-vulnerabilities", + "confirmed_by": [] + } ] }, { @@ -7763,7 +7861,18 @@ window.CROSSWALK_INCIDENTS = [ "ast09", "shadow-ai", "exposure", - "openclaw" + "openclaw", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L4", + "outcome": "present-but-misconfigured", + "basis": "Out of the box, OpenClaw binds by default to 0.0.0.0:18789, meaning it listens on all interfaces unless an operator explicitly restricts it.", + "source_url": "https://www.bitdefender.com/en-us/blog/hotforsecurity/135k-openclaw-ai-agents-exposed-online", + "confirmed_by": [] + } ] }, { @@ -7822,7 +7931,18 @@ window.CROSSWALK_INCIDENTS = [ "ast08", "scanner-bypass", "prompt-injection", - "trail-of-bits" + "trail-of-bits", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "present-but-bypassed", + "basis": "We recently bypassed ClawHub’s malicious skill detector, Cisco’s agent skill scanner, and all three of the scanners integrated into skills.sh.", + "source_url": "https://blog.trailofbits.com/2026/06/03/the-sorry-state-of-skill-distribution/", + "confirmed_by": [] + } ] }, { @@ -7889,7 +8009,26 @@ window.CROSSWALK_INCIDENTS = [ "ast08", "external-instructions", "scanner-bypass", - "air-security" + "air-security", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L5", + "outcome": "failed", + "basis": "We started by running our skill locally against Cisco's and Nvidia's scanners, and all of skills.sh's scanners: All vetted brand-landingpage as safe.", + "source_url": "https://www.air.security/blog-posts/the-story-of-skills", + "confirmed_by": [] + }, + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "failed", + "basis": "The bottom line: every signal people use to judge a skill cleared it - scanners, stars, reputation. Every one of them failed, and 26,000 agents were compromised.", + "source_url": "https://www.air.security/blog-posts/the-story-of-skills", + "confirmed_by": [] + } ] }, { @@ -8013,7 +8152,18 @@ window.CROSSWALK_INCIDENTS = [ "ast10", "dependency-hijack", "supply-chain", - "air-security" + "air-security", + "draft-evidence" + ], + "control_failures": [ + { + "framework": "MAESTRO", + "control_id": "L7", + "outcome": "present-but-misconfigured", + "basis": "GitHub does block re-creating previously very popular repositories, but the bar to qualify is high enough that most of the dangling repos we found weren't covered by it.", + "source_url": "https://www.air.security/blog-posts/skilljacking", + "confirmed_by": [] + } ] } ]; \ No newline at end of file From 4ea96de4a6377b1349f4bfbe71b96c9611148605 Mon Sep 17 00:00:00 2001 From: emmanuelgjr Date: Mon, 14 Sep 2026 10:55:34 -0400 Subject: [PATCH 5/5] Fix #86: four AST10 incident records said things their sources do not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each change below was checked against the fetched primary source. - INC-124 (ClawJacked): drop CVE-2026-28363 and "CVSS 9.9". NVD describes that CVE as a tools.exec.safeBins allowlist bypass fixed in 2026.2.23, a different bug; the Oasis Security disclosure gives no CVE or CVSS. Description now follows Oasis: brute-forced gateway password, rate limiter exempting localhost, auto-approved localhost pairing, fixed in under 24 hours in 2026.2.25, classified High by OpenClaw. - INC-117: the record said the skills were found by behavioural analysis "rather than by static review" and that registry checks had cleared them. Alice's release says Caterpillar "statically inspects skill logic" and says nothing about registry checks. - INC-119: Snyk's Leaky Skills write-up describes skills that route secrets through the LLM context and logs in plaintext (283 of 3,984), not over-permissioning; the record's reference slug was a guess. - INC-125: Hudson Rock reports one live infection whose files were taken by a broad file-grabbing routine, "not ... a specialized OpenClaw module"; Vidar is its CTO's "likely" attribution to The Hacker News. Primary-source URLs added to references/external_refs for all four. Severity, owasp_entries, MAESTRO layer/role and mitigations unchanged (C4) — where they rested on a corrected claim, that is flagged in the PR. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014SfR2YzLxRH54DAVzDk8gR --- data/entries/AST01.json | 4 +-- data/entries/AST03.json | 2 +- data/entries/AST06.json | 4 +-- data/entries/AST07.json | 2 +- data/entries/AST08.json | 2 +- data/incidents.json | 79 ++++++++++++++++++++++++++--------------- docs/data.js | 14 ++++---- docs/incidents.js | 79 ++++++++++++++++++++++++++--------------- 8 files changed, 116 insertions(+), 70 deletions(-) diff --git a/data/entries/AST01.json b/data/entries/AST01.json index 22c7aff..88a1f77 100644 --- a/data/entries/AST01.json +++ b/data/entries/AST01.json @@ -107,7 +107,7 @@ "incident_id": "INC-116" }, { - "name": "Actively malicious OpenClaw skills in use by 6,000+ users, found by behavioural analysis", + "name": "Actively malicious OpenClaw skills in use by 6,000+ users, flagged by a skill scanner", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-117" @@ -125,7 +125,7 @@ "incident_id": "INC-120" }, { - "name": "Vidar infostealer variants targeting OpenClaw agent identity files", + "name": "Infostealer infection exfiltrates OpenClaw agent identity and memory files", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-125" diff --git a/data/entries/AST03.json b/data/entries/AST03.json index 8f422b1..fccf8d3 100644 --- a/data/entries/AST03.json +++ b/data/entries/AST03.json @@ -87,7 +87,7 @@ "incident_id": "INC-118" }, { - "name": "280+ leaky skills exposing API keys and PII through over-permissioning", + "name": "283 leaky skills pass API keys and PII through the LLM context in plaintext", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-119" diff --git a/data/entries/AST06.json b/data/entries/AST06.json index cb44f59..2fbc088 100644 --- a/data/entries/AST06.json +++ b/data/entries/AST06.json @@ -64,13 +64,13 @@ "tools": [], "incidents": [ { - "name": "ClawJacked — localhost WebSocket hijack of OpenClaw instances (CVE-2026-28363, CVSS 9.9)", + "name": "ClawJacked — any website could take over a local OpenClaw agent via its localhost WebSocket", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-124" }, { - "name": "Vidar infostealer variants targeting OpenClaw agent identity files", + "name": "Infostealer infection exfiltrates OpenClaw agent identity and memory files", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-125" diff --git a/data/entries/AST07.json b/data/entries/AST07.json index 86cc0a0..7af6bd6 100644 --- a/data/entries/AST07.json +++ b/data/entries/AST07.json @@ -63,7 +63,7 @@ "tools": [], "incidents": [ { - "name": "ClawJacked — localhost WebSocket hijack of OpenClaw instances (CVE-2026-28363, CVSS 9.9)", + "name": "ClawJacked — any website could take over a local OpenClaw agent via its localhost WebSocket", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-124" diff --git a/data/entries/AST08.json b/data/entries/AST08.json index 4b4e366..18a0a68 100644 --- a/data/entries/AST08.json +++ b/data/entries/AST08.json @@ -58,7 +58,7 @@ "tools": [], "incidents": [ { - "name": "Actively malicious OpenClaw skills in use by 6,000+ users, found by behavioural analysis", + "name": "Actively malicious OpenClaw skills in use by 6,000+ users, flagged by a skill scanner", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-117" diff --git a/data/incidents.json b/data/incidents.json index 7fa8509..a719f91 100644 --- a/data/incidents.json +++ b/data/incidents.json @@ -7079,12 +7079,12 @@ }, { "id": "INC-117", - "title": "Actively malicious OpenClaw skills in use by 6,000+ users, found by behavioural analysis", + "title": "Actively malicious OpenClaw skills in use by 6,000+ users, flagged by a skill scanner", "date": "2026-02", "year": 2026, "category": "real-world", "severity": "High", - "description": "On 4 February 2026 several published OpenClaw skills were found to be actively malicious while in use by more than 6,000 users. They were detected by behavioural analysis rather than by static review — the registry's own checks had cleared them.", + "description": "On 4 February 2026 Alice reported that its skill scanner, Caterpillar, had flagged several published OpenClaw skills it found to be actively malicious, including skills in use by more than 6,000 OpenClaw users when they were caught. Caterpillar statically inspects skill logic and configurations for injection paths, unsafe tool access and obfuscated behaviour. The release does not say how the skills were published or whether any registry review had examined them.", "owasp_entries": [ "AST01", "AST08" @@ -7094,18 +7094,18 @@ "layer": "L3", "label": "Agent Frameworks", "role": "origin", - "notes": "Published skills cleared by registry checks" + "notes": "Malicious skills published to the OpenClaw skill ecosystem" }, { "layer": "L5", "label": "Evaluation & Observability", "role": "blind-spot", - "notes": "Detection came from runtime behaviour, not from review at publication" + "notes": "Skills were in use by 6,000+ users before a scanner flagged them" } ], - "attack_vector": "Malicious skills published to a registry and installed by users before any behavioural signal surfaced", + "attack_vector": "Malicious skills published to the OpenClaw skill ecosystem and installed by users", "affected": "OpenClaw users — 6,000+ installations", - "impact": "Malicious skill execution in user environments; publication-time review did not catch it", + "impact": "Malicious skill execution in the environments of 6,000+ users before the skills were flagged", "mitigations": [ "Runtime behavioural monitoring of skill execution", "Post-publication continuous rescanning" @@ -7118,7 +7118,8 @@ }, { "source": "research", - "id": "Alice — malicious OpenClaw skills, behavioural detection (2026-02-04)" + "id": "Alice — Caterpillar release: malicious OpenClaw skills used by 6,000+ users (2026-02-04)", + "url": "https://www.prnewswire.com/news-releases/alice-releases-caterpillar-after-catching-malicious-openclaw-skills-used-by-6-000-users-302679381.html" } ], "references": [ @@ -7126,6 +7127,11 @@ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "Alice Releases Caterpillar After Catching Malicious OpenClaw Skills Used by 6,000+ Users", + "url": "https://www.prnewswire.com/news-releases/alice-releases-caterpillar-after-catching-malicious-openclaw-skills-used-by-6-000-users-302679381.html", + "type": "vendor" } ], "tags": [ @@ -7133,7 +7139,7 @@ "ast01", "ast08", "openclaw", - "behavioural-detection" + "skill-scanner" ] }, { @@ -7209,12 +7215,12 @@ }, { "id": "INC-119", - "title": "280+ leaky skills exposing API keys and PII through over-permissioning", + "title": "283 leaky skills pass API keys and PII through the LLM context in plaintext", "date": "2026-02", "year": 2026, "category": "research-demonstrated", "severity": "High", - "description": "Published alongside ToxicSkills on 5 February 2026, Snyk's \"280+ Leaky Skills\" showed credential exposure at scale through over-permissioned skills on OpenClaw and ClawHub — skills granted broader access than their function required, then leaking API keys and PII through it.", + "description": "Published by Snyk on 5 February 2026 as \"280+ Leaky Skills\". Scanning all 3,984 skills on ClawHub, Snyk found 283 (an estimated 7.1% of the registry) with critical flaws that expose sensitive credentials. They are not malware: they are functional, popular skills whose instructions make the agent pass API keys, passwords and even credit card numbers through the LLM's context window and output logs in plaintext — for example by telling the agent to echo a secret, or by exporting session logs without redaction.", "owasp_entries": [ "AST03" ], @@ -7223,7 +7229,7 @@ "layer": "L3", "label": "Agent Frameworks", "role": "origin", - "notes": "Skills declare more permission than their function needs" + "notes": "Skill instructions direct the agent to handle secrets in plaintext" }, { "layer": "L6", @@ -7232,7 +7238,7 @@ "notes": "API key and PII exposure through the granted scope" } ], - "attack_vector": "Over-broad skill permissions turn ordinary skill execution into credential and PII disclosure", + "attack_vector": "Skill instructions route secrets and PII through the LLM context, conversation history and logs in plaintext", "affected": "OpenClaw / ClawHub — 280+ skills", "impact": "API key and PII exposure at ecosystem scale", "mitigations": [ @@ -7247,7 +7253,8 @@ }, { "source": "research", - "id": "Snyk — 280+ Leaky Skills: How OpenClaw & ClawHub Are Exposing API Keys and PII (2026-02-05)" + "id": "Snyk — 280+ Leaky Skills: How OpenClaw & ClawHub Are Exposing API Keys and PII (2026-02-05)", + "url": "https://snyk.io/blog/openclaw-skills-credential-leaks-research/" } ], "references": [ @@ -7255,6 +7262,11 @@ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "280+ Leaky Skills: How OpenClaw & ClawHub Are Exposing API Keys and PII", + "url": "https://snyk.io/blog/openclaw-skills-credential-leaks-research/", + "type": "research" } ], "tags": [ @@ -7574,12 +7586,12 @@ }, { "id": "INC-124", - "title": "ClawJacked — localhost WebSocket hijack of OpenClaw instances (CVE-2026-28363, CVSS 9.9)", + "title": "ClawJacked — any website could take over a local OpenClaw agent via its localhost WebSocket", "date": "2026-02", "year": 2026, "category": "real-world", "severity": "Critical", - "description": "Disclosed by Oasis Security on 26 February 2026. Malicious websites could brute-force localhost WebSocket connections with no rate limiting to silently hijack local OpenClaw instances, register new devices without user prompts, and exfiltrate data through the agent's existing integrations. OpenClaw patched within 24 hours in version 2026.2.25.", + "description": "Disclosed by Oasis Security on 26 February 2026. A malicious website could open a WebSocket to the OpenClaw gateway on localhost and brute-force the gateway password at hundreds of attempts per second, because the gateway's rate limiter exempted localhost connections. Once authenticated it registered as a trusted device — the gateway auto-approved localhost pairings with no user prompt — giving full control of the agent and its connected nodes and integrations. The OpenClaw team classified the issue High severity and shipped a fix in under 24 hours, in version 2026.2.25. No CVE identifier is given in the disclosure.", "owasp_entries": [ "AST06", "AST07" @@ -7604,7 +7616,7 @@ "notes": "Exfiltration through the agent's existing integrations" } ], - "attack_vector": "Browser-originated brute force against an unauthenticated, unrate-limited localhost WebSocket", + "attack_vector": "Browser-originated brute force of the gateway password over a localhost WebSocket exempt from rate limiting", "affected": "OpenClaw before 2026.2.25", "impact": "Silent takeover of a local agent instance and data exfiltration through its connected integrations", "mitigations": [ @@ -7618,18 +7630,14 @@ "id": "OWASP-AST10-2026-timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/" }, - { - "source": "CVE", - "id": "CVE-2026-28363", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-28363" - }, { "source": "campaign", "id": "ClawJacked" }, { "source": "research", - "id": "Oasis Security — ClawJacked disclosure (2026-02-26)" + "id": "Oasis Security — ClawJacked disclosure (2026-02-26)", + "url": "https://www.oasis.security/blog/openclaw-vulnerability" } ], "control_failures": [ @@ -7655,13 +7663,17 @@ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "OpenClaw Vulnerability: Website-to-Local Agent Takeover", + "url": "https://www.oasis.security/blog/openclaw-vulnerability", + "type": "disclosure" } ], "tags": [ "agentic-skills", "ast06", "ast07", - "cve", "openclaw", "clawjacked", "websocket", @@ -7670,12 +7682,12 @@ }, { "id": "INC-125", - "title": "Vidar infostealer variants targeting OpenClaw agent identity files", + "title": "Infostealer infection exfiltrates OpenClaw agent identity and memory files", "date": "2026-02", "year": 2026, "category": "real-world", "severity": "High", - "description": "Hudson Rock identified Vidar infostealer variants specifically targeting OpenClaw agent identity files — openclaw.json, device.json, soul.md and memory.md. Commodity infostealer tooling had been retargeted at agent identity and memory as an asset class in its own right.", + "description": "On 16 February 2026 Hudson Rock reported a live infection in which an infostealer exfiltrated a victim's OpenClaw configuration environment: openclaw.json (the gateway authentication token), device.json (the device's cryptographic keys), soul.md and memory files such as AGENTS.md and MEMORY.md. Hudson Rock states the data was not captured by a specialised OpenClaw module; a broad file-grabbing routine swept for sensitive file extensions and directory names such as .openclaw. Hudson Rock's CTO told The Hacker News the stealer was likely a variant of Vidar. Hudson Rock expects dedicated AI-stealer modules to follow.", "owasp_entries": [ "AST01", "AST06" @@ -7694,9 +7706,9 @@ "notes": "Agent identity and memory files exfiltrated" } ], - "attack_vector": "Infostealer malware retargeted to collect agent identity, device and memory files", + "attack_vector": "Generic infostealer file-grabbing that sweeps sensitive extensions and directories, capturing agent identity, device and memory files", "affected": "OpenClaw installations on compromised hosts", - "impact": "Agent identity and memory theft, enabling impersonation and context poisoning", + "impact": "Theft of the gateway token, device keys, and agent identity and memory files, enabling impersonation and exposure of the user's personal context", "mitigations": [ "Treat agent identity files as secrets", "Encrypt agent state at rest", @@ -7710,7 +7722,8 @@ }, { "source": "research", - "id": "Hudson Rock — Vidar variants targeting OpenClaw identity files (2026-02)" + "id": "Hudson Rock — Real-World Infostealer Infection Targeting OpenClaw Configurations (2026-02-16)", + "url": "https://www.infostealers.com/article/hudson-rock-identifies-real-world-infostealer-infection-targeting-openclaw-configurations/" } ], "references": [ @@ -7718,6 +7731,16 @@ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "Hudson Rock Identifies Real-World Infostealer Infection Targeting OpenClaw Configurations", + "url": "https://www.infostealers.com/article/hudson-rock-identifies-real-world-infostealer-infection-targeting-openclaw-configurations/", + "type": "research" + }, + { + "title": "Infostealer Steals OpenClaw AI Agent Configuration Files and Gateway Tokens", + "url": "https://thehackernews.com/2026/02/infostealer-steals-openclaw-ai-agent.html", + "type": "news" } ], "tags": [ diff --git a/docs/data.js b/docs/data.js index 2d4a204..55c60e4 100644 --- a/docs/data.js +++ b/docs/data.js @@ -10952,7 +10952,7 @@ window.CROSSWALK_DATA = [ "incident_id": "INC-116" }, { - "name": "Actively malicious OpenClaw skills in use by 6,000+ users, found by behavioural analysis", + "name": "Actively malicious OpenClaw skills in use by 6,000+ users, flagged by a skill scanner", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-117" @@ -10970,7 +10970,7 @@ window.CROSSWALK_DATA = [ "incident_id": "INC-120" }, { - "name": "Vidar infostealer variants targeting OpenClaw agent identity files", + "name": "Infostealer infection exfiltrates OpenClaw agent identity and memory files", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-125" @@ -11185,7 +11185,7 @@ window.CROSSWALK_DATA = [ "incident_id": "INC-118" }, { - "name": "280+ leaky skills exposing API keys and PII through over-permissioning", + "name": "283 leaky skills pass API keys and PII through the LLM context in plaintext", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-119" @@ -11506,13 +11506,13 @@ window.CROSSWALK_DATA = [ "tools": [], "incidents": [ { - "name": "ClawJacked — localhost WebSocket hijack of OpenClaw instances (CVE-2026-28363, CVSS 9.9)", + "name": "ClawJacked — any website could take over a local OpenClaw agent via its localhost WebSocket", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-124" }, { - "name": "Vidar infostealer variants targeting OpenClaw agent identity files", + "name": "Infostealer infection exfiltrates OpenClaw agent identity and memory files", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-125" @@ -11611,7 +11611,7 @@ window.CROSSWALK_DATA = [ "tools": [], "incidents": [ { - "name": "ClawJacked — localhost WebSocket hijack of OpenClaw instances (CVE-2026-28363, CVSS 9.9)", + "name": "ClawJacked — any website could take over a local OpenClaw agent via its localhost WebSocket", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-124" @@ -11699,7 +11699,7 @@ window.CROSSWALK_DATA = [ "tools": [], "incidents": [ { - "name": "Actively malicious OpenClaw skills in use by 6,000+ users, found by behavioural analysis", + "name": "Actively malicious OpenClaw skills in use by 6,000+ users, flagged by a skill scanner", "url": "https://github.com/GenAI-Security-Project/GenAI-Data-Security-Initiative/blob/main/crosswalk/data/incidents.json", "year": 2026, "incident_id": "INC-117" diff --git a/docs/incidents.js b/docs/incidents.js index 1853c6d..87e90db 100644 --- a/docs/incidents.js +++ b/docs/incidents.js @@ -7077,12 +7077,12 @@ window.CROSSWALK_INCIDENTS = [ }, { "id": "INC-117", - "title": "Actively malicious OpenClaw skills in use by 6,000+ users, found by behavioural analysis", + "title": "Actively malicious OpenClaw skills in use by 6,000+ users, flagged by a skill scanner", "date": "2026-02", "year": 2026, "category": "real-world", "severity": "High", - "description": "On 4 February 2026 several published OpenClaw skills were found to be actively malicious while in use by more than 6,000 users. They were detected by behavioural analysis rather than by static review — the registry's own checks had cleared them.", + "description": "On 4 February 2026 Alice reported that its skill scanner, Caterpillar, had flagged several published OpenClaw skills it found to be actively malicious, including skills in use by more than 6,000 OpenClaw users when they were caught. Caterpillar statically inspects skill logic and configurations for injection paths, unsafe tool access and obfuscated behaviour. The release does not say how the skills were published or whether any registry review had examined them.", "owasp_entries": [ "AST01", "AST08" @@ -7092,18 +7092,18 @@ window.CROSSWALK_INCIDENTS = [ "layer": "L3", "label": "Agent Frameworks", "role": "origin", - "notes": "Published skills cleared by registry checks" + "notes": "Malicious skills published to the OpenClaw skill ecosystem" }, { "layer": "L5", "label": "Evaluation & Observability", "role": "blind-spot", - "notes": "Detection came from runtime behaviour, not from review at publication" + "notes": "Skills were in use by 6,000+ users before a scanner flagged them" } ], - "attack_vector": "Malicious skills published to a registry and installed by users before any behavioural signal surfaced", + "attack_vector": "Malicious skills published to the OpenClaw skill ecosystem and installed by users", "affected": "OpenClaw users — 6,000+ installations", - "impact": "Malicious skill execution in user environments; publication-time review did not catch it", + "impact": "Malicious skill execution in the environments of 6,000+ users before the skills were flagged", "mitigations": [ "Runtime behavioural monitoring of skill execution", "Post-publication continuous rescanning" @@ -7116,7 +7116,8 @@ window.CROSSWALK_INCIDENTS = [ }, { "source": "research", - "id": "Alice — malicious OpenClaw skills, behavioural detection (2026-02-04)" + "id": "Alice — Caterpillar release: malicious OpenClaw skills used by 6,000+ users (2026-02-04)", + "url": "https://www.prnewswire.com/news-releases/alice-releases-caterpillar-after-catching-malicious-openclaw-skills-used-by-6-000-users-302679381.html" } ], "references": [ @@ -7124,6 +7125,11 @@ window.CROSSWALK_INCIDENTS = [ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "Alice Releases Caterpillar After Catching Malicious OpenClaw Skills Used by 6,000+ Users", + "url": "https://www.prnewswire.com/news-releases/alice-releases-caterpillar-after-catching-malicious-openclaw-skills-used-by-6-000-users-302679381.html", + "type": "vendor" } ], "tags": [ @@ -7131,7 +7137,7 @@ window.CROSSWALK_INCIDENTS = [ "ast01", "ast08", "openclaw", - "behavioural-detection" + "skill-scanner" ] }, { @@ -7207,12 +7213,12 @@ window.CROSSWALK_INCIDENTS = [ }, { "id": "INC-119", - "title": "280+ leaky skills exposing API keys and PII through over-permissioning", + "title": "283 leaky skills pass API keys and PII through the LLM context in plaintext", "date": "2026-02", "year": 2026, "category": "research-demonstrated", "severity": "High", - "description": "Published alongside ToxicSkills on 5 February 2026, Snyk's \"280+ Leaky Skills\" showed credential exposure at scale through over-permissioned skills on OpenClaw and ClawHub — skills granted broader access than their function required, then leaking API keys and PII through it.", + "description": "Published by Snyk on 5 February 2026 as \"280+ Leaky Skills\". Scanning all 3,984 skills on ClawHub, Snyk found 283 (an estimated 7.1% of the registry) with critical flaws that expose sensitive credentials. They are not malware: they are functional, popular skills whose instructions make the agent pass API keys, passwords and even credit card numbers through the LLM's context window and output logs in plaintext — for example by telling the agent to echo a secret, or by exporting session logs without redaction.", "owasp_entries": [ "AST03" ], @@ -7221,7 +7227,7 @@ window.CROSSWALK_INCIDENTS = [ "layer": "L3", "label": "Agent Frameworks", "role": "origin", - "notes": "Skills declare more permission than their function needs" + "notes": "Skill instructions direct the agent to handle secrets in plaintext" }, { "layer": "L6", @@ -7230,7 +7236,7 @@ window.CROSSWALK_INCIDENTS = [ "notes": "API key and PII exposure through the granted scope" } ], - "attack_vector": "Over-broad skill permissions turn ordinary skill execution into credential and PII disclosure", + "attack_vector": "Skill instructions route secrets and PII through the LLM context, conversation history and logs in plaintext", "affected": "OpenClaw / ClawHub — 280+ skills", "impact": "API key and PII exposure at ecosystem scale", "mitigations": [ @@ -7245,7 +7251,8 @@ window.CROSSWALK_INCIDENTS = [ }, { "source": "research", - "id": "Snyk — 280+ Leaky Skills: How OpenClaw & ClawHub Are Exposing API Keys and PII (2026-02-05)" + "id": "Snyk — 280+ Leaky Skills: How OpenClaw & ClawHub Are Exposing API Keys and PII (2026-02-05)", + "url": "https://snyk.io/blog/openclaw-skills-credential-leaks-research/" } ], "references": [ @@ -7253,6 +7260,11 @@ window.CROSSWALK_INCIDENTS = [ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "280+ Leaky Skills: How OpenClaw & ClawHub Are Exposing API Keys and PII", + "url": "https://snyk.io/blog/openclaw-skills-credential-leaks-research/", + "type": "research" } ], "tags": [ @@ -7572,12 +7584,12 @@ window.CROSSWALK_INCIDENTS = [ }, { "id": "INC-124", - "title": "ClawJacked — localhost WebSocket hijack of OpenClaw instances (CVE-2026-28363, CVSS 9.9)", + "title": "ClawJacked — any website could take over a local OpenClaw agent via its localhost WebSocket", "date": "2026-02", "year": 2026, "category": "real-world", "severity": "Critical", - "description": "Disclosed by Oasis Security on 26 February 2026. Malicious websites could brute-force localhost WebSocket connections with no rate limiting to silently hijack local OpenClaw instances, register new devices without user prompts, and exfiltrate data through the agent's existing integrations. OpenClaw patched within 24 hours in version 2026.2.25.", + "description": "Disclosed by Oasis Security on 26 February 2026. A malicious website could open a WebSocket to the OpenClaw gateway on localhost and brute-force the gateway password at hundreds of attempts per second, because the gateway's rate limiter exempted localhost connections. Once authenticated it registered as a trusted device — the gateway auto-approved localhost pairings with no user prompt — giving full control of the agent and its connected nodes and integrations. The OpenClaw team classified the issue High severity and shipped a fix in under 24 hours, in version 2026.2.25. No CVE identifier is given in the disclosure.", "owasp_entries": [ "AST06", "AST07" @@ -7602,7 +7614,7 @@ window.CROSSWALK_INCIDENTS = [ "notes": "Exfiltration through the agent's existing integrations" } ], - "attack_vector": "Browser-originated brute force against an unauthenticated, unrate-limited localhost WebSocket", + "attack_vector": "Browser-originated brute force of the gateway password over a localhost WebSocket exempt from rate limiting", "affected": "OpenClaw before 2026.2.25", "impact": "Silent takeover of a local agent instance and data exfiltration through its connected integrations", "mitigations": [ @@ -7616,18 +7628,14 @@ window.CROSSWALK_INCIDENTS = [ "id": "OWASP-AST10-2026-timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/" }, - { - "source": "CVE", - "id": "CVE-2026-28363", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-28363" - }, { "source": "campaign", "id": "ClawJacked" }, { "source": "research", - "id": "Oasis Security — ClawJacked disclosure (2026-02-26)" + "id": "Oasis Security — ClawJacked disclosure (2026-02-26)", + "url": "https://www.oasis.security/blog/openclaw-vulnerability" } ], "control_failures": [ @@ -7653,13 +7661,17 @@ window.CROSSWALK_INCIDENTS = [ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "OpenClaw Vulnerability: Website-to-Local Agent Takeover", + "url": "https://www.oasis.security/blog/openclaw-vulnerability", + "type": "disclosure" } ], "tags": [ "agentic-skills", "ast06", "ast07", - "cve", "openclaw", "clawjacked", "websocket", @@ -7668,12 +7680,12 @@ window.CROSSWALK_INCIDENTS = [ }, { "id": "INC-125", - "title": "Vidar infostealer variants targeting OpenClaw agent identity files", + "title": "Infostealer infection exfiltrates OpenClaw agent identity and memory files", "date": "2026-02", "year": 2026, "category": "real-world", "severity": "High", - "description": "Hudson Rock identified Vidar infostealer variants specifically targeting OpenClaw agent identity files — openclaw.json, device.json, soul.md and memory.md. Commodity infostealer tooling had been retargeted at agent identity and memory as an asset class in its own right.", + "description": "On 16 February 2026 Hudson Rock reported a live infection in which an infostealer exfiltrated a victim's OpenClaw configuration environment: openclaw.json (the gateway authentication token), device.json (the device's cryptographic keys), soul.md and memory files such as AGENTS.md and MEMORY.md. Hudson Rock states the data was not captured by a specialised OpenClaw module; a broad file-grabbing routine swept for sensitive file extensions and directory names such as .openclaw. Hudson Rock's CTO told The Hacker News the stealer was likely a variant of Vidar. Hudson Rock expects dedicated AI-stealer modules to follow.", "owasp_entries": [ "AST01", "AST06" @@ -7692,9 +7704,9 @@ window.CROSSWALK_INCIDENTS = [ "notes": "Agent identity and memory files exfiltrated" } ], - "attack_vector": "Infostealer malware retargeted to collect agent identity, device and memory files", + "attack_vector": "Generic infostealer file-grabbing that sweeps sensitive extensions and directories, capturing agent identity, device and memory files", "affected": "OpenClaw installations on compromised hosts", - "impact": "Agent identity and memory theft, enabling impersonation and context poisoning", + "impact": "Theft of the gateway token, device keys, and agent identity and memory files, enabling impersonation and exposure of the user's personal context", "mitigations": [ "Treat agent identity files as secrets", "Encrypt agent state at rest", @@ -7708,7 +7720,8 @@ window.CROSSWALK_INCIDENTS = [ }, { "source": "research", - "id": "Hudson Rock — Vidar variants targeting OpenClaw identity files (2026-02)" + "id": "Hudson Rock — Real-World Infostealer Infection Targeting OpenClaw Configurations (2026-02-16)", + "url": "https://www.infostealers.com/article/hudson-rock-identifies-real-world-infostealer-infection-targeting-openclaw-configurations/" } ], "references": [ @@ -7716,6 +7729,16 @@ window.CROSSWALK_INCIDENTS = [ "title": "OWASP Agentic Skills Top 10 — incident timeline", "url": "https://owasp.org/www-project-agentic-skills-top-10/", "type": "research" + }, + { + "title": "Hudson Rock Identifies Real-World Infostealer Infection Targeting OpenClaw Configurations", + "url": "https://www.infostealers.com/article/hudson-rock-identifies-real-world-infostealer-infection-targeting-openclaw-configurations/", + "type": "research" + }, + { + "title": "Infostealer Steals OpenClaw AI Agent Configuration Files and Gateway Tokens", + "url": "https://thehackernews.com/2026/02/infostealer-steals-openclaw-ai-agent.html", + "type": "news" } ], "tags": [