@blamejs/exceptd-skills 0.12.10 → 0.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +3 -1
- package/data/_indexes/_meta.json +5 -5
- package/data/_indexes/activity-feed.json +1 -1
- package/data/_indexes/catalog-summaries.json +1 -1
- package/data/_indexes/frequency.json +2 -0
- package/data/cve-catalog.json +9 -9
- package/data/cwe-catalog.json +4 -3
- package/data/framework-control-gaps.json +52 -0
- package/data/playbooks/library-author.json +3 -3
- package/lib/refresh-external.js +37 -9
- package/lib/source-ghsa.js +7 -1
- package/lib/source-osv.js +228 -57
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.11 — 2026-05-13
|
|
4
|
+
|
|
5
|
+
**Patch: OSV source hardening, indicator regex widening, CWE/framework-gap reconciliation. v0.12.10 audit closeout.**
|
|
6
|
+
|
|
7
|
+
### OSV source hardening
|
|
8
|
+
|
|
9
|
+
`lib/source-osv.js` matures from greenfield to GHSA-parity:
|
|
10
|
+
|
|
11
|
+
- **Structured fixture-I/O error envelope.** Missing or malformed `EXCEPTD_OSV_FIXTURE` paths no longer crash with a Node stack trace; the source returns `{ok:false, error, source:"offline"}` matching the GHSA convention. Operators piping the CLI through `jq` or scripting around exit codes get a structured failure they can branch on.
|
|
12
|
+
- **Case-fold ids before lookup.** `fetchAdvisoryById("mal-2026-3083")` (lowercase) now resolves correctly. OSV.dev's `/v1/vulns/{id}` is case-sensitive — the source uppercases the id at entry before any branch on fixture lookup or network call.
|
|
13
|
+
- **Highest-CVSS-version wins + compute from vector.** `extractCvss` previously overwrote the chosen vector on every loop iteration ("last wins" not "highest-version wins") and returned `null` `score` when the OSV record carried only a vector string with no embedded numeric tail. Both fixed: explicit version-comparison via the `CVSS:N.M` prefix, and a new `cvss3BaseScore(vector)` helper that computes the CVSS 3.1 base score per FIRST §7.1 (handles Scope:U + Scope:C). MAL-* records that previously normalized to `cvss_score: null` / `active_exploitation: "unknown"` now carry computed scores.
|
|
14
|
+
- **GHSA-404 → OSV fallback for CVE-*.** `seedSingleAdvisory` previously routed `CVE-*` unconditionally through `source-ghsa`. When GHSA returned 404 for a CVE that had only PYSEC / RUSTSEC / SNYK / MAL coverage, the operator saw `GHSA returned HTTP 404` even though OSV had the record. Now: on GHSA-404 for a CVE-* id, retry via `source-osv.fetchAdvisoryById(id)`; surface the combined error when both 404.
|
|
15
|
+
- **`epss_note` on non-CVE drafts.** Non-CVE catalog keys (MAL-*, SNYK-*, RUSTSEC-*, etc.) now carry a populated `epss_note` documenting the FIRST EPSS API limitation — drafts no longer look incomplete to downstream consumers grepping for the field.
|
|
16
|
+
- **`verification_sources` deduped.** The canonical `osv.dev/vulnerability/<id>` URL was previously both prepended unconditionally AND pulled from `rec.references[]`. Deduped via `new Set` before return.
|
|
17
|
+
- **`buildDiff` error categorization.** Returns `unreachable_count` + `normalize_error_count` separately so an operator can distinguish "OSV unreachable" from "10 ids returned but none normalized cleanly."
|
|
18
|
+
- **`GHSA-` dropped from `OSV_ID_PREFIXES`.** The export previously listed GHSA-* even though the dispatcher unconditionally routes GHSA-* through `source-ghsa`. `isOsvId("GHSA-...")` now returns false. A top-of-file comment documents the routing decision (GHSA has richer field coverage for that namespace).
|
|
19
|
+
- **`OSV_HOST_OVERRIDE` env var for offline HTTP testing.** New stubbing surface — lets `tests/source-osv.test.js` spin up a local HTTP server to exercise HTTP 500 / 429 / timeout / parse-error paths previously uncovered. 429 surfaces as `rate-limited`; timeout error message clarified.
|
|
20
|
+
- **`seedSingleAdvisory` exported** for in-process testing.
|
|
21
|
+
|
|
22
|
+
### Indicator regex widening
|
|
23
|
+
|
|
24
|
+
`gha-workflow-script-injection-sink` (added v0.12.10) previously anchored on `run:\s*\|` (block-scalar pipe only). Single-line `run: echo "${{ github.event.comment.body }}"` bypassed the regex despite being the same vulnerability class. Widened to `run:[\s\S]*?...` which admits both block-scalar AND single-line forms. The indicator's `confidence` drops from `deterministic` → `high` and `deterministic` flag flips to `false` to reflect the reasoning step still required for the false-positive demotion (sandboxed `pull_request` + `contents: read` permissions). `tests/gha-workflow-script-injection-sink.test.js` lands as a new end-to-end regex test with 8 fixture YAML cases covering both the catch and the FP-demotion classes. All 5 of this repo's own `.github/workflows/*.yml` files remain clean against the widened regex.
|
|
25
|
+
|
|
26
|
+
### CWE reverse-references
|
|
27
|
+
|
|
28
|
+
The v0.12.10 catalog additions cited existing CWEs (CWE-89, CWE-77, CWE-94) without updating their reverse-reference `evidence_cves` arrays. Bidirectional linkage restored: CWE-89 now lists CVE-2026-42208 (LiteLLM SQLi), CWE-77 lists MAL-2026-3083 (elementary-data secondary classification), CWE-94 adds MAL-2026-3083 alongside the existing CVE-2025-53773 and CVE-2026-30615.
|
|
29
|
+
|
|
30
|
+
### Framework-control-gaps key reconciliation
|
|
31
|
+
|
|
32
|
+
Eight `framework_control_gaps` keys used by the v0.12.10 catalog additions did not resolve in `data/framework-control-gaps.json`. Six reconciled to canonical existing forms: `SLSA-L3` → `SLSA-v1.0-Build-L3`; `OWASP-LLM01` → `OWASP-LLM-Top-10-2025-LLM01`; `NIST-800-218-PO.4` → `NIST-800-218-SSDF`; `NIS2-Art21-2d` / `-2g` → `NIS2-Art21-patch-management`; `NIS2-Art21-2e` → `NIS2-Art21-incident-handling`. Two genuinely-distinct citations gained new entries in the framework-gaps catalog: `EU-CRA-Art13` (essential cybersecurity requirements + technical documentation; the elementary-data class of supply-chain compromise where the maintainer is a victim) and `NIST-800-53-SI-10` (information input validation; the trust-boundary-vs-inside-boundary distinction that argument-injection / SQL-injection / prompt-injection exploit). All `framework_control_gaps` references in the catalog now resolve to a real entry.
|
|
33
|
+
|
|
34
|
+
### Repository
|
|
35
|
+
|
|
36
|
+
- `lib/source-ghsa.js` "unrecognized id format" error message widened to enumerate the OSV-native prefixes operators can pass via `--advisory` (was previously CVE/GHSA only).
|
|
37
|
+
- `README.md` documents the OSV source: install command, `--advisory MAL-...` form, `EXCEPTD_OSV_FIXTURE` env var, the fresh-disclosure workflow expanded to mention OSV's coverage breadth.
|
|
38
|
+
|
|
39
|
+
Test count: 462 → 492 (+30: 18 OSV source-hardening tests + 10 indicator regex tests + 2 catalog drift assertions). Predeploy gates: 15/15. Skills: 38/38 signed and verified.
|
|
40
|
+
|
|
3
41
|
## 0.12.10 — 2026-05-13
|
|
4
42
|
|
|
5
43
|
**Patch: OSV.dev wired as an upstream source, three new catalog entries, one new library-author indicator.**
|
package/README.md
CHANGED
|
@@ -135,6 +135,7 @@ You want to refresh CVE/RFC data, run currency checks, or generate reports. Inst
|
|
|
135
135
|
npx @blamejs/exceptd-skills doctor # health check
|
|
136
136
|
npx @blamejs/exceptd-skills refresh --apply --swarm # pull KEV/NVD/EPSS/RFC/GHSA + apply
|
|
137
137
|
npx @blamejs/exceptd-skills refresh --advisory CVE-2026-45321 # seed one CVE draft from GHSA
|
|
138
|
+
npx @blamejs/exceptd-skills refresh --advisory MAL-2026-3083 # seed via OSV (MAL-/SNYK-/RUSTSEC-/USN-/PYSEC-/GO-/MGASA-/UVI-)
|
|
138
139
|
npx @blamejs/exceptd-skills refresh --curate CVE-2026-45321 # surface editorial questions for a draft
|
|
139
140
|
npx @blamejs/exceptd-skills refresh --network # swap data/ from latest signed npm tarball
|
|
140
141
|
```
|
|
@@ -148,7 +149,7 @@ exceptd help
|
|
|
148
149
|
|
|
149
150
|
Air-gapped operation: run `exceptd refresh --prefetch` on a connected host, copy the resulting `.cache/upstream/` to the airgap, run `exceptd refresh --from-cache <path> --apply` over there. The vendored upstream snapshots replace every network call.
|
|
150
151
|
|
|
151
|
-
Fresh-disclosure workflow (v0.12.0): the nightly auto-PR job pulls KEV / NVD / EPSS / IETF / **GHSA** (added in v0.12.0). KEV typically takes days; NVD ~10 days; GHSA fires within hours of disclosure and covers npm + PyPI + Maven + Go + NuGet +
|
|
152
|
+
Fresh-disclosure workflow (v0.12.0): the nightly auto-PR job pulls KEV / NVD / EPSS / IETF / **GHSA** (added in v0.12.0) / **OSV** (added in v0.12.10). KEV typically takes days; NVD ~10 days; GHSA fires within hours of disclosure and covers npm + PyPI + Maven + Go + NuGet + …; OSV aggregates the OSSF Malicious Packages dataset (`MAL-*` keys) + Snyk + RustSec + Mageia + Ubuntu USN + Go Vuln DB + PYSEC + UVI on top of GHSA — useful for malicious-package compromises that don't have CVEs yet (`exceptd refresh --advisory MAL-2026-3083`). New IDs land as drafts (`_auto_imported: true`, `_draft: true`) that the catalog validator treats as warnings, not errors — operators get the fresh entry immediately, editorial review (framework gaps, IoCs, ATLAS/ATT&CK refs) follows via `exceptd refresh --curate <ID>`. For "I want this advisory today, not tomorrow": `exceptd refresh --advisory <CVE-or-GHSA-or-MAL-or-SNYK-or-RUSTSEC-ID> --apply`.
|
|
152
153
|
|
|
153
154
|
Optional env vars for higher rate budgets:
|
|
154
155
|
|
|
@@ -157,6 +158,7 @@ Optional env vars for higher rate budgets:
|
|
|
157
158
|
| `NVD_API_KEY` | Lifts NVD 2.0 from 5 → 50 requests per 30s window. Free key at <https://nvd.nist.gov/developers/request-an-api-key>. |
|
|
158
159
|
| `GITHUB_TOKEN` | Lifts GitHub Releases + GHSA from 60 → 5000 requests per hour. |
|
|
159
160
|
| `EXCEPTD_GHSA_FIXTURE` | Path to a JSON fixture matching the api.github.com/advisories shape. For offline tests + air-gap workflows. |
|
|
161
|
+
| `EXCEPTD_OSV_FIXTURE` | Path to a JSON fixture matching the OSV schema (https://ossf.github.io/osv-schema/). For offline tests + air-gap workflows against the OSV source (added v0.12.10). |
|
|
160
162
|
| `EXCEPTD_REGISTRY_FIXTURE` | Path to a JSON fixture matching the npm registry response. Used by `doctor --registry-check` + `run --upstream-check` + `refresh --network` for offline testing. |
|
|
161
163
|
|
|
162
164
|
### 3. Maintainer (extend / sign / publish)
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-13T21:19:48.889Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 49,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "b7e77cd5de579732b6dd352720557c3ba2ac93f472de50f4e1f861a665a2760b",
|
|
8
8
|
"data/atlas-ttps.json": "f3f75ff2778a0a2c7d953a21386bc4f265cb2685ce41242eee45f9e9f2a6add6",
|
|
9
|
-
"data/cve-catalog.json": "
|
|
10
|
-
"data/cwe-catalog.json": "
|
|
9
|
+
"data/cve-catalog.json": "197f5313d93f0a7225d5ff275e21cbd067b3970a6f2fdc6da35f81c847e8bdee",
|
|
10
|
+
"data/cwe-catalog.json": "19ce1fad3ed0b0687ec9a328b2d6cd1b544eea7f19140234ec1a8467de1f908d",
|
|
11
11
|
"data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
|
|
12
12
|
"data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
|
|
13
13
|
"data/exploit-availability.json": "7dad52f459c324c40aa4df7cd9157f6a19f670fdfb9d8f687d777c9d99798668",
|
|
14
|
-
"data/framework-control-gaps.json": "
|
|
14
|
+
"data/framework-control-gaps.json": "9240ea4a825090fe2716947f2f6f9171c065a133ef003e04d2fbc4f01fc55bdf",
|
|
15
15
|
"data/global-frameworks.json": "84fd19061f052e4ccf66308a7b8d3fd38e00325e97e9e5e19e4d9b302c128957",
|
|
16
16
|
"data/rfc-references.json": "583360bae01e324d752bd28a7d344b4276478381426428d683fc82b0ac19d64a",
|
|
17
17
|
"data/zeroday-lessons.json": "d670e73dfd5237ceb71a56326676d90c05387b9547f8ed6f3a60a153854b444b",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"rebuild_after_days": 365,
|
|
151
151
|
"note": "Per-entry last_verified governs decay. Skills depending on this catalog must check entry freshness before high-stakes use."
|
|
152
152
|
},
|
|
153
|
-
"entry_count":
|
|
153
|
+
"entry_count": 61,
|
|
154
154
|
"sample_keys": [
|
|
155
155
|
"NIST-800-53-SI-2",
|
|
156
156
|
"NIST-800-53-SC-8",
|
package/data/cve-catalog.json
CHANGED
|
@@ -708,11 +708,11 @@
|
|
|
708
708
|
"Set npm registry cooldown: .npmrc `before=72h` (npm 11+) or `minimumReleaseAge=4320` to refuse any fresh-publish under 72 hours"
|
|
709
709
|
],
|
|
710
710
|
"framework_control_gaps": {
|
|
711
|
-
"SLSA-L3": "FIRST documented npm package shipping valid SLSA provenance while being malicious — provenance only proves WHICH pipeline built the artifact, not that the pipeline BEHAVED AS INTENDED. SLSA L3 build integrity is necessary but insufficient against cache-poisoning attacks within the build.",
|
|
711
|
+
"SLSA-v1.0-Build-L3": "FIRST documented npm package shipping valid SLSA provenance while being malicious — provenance only proves WHICH pipeline built the artifact, not that the pipeline BEHAVED AS INTENDED. SLSA L3 build integrity is necessary but insufficient against cache-poisoning attacks within the build.",
|
|
712
712
|
"NIST-800-53-SA-12": "Supply chain protection treats provenance + signing as the trust anchor. CVE-2026-45321 demonstrates both can be intact on a malicious package.",
|
|
713
713
|
"NIST-800-218-SSDF": "PS.3 + PO.3 don't address cache poisoning between sibling workflows in the same repo. SSDF presumes per-workflow trust isolation that GitHub Actions' shared actions/cache breaks.",
|
|
714
714
|
"EU-CRA-Art13": "Required vulnerability handling doesn't cover the case where the upstream maintainer is unwitting — the maintainer was a victim, not a participant.",
|
|
715
|
-
"NIS2-Art21-
|
|
715
|
+
"NIS2-Art21-patch-management": "Supply chain risk management presumes detectable signal at consumption. Valid provenance neutralizes the standard consumer-side check.",
|
|
716
716
|
"DORA-Art28": "ICT third-party risk doesn't cover transitive cache poisoning in upstream CI/CD."
|
|
717
717
|
},
|
|
718
718
|
"atlas_refs": [
|
|
@@ -875,11 +875,11 @@
|
|
|
875
875
|
"GHCR :latest re-points to clean image; rebuild any image FROM elementary-data:0.23.3"
|
|
876
876
|
],
|
|
877
877
|
"framework_control_gaps": {
|
|
878
|
-
"SLSA-L3": "Same shape as CVE-2026-45321 — provenance valid, payload malicious. The publishing pipeline ran on a malicious orphan commit and emitted a legitimate signed release. SLSA-L3 attests WHICH pipeline built the artifact, not that the pipeline was driven by trusted inputs.",
|
|
878
|
+
"SLSA-v1.0-Build-L3": "Same shape as CVE-2026-45321 — provenance valid, payload malicious. The publishing pipeline ran on a malicious orphan commit and emitted a legitimate signed release. SLSA-L3 attests WHICH pipeline built the artifact, not that the pipeline was driven by trusted inputs.",
|
|
879
879
|
"NIST-800-53-SA-12": "Supply chain protection treats signed release as the trust anchor. The signature was valid; the input to the signing pipeline was attacker-controlled.",
|
|
880
|
-
"NIST-800-218-
|
|
880
|
+
"NIST-800-218-SSDF": "Define and use secure development security checks. Direct interpolation of github.event.* into run: scripts is a documented secure-development anti-pattern (GitHub Actions docs explicitly warn against it) but is not framework-enforced.",
|
|
881
881
|
"EU-CRA-Art13": "Required vulnerability handling doesn't address the case where the maintainer was an unwitting publisher.",
|
|
882
|
-
"NIS2-Art21-
|
|
882
|
+
"NIS2-Art21-patch-management": "Supply chain risk management presumes detectable signal at consumption. Valid signature neutralizes consumer-side checks."
|
|
883
883
|
},
|
|
884
884
|
"atlas_refs": [
|
|
885
885
|
"AML.T0010",
|
|
@@ -1026,8 +1026,8 @@
|
|
|
1026
1026
|
],
|
|
1027
1027
|
"framework_control_gaps": {
|
|
1028
1028
|
"NIST-800-53-SI-10": "Input validation control doesn't address argument-vs-statement distinction in SQL libraries. SI-10 is satisfied by 'we validate inputs' regardless of whether the validation runs before the SQL parameter binding.",
|
|
1029
|
-
"OWASP-LLM01": "Prompt injection control set doesn't address the AI-PROXY backend SQL surface — LiteLLM is the substrate that gates LLM API access, not the LLM itself.",
|
|
1030
|
-
"NIS2-Art21-
|
|
1029
|
+
"OWASP-LLM-Top-10-2025-LLM01": "Prompt injection control set doesn't address the AI-PROXY backend SQL surface — LiteLLM is the substrate that gates LLM API access, not the LLM itself.",
|
|
1030
|
+
"NIS2-Art21-incident-handling": "Cryptographic measures control doesn't address application-layer SQL injection.",
|
|
1031
1031
|
"EU-AI-Act-Art-15": "Robustness + cybersecurity requirement is undefined operationally for AI gateway infrastructure."
|
|
1032
1032
|
},
|
|
1033
1033
|
"atlas_refs": [
|
|
@@ -1126,8 +1126,8 @@
|
|
|
1126
1126
|
],
|
|
1127
1127
|
"framework_control_gaps": {
|
|
1128
1128
|
"NIST-800-53-SI-10": "Input validation control doesn't address the argv-vs-string boundary that argument injection exploits — many MCP servers concatenate user input into shell commands without registering this as a code-review failure.",
|
|
1129
|
-
"OWASP-LLM01": "Prompt-injection-as-access-control gap — the attacker doesn't compromise the MCP server directly; they feed adversarial input that the AI passes through.",
|
|
1130
|
-
"NIS2-Art21-
|
|
1129
|
+
"OWASP-LLM-Top-10-2025-LLM01": "Prompt-injection-as-access-control gap — the attacker doesn't compromise the MCP server directly; they feed adversarial input that the AI passes through.",
|
|
1130
|
+
"NIS2-Art21-patch-management": "Patch management presumes traditional CVE timelines; MCP plugin ecosystem patch awareness lags."
|
|
1131
1131
|
},
|
|
1132
1132
|
"atlas_refs": [
|
|
1133
1133
|
"AML.T0053",
|
package/data/cwe-catalog.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"skills_referencing": [
|
|
116
116
|
"exploit-scoring"
|
|
117
117
|
],
|
|
118
|
-
"evidence_cves": [],
|
|
118
|
+
"evidence_cves": ["CVE-2026-42208"],
|
|
119
119
|
"framework_controls_partially_addressing": [
|
|
120
120
|
"NIST-800-53-SI-10",
|
|
121
121
|
"ISO-27001-2022-A.8.28",
|
|
@@ -247,7 +247,8 @@
|
|
|
247
247
|
],
|
|
248
248
|
"evidence_cves": [
|
|
249
249
|
"CVE-2025-53773",
|
|
250
|
-
"CVE-2026-30615"
|
|
250
|
+
"CVE-2026-30615",
|
|
251
|
+
"MAL-2026-3083"
|
|
251
252
|
],
|
|
252
253
|
"framework_controls_partially_addressing": [
|
|
253
254
|
"NIST-800-53-SI-10",
|
|
@@ -392,7 +393,7 @@
|
|
|
392
393
|
"mcp-agent-trust",
|
|
393
394
|
"ai-attack-surface"
|
|
394
395
|
],
|
|
395
|
-
"evidence_cves": [],
|
|
396
|
+
"evidence_cves": ["MAL-2026-3083"],
|
|
396
397
|
"framework_controls_partially_addressing": [
|
|
397
398
|
"NIST-800-53-SI-10",
|
|
398
399
|
"ISO-27001-2022-A.8.28"
|
|
@@ -1494,5 +1494,57 @@
|
|
|
1494
1494
|
"AML.T0048"
|
|
1495
1495
|
],
|
|
1496
1496
|
"attack_refs": []
|
|
1497
|
+
},
|
|
1498
|
+
"EU-CRA-Art13": {
|
|
1499
|
+
"framework": "EU Cyber Resilience Act (2024/2847)",
|
|
1500
|
+
"control_id": "Art. 13",
|
|
1501
|
+
"control_name": "Essential cybersecurity requirements + technical documentation",
|
|
1502
|
+
"designed_for": "Manufacturers placing products with digital elements on the EU market; sets the essential cybersecurity requirements (Annex I) and the technical-documentation duty",
|
|
1503
|
+
"misses": [
|
|
1504
|
+
"Vulnerability handling clauses presume the maintainer is aware of the vulnerability and able to remediate. The elementary-data PyPI worm (MAL-2026-3083) compromised the publishing pipeline — the maintainer was a victim, not a participant — and the published release carried a valid signature.",
|
|
1505
|
+
"'Technical documentation' obligations do not require the manufacturer to retain or publish the build-pipeline configuration that produced each release. Operators consuming a malicious release have no way to inspect the workflow that built it.",
|
|
1506
|
+
"Art. 14 (24-hour notification of actively-exploited vulnerabilities) clock starts from manufacturer awareness; supply-chain-victim manufacturers may not know they are exploited until consumer-side detection (StepSecurity / Snyk / OSV) surfaces the IoCs."
|
|
1507
|
+
],
|
|
1508
|
+
"real_requirement": "Manufacturer publishes the canonical build-pipeline definition alongside each release (workflow file hash, runner attestation, scope of secrets accessed). Operators verify the published pipeline matches the pipeline that produced the release-being-installed. Notification clock starts from FIRST awareness — manufacturer's OR competent-authority's OR widely-published security researcher's.",
|
|
1509
|
+
"status": "open",
|
|
1510
|
+
"opened_date": "2026-05-13",
|
|
1511
|
+
"evidence_cves": [
|
|
1512
|
+
"MAL-2026-3083",
|
|
1513
|
+
"CVE-2025-53773"
|
|
1514
|
+
],
|
|
1515
|
+
"atlas_refs": [
|
|
1516
|
+
"AML.T0010",
|
|
1517
|
+
"AML.T0055"
|
|
1518
|
+
],
|
|
1519
|
+
"attack_refs": [
|
|
1520
|
+
"T1195.001",
|
|
1521
|
+
"T1195.002"
|
|
1522
|
+
]
|
|
1523
|
+
},
|
|
1524
|
+
"NIST-800-53-SI-10": {
|
|
1525
|
+
"framework": "NIST SP 800-53 Rev 5",
|
|
1526
|
+
"control_id": "SI-10",
|
|
1527
|
+
"control_name": "Information Input Validation",
|
|
1528
|
+
"designed_for": "Validating untrusted input at system boundaries before consumption by downstream code paths",
|
|
1529
|
+
"misses": [
|
|
1530
|
+
"Treats 'input validation' as a single layer at the trust boundary. Modern injection classes (SQL, argument, command, prompt) live INSIDE the trust boundary — the input is already 'validated' as authentic but the consumer concatenates it into a syntax the original validator did not anticipate (SQL query, kubectl argv, shell command).",
|
|
1531
|
+
"Does not distinguish argv-array vs string-form invocation. CVE-2026-39884 (mcp-server-kubernetes argument injection) and the broader CWE-88 class are invisible to a SI-10-compliant codebase that 'validates' the user-input string for length and character class.",
|
|
1532
|
+
"Does not address parameterised-query vs string-concat distinction. CVE-2026-42208 (LiteLLM SQLi on CISA KEV) is the cardinal recent example — input was validated, then concatenated into SQL during error-handling, which the validator did not gate.",
|
|
1533
|
+
"Auditing for SI-10 typically samples function boundaries; the argument-injection / SQL-injection / prompt-injection failure modes all occur inside the boundary."
|
|
1534
|
+
],
|
|
1535
|
+
"real_requirement": "Per-injection-class structural controls in addition to boundary validation. Parameterised queries enforced at the ORM/driver level (CWE-89). Argv-array form for spawned subprocesses (CWE-88). Tool-arg / function-call sanitisation in MCP / AI-agent surfaces (CWE-94). Lint rules flagging string-concat into SQL, exec, or AI-tool arguments. SI-10 compliance attestation augmented with a per-class checklist that names the specific structural control.",
|
|
1536
|
+
"status": "open",
|
|
1537
|
+
"opened_date": "2026-05-13",
|
|
1538
|
+
"evidence_cves": [
|
|
1539
|
+
"CVE-2026-42208",
|
|
1540
|
+
"CVE-2026-39884"
|
|
1541
|
+
],
|
|
1542
|
+
"atlas_refs": [
|
|
1543
|
+
"AML.T0053"
|
|
1544
|
+
],
|
|
1545
|
+
"attack_refs": [
|
|
1546
|
+
"T1190",
|
|
1547
|
+
"T1059"
|
|
1548
|
+
]
|
|
1497
1549
|
}
|
|
1498
1550
|
}
|
|
@@ -760,10 +760,10 @@
|
|
|
760
760
|
{
|
|
761
761
|
"id": "gha-workflow-script-injection-sink",
|
|
762
762
|
"type": "file_path",
|
|
763
|
-
"value": "Within the release-workflows artifact (any file under .github/workflows/*.yml): a `run:` shell
|
|
763
|
+
"value": "Within the release-workflows artifact (any file under .github/workflows/*.yml): a `run:` shell — block-scalar (`run: |`) OR single-line (`run: <command>`) — interpolates an attacker-controllable github.event field — ${{ github.event.comment.body }}, ${{ github.event.issue.body }}, ${{ github.event.issue.title }}, ${{ github.event.pull_request.body }}, ${{ github.event.pull_request.title }}, ${{ github.event.review.body }}, ${{ github.event.head_commit.message }}, ${{ github.head_ref }}, ${{ github.event.discussion.body }}, ${{ github.event.discussion.title }} — without first capturing the value into an env: variable. Grep regex (multi-line YAML aware, matches both block-scalar and single-line run: shapes): `run:[\\s\\S]*?\\$\\{\\{\\s*github\\.(event\\.(comment|issue|pull_request|review|head_commit|discussion)\\.|head_ref)`. Corroborate via the branch-tag-protection artifact: if any workflow with this sink also triggers on `pull_request_target` / `issue_comment` / `pull_request_review_comment` AND its job has `permissions: contents: write` (or unrestricted GITHUB_TOKEN), the sink is exploitable by any GitHub user who can comment on the repo.",
|
|
764
764
|
"description": "GitHub Actions script-injection sink. Elementary-data 0.23.3 (April 2026) was forged via this exact pattern — `${{ github.event.comment.body }}` interpolated into a `run:` block in update_pylon_issue.yml, escalated via the workflow's GITHUB_TOKEN to publish a malicious release. Without this indicator, a publisher account compromise via attacker-controlled comments looks identical to a clean release at the consumer side.",
|
|
765
|
-
"confidence": "
|
|
766
|
-
"deterministic":
|
|
765
|
+
"confidence": "high",
|
|
766
|
+
"deterministic": false,
|
|
767
767
|
"false_positive_checks_required": [
|
|
768
768
|
"If the run: block reads the github.event field via an `env:` variable first (env: COMMENT_BODY: ${{ github.event.comment.body }}) and then references $COMMENT_BODY in the shell — that is the documented-safe pattern; demote to miss.",
|
|
769
769
|
"If the workflow only runs in a sandboxed `pull_request` event (not `pull_request_target`) AND has default `permissions: contents: read` AND does not use secrets.* — the sink is not exploitable; demote to miss."
|
package/lib/refresh-external.js
CHANGED
|
@@ -125,10 +125,14 @@ Modes:
|
|
|
125
125
|
--swarm fan out sources across worker threads. Best with --from-cache.
|
|
126
126
|
--advisory <id> (v0.12.0) seed a single catalog entry from an advisory ID.
|
|
127
127
|
CVE-* and GHSA-* route through the GitHub Advisory
|
|
128
|
-
Database.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
Database. When GHSA returns 404 for a CVE-* id
|
|
129
|
+
(CNAs / OSV mirrors operate on different cadences) the
|
|
130
|
+
dispatcher falls back to OSV.dev's /v1/vulns/{id}
|
|
131
|
+
before failing (v0.12.11). MAL-*, SNYK-*, RUSTSEC-*,
|
|
132
|
+
USN-*, UVI-*, GO-*, MGASA-*, PYSEC-*, and other
|
|
133
|
+
OSV-native namespaces route through OSV.dev (v0.12.10).
|
|
134
|
+
Writes a DRAFT to data/cve-catalog.json marked with
|
|
135
|
+
_auto_imported: true.
|
|
132
136
|
Editorial fields (framework_control_gaps, iocs,
|
|
133
137
|
atlas_refs, attack_refs) remain null pending review via:
|
|
134
138
|
exceptd run cve-curation --advisory <id>
|
|
@@ -861,7 +865,27 @@ async function seedSingleAdvisory(opts) {
|
|
|
861
865
|
const sourceName = useOsv ? "osv" : "ghsa";
|
|
862
866
|
const fixtureEnv = useOsv ? "EXCEPTD_OSV_FIXTURE" : "EXCEPTD_GHSA_FIXTURE";
|
|
863
867
|
|
|
864
|
-
|
|
868
|
+
let result = await sourceMod.fetchAdvisoryById(id, {});
|
|
869
|
+
// F4 (v0.12.11): CVE-* identifiers may have an OSV record before GHSA
|
|
870
|
+
// publishes one (CNAs and OSV mirrors operate on different cadences).
|
|
871
|
+
// When GHSA returns 404 specifically, retry through OSV's /v1/vulns/{id}
|
|
872
|
+
// — OSV indexes CVE ids as primary keys. If both 404, surface a combined
|
|
873
|
+
// error message so operators know both sources were tried before failing.
|
|
874
|
+
let fallbackSourceUsed = null;
|
|
875
|
+
if (!result.ok && !useOsv && /^CVE-/i.test(id) && /HTTP 404/.test(result.error || "")) {
|
|
876
|
+
const fallback = await osvMod.fetchAdvisoryById(id, {});
|
|
877
|
+
if (fallback.ok) {
|
|
878
|
+
result = fallback;
|
|
879
|
+
fallbackSourceUsed = "osv";
|
|
880
|
+
} else if (/HTTP 404/.test(fallback.error || "") || /not in fixture/.test(fallback.error || "")) {
|
|
881
|
+
// Both sources tried, both 404 — combine the error message.
|
|
882
|
+
const combined = { ok: false, verb: "refresh", error: `--advisory ${id}: not found in GHSA or OSV (GHSA: ${result.error}; OSV: ${fallback.error})`, source: "offline", routed_to: "ghsa+osv", hint: `Both GHSA and OSV.dev returned 404 for ${id}. Verify the CVE id (CVE-YYYY-NNNN) and that an advisory record exists upstream.` };
|
|
883
|
+
if (opts.json) process.stdout.write(JSON.stringify(combined) + "\n");
|
|
884
|
+
else process.stderr.write(`[refresh --advisory] ${combined.error}\n hint: ${combined.hint}\n`);
|
|
885
|
+
process.exitCode = 2;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
865
889
|
if (!result.ok) {
|
|
866
890
|
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: ${result.error}`, source: result.source, routed_to: sourceName, hint: `Verify the ID format (CVE-YYYY-NNNN, GHSA-*, MAL-*, SNYK-*, RUSTSEC-*, USN-*, etc.) and network reachability. Set ${fixtureEnv} for offline testing.` };
|
|
867
891
|
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
@@ -869,17 +893,21 @@ async function seedSingleAdvisory(opts) {
|
|
|
869
893
|
process.exitCode = 2;
|
|
870
894
|
return;
|
|
871
895
|
}
|
|
896
|
+
// If the OSV fallback fired, normalize/route through the OSV module from
|
|
897
|
+
// here on — the advisory shape is OSV's, not GHSA's.
|
|
898
|
+
const effectiveMod = fallbackSourceUsed === "osv" ? osvMod : sourceMod;
|
|
899
|
+
const effectiveName = fallbackSourceUsed === "osv" ? "osv" : sourceName;
|
|
872
900
|
const advisory = result.advisories[0];
|
|
873
901
|
if (!advisory) {
|
|
874
|
-
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: no matching advisory found`, source: result.source, routed_to:
|
|
902
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: no matching advisory found`, source: result.source, routed_to: effectiveName };
|
|
875
903
|
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
876
904
|
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
877
905
|
process.exitCode = 2;
|
|
878
906
|
return;
|
|
879
907
|
}
|
|
880
|
-
const normalized =
|
|
908
|
+
const normalized = effectiveMod.normalizeAdvisory(advisory);
|
|
881
909
|
if (!normalized) {
|
|
882
|
-
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: advisory could not be normalized (missing required fields)`, routed_to:
|
|
910
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: advisory could not be normalized (missing required fields)`, routed_to: effectiveName, source_id: advisory.ghsa_id || advisory.id || null };
|
|
883
911
|
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
884
912
|
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
885
913
|
process.exitCode = 2;
|
|
@@ -1063,4 +1091,4 @@ if (require.main === module) {
|
|
|
1063
1091
|
});
|
|
1064
1092
|
}
|
|
1065
1093
|
|
|
1066
|
-
module.exports = { ALL_SOURCES, loadCtx, parseArgs };
|
|
1094
|
+
module.exports = { ALL_SOURCES, loadCtx, parseArgs, seedSingleAdvisory };
|
package/lib/source-ghsa.js
CHANGED
|
@@ -119,7 +119,13 @@ async function fetchAdvisoryById(id, opts = {}) {
|
|
|
119
119
|
if (/^CVE-\d{4}-\d+$/i.test(id)) {
|
|
120
120
|
return fetchAdvisories({ ...opts, path: `/advisories?cve_id=${encodeURIComponent(id.toUpperCase())}` });
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
// v0.12.11: widen the error to enumerate OSV-native prefixes — operators
|
|
123
|
+
// running `exceptd refresh --advisory FOO-BAR` previously got an error
|
|
124
|
+
// mentioning only CVE / GHSA, even though MAL-*, SNYK-*, RUSTSEC-*,
|
|
125
|
+
// USN-*, PYSEC-*, GO-*, MGASA-*, UVI- are also valid id shapes routed
|
|
126
|
+
// through source-osv. The hint here mirrors lib/refresh-external.js
|
|
127
|
+
// seedSingleAdvisory's documented acceptance set.
|
|
128
|
+
return { ok: false, error: `unrecognized id format: ${id}. Expected one of: CVE-YYYY-NNNN, GHSA-* (routed through source-ghsa); MAL-* / SNYK-* / RUSTSEC-* / USN-* / PYSEC-* / GO-* / MGASA-* / UVI- (routed through source-osv).`, source: "offline" };
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
/**
|
package/lib/source-osv.js
CHANGED
|
@@ -39,17 +39,24 @@
|
|
|
39
39
|
const https = require("https");
|
|
40
40
|
const fs = require("fs");
|
|
41
41
|
|
|
42
|
+
// OSV_HOST_OVERRIDE lets tests redirect the network call to a local HTTP
|
|
43
|
+
// server bound on 127.0.0.1:<port>. The override accepts either a bare
|
|
44
|
+
// `host:port` string or a full `http://host:port` URL. When set, the
|
|
45
|
+
// underlying request switches from `https` to `http` so the test server
|
|
46
|
+
// doesn't need a TLS cert. Production callers never set this.
|
|
42
47
|
const OSV_HOST = "api.osv.dev";
|
|
43
48
|
const REQUEST_TIMEOUT_MS = 10000;
|
|
44
49
|
const USER_AGENT = "exceptd-security/source-osv (+https://exceptd.com)";
|
|
45
50
|
|
|
46
|
-
// Identifier namespaces OSV uses as PRIMARY keys
|
|
47
|
-
// this
|
|
48
|
-
//
|
|
49
|
-
//
|
|
51
|
+
// Identifier namespaces OSV uses as PRIMARY keys. GHSA-* is intentionally
|
|
52
|
+
// NOT in this list — `seedSingleAdvisory` in lib/refresh-external.js routes
|
|
53
|
+
// CVE-* and GHSA-* through `source-ghsa` because GHSA carries richer field
|
|
54
|
+
// coverage (cvss object, vulnerable_version_range string, ghsa_id linkage)
|
|
55
|
+
// than OSV's import of the same advisories. Keep this list in sync with the
|
|
56
|
+
// dispatcher in lib/refresh-external.js — adding a new prefix here is not
|
|
57
|
+
// enough; the dispatcher's --advisory regex must also accept it.
|
|
50
58
|
const OSV_ID_PREFIXES = [
|
|
51
59
|
"MAL-", // OSSF Malicious Packages
|
|
52
|
-
"GHSA-", // GitHub Security Advisories (OSV import)
|
|
53
60
|
"SNYK-", // Snyk
|
|
54
61
|
"RUSTSEC-", // RustSec
|
|
55
62
|
"GO-", // Go vuln DB
|
|
@@ -72,24 +79,47 @@ const OSV_ID_PREFIXES = [
|
|
|
72
79
|
|
|
73
80
|
/**
|
|
74
81
|
* Return true when `id` looks like an OSV-native primary key (i.e. NOT a
|
|
75
|
-
* CVE-* identifier
|
|
76
|
-
*
|
|
82
|
+
* CVE-* identifier and NOT a GHSA-* identifier). Both CVE-* and GHSA-*
|
|
83
|
+
* route through `source-ghsa` for richer field coverage.
|
|
77
84
|
*/
|
|
78
85
|
function isOsvId(id) {
|
|
79
86
|
if (!id || typeof id !== "string") return false;
|
|
80
87
|
const up = id.toUpperCase();
|
|
81
88
|
if (/^CVE-\d{4}-\d+$/.test(up)) return false;
|
|
89
|
+
if (up.startsWith("GHSA-")) return false;
|
|
82
90
|
return OSV_ID_PREFIXES.some((p) => up.startsWith(p));
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
94
|
+
* Resolve the OSV transport target. When OSV_HOST_OVERRIDE is set the
|
|
95
|
+
* request switches to plain HTTP on the override host:port so test
|
|
96
|
+
* harnesses can stand up a local server without TLS. Production omits the
|
|
97
|
+
* override entirely and lands on api.osv.dev over HTTPS.
|
|
87
98
|
*/
|
|
88
|
-
function
|
|
99
|
+
function osvTransport() {
|
|
100
|
+
const override = process.env.OSV_HOST_OVERRIDE;
|
|
101
|
+
if (!override) return { mod: https, host: OSV_HOST, port: 443 };
|
|
102
|
+
// Accept either "host:port" or a full URL.
|
|
103
|
+
let raw = override.trim();
|
|
104
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
105
|
+
const u = new URL(raw);
|
|
106
|
+
return { mod: require("http"), host: u.hostname, port: parseInt(u.port, 10) || 80 };
|
|
107
|
+
}
|
|
108
|
+
const [h, p] = raw.split(":");
|
|
109
|
+
return { mod: require("http"), host: h || "127.0.0.1", port: parseInt(p, 10) || 80 };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Low-level GET against OSV. Resolves to { ok, record|error, source }.
|
|
114
|
+
* Honors OSV_HOST_OVERRIDE for offline tests.
|
|
115
|
+
*/
|
|
116
|
+
function osvGet(reqPath, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
89
117
|
return new Promise((resolve) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
const { mod, host, port } = osvTransport();
|
|
119
|
+
const req = mod.get({
|
|
120
|
+
host,
|
|
121
|
+
port,
|
|
122
|
+
path: reqPath,
|
|
93
123
|
headers: {
|
|
94
124
|
"Accept": "application/json",
|
|
95
125
|
"User-Agent": USER_AGENT,
|
|
@@ -98,7 +128,11 @@ function osvGet(path, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
98
128
|
}, (res) => {
|
|
99
129
|
if (res.statusCode !== 200) {
|
|
100
130
|
res.resume();
|
|
101
|
-
|
|
131
|
+
const status = res.statusCode;
|
|
132
|
+
const error = status === 429
|
|
133
|
+
? `OSV rate-limited (HTTP 429)`
|
|
134
|
+
: `OSV returned HTTP ${status}`;
|
|
135
|
+
return resolve({ ok: false, error, status, source: "offline" });
|
|
102
136
|
}
|
|
103
137
|
const chunks = [];
|
|
104
138
|
res.on("data", (c) => chunks.push(c));
|
|
@@ -111,20 +145,22 @@ function osvGet(path, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
111
145
|
}
|
|
112
146
|
});
|
|
113
147
|
});
|
|
114
|
-
req.on("timeout", () => req.destroy(new Error("
|
|
148
|
+
req.on("timeout", () => req.destroy(new Error("OSV request timed out")));
|
|
115
149
|
req.on("error", (e) => resolve({ ok: false, error: e.message, source: "offline" }));
|
|
116
150
|
});
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
/**
|
|
120
|
-
* Low-level
|
|
154
|
+
* Low-level POST against OSV. Body is JSON-stringified.
|
|
121
155
|
*/
|
|
122
|
-
function osvPost(
|
|
156
|
+
function osvPost(reqPath, body, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
123
157
|
return new Promise((resolve) => {
|
|
124
158
|
const payload = Buffer.from(JSON.stringify(body), "utf8");
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
159
|
+
const { mod, host, port } = osvTransport();
|
|
160
|
+
const req = mod.request({
|
|
161
|
+
host,
|
|
162
|
+
port,
|
|
163
|
+
path: reqPath,
|
|
128
164
|
method: "POST",
|
|
129
165
|
headers: {
|
|
130
166
|
"Content-Type": "application/json",
|
|
@@ -136,7 +172,11 @@ function osvPost(path, body, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
136
172
|
}, (res) => {
|
|
137
173
|
if (res.statusCode !== 200) {
|
|
138
174
|
res.resume();
|
|
139
|
-
|
|
175
|
+
const status = res.statusCode;
|
|
176
|
+
const error = status === 429
|
|
177
|
+
? `OSV rate-limited (HTTP 429)`
|
|
178
|
+
: `OSV returned HTTP ${status}`;
|
|
179
|
+
return resolve({ ok: false, error, status, source: "offline" });
|
|
140
180
|
}
|
|
141
181
|
const chunks = [];
|
|
142
182
|
res.on("data", (c) => chunks.push(c));
|
|
@@ -149,7 +189,7 @@ function osvPost(path, body, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
149
189
|
}
|
|
150
190
|
});
|
|
151
191
|
});
|
|
152
|
-
req.on("timeout", () => req.destroy(new Error("
|
|
192
|
+
req.on("timeout", () => req.destroy(new Error("OSV request timed out")));
|
|
153
193
|
req.on("error", (e) => resolve({ ok: false, error: e.message, source: "offline" }));
|
|
154
194
|
req.write(payload);
|
|
155
195
|
req.end();
|
|
@@ -157,14 +197,36 @@ function osvPost(path, body, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
157
197
|
}
|
|
158
198
|
|
|
159
199
|
/**
|
|
160
|
-
* Read EXCEPTD_OSV_FIXTURE and return
|
|
161
|
-
*
|
|
200
|
+
* Read EXCEPTD_OSV_FIXTURE and return a structured envelope. Matches the
|
|
201
|
+
* GHSA-source convention: on any failure (missing file, malformed JSON,
|
|
202
|
+
* root not object/array) return `{ ok: false, error, source: "offline" }`
|
|
203
|
+
* rather than throw — operators on the CLI surface get a structured error
|
|
204
|
+
* instead of a Node stack trace.
|
|
205
|
+
*
|
|
206
|
+
* Returns:
|
|
207
|
+
* null when env var is unset
|
|
208
|
+
* { ok: true, advisories: [...], source } on success
|
|
209
|
+
* { ok: false, error, source: "offline" } on any failure
|
|
162
210
|
*/
|
|
163
211
|
function readFixture() {
|
|
164
212
|
const fp = process.env.EXCEPTD_OSV_FIXTURE;
|
|
165
213
|
if (!fp) return null;
|
|
166
|
-
|
|
167
|
-
|
|
214
|
+
let raw;
|
|
215
|
+
try {
|
|
216
|
+
raw = fs.readFileSync(fp, "utf8");
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { ok: false, error: `fixture: ${e.message}`, source: "offline" };
|
|
219
|
+
}
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(raw);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
return { ok: false, error: `fixture: ${e.message}`, source: "offline" };
|
|
225
|
+
}
|
|
226
|
+
if (parsed == null || (typeof parsed !== "object")) {
|
|
227
|
+
return { ok: false, error: `fixture: root must be an OSV record object or array (got ${typeof parsed})`, source: "offline" };
|
|
228
|
+
}
|
|
229
|
+
return { ok: true, advisories: Array.isArray(parsed) ? parsed : [parsed], source: "fixture" };
|
|
168
230
|
}
|
|
169
231
|
|
|
170
232
|
/**
|
|
@@ -176,12 +238,19 @@ function readFixture() {
|
|
|
176
238
|
*/
|
|
177
239
|
async function fetchAdvisoryById(id, opts = {}) {
|
|
178
240
|
if (!id || typeof id !== "string") {
|
|
179
|
-
return { ok: false, error: "id is required (MAL-*,
|
|
241
|
+
return { ok: false, error: "id is required (MAL-*, SNYK-*, RUSTSEC-*, etc.)", source: "offline" };
|
|
180
242
|
}
|
|
243
|
+
// OSV.dev's /v1/vulns/{id} is case-sensitive — `mal-2026-3083` 404s while
|
|
244
|
+
// `MAL-2026-3083` resolves. Uppercase at entry so operators piping
|
|
245
|
+
// lowercase ids from grep/jq don't get a surprising 404 from the network
|
|
246
|
+
// path. Fixture lookup already case-folds, so this normalization is a
|
|
247
|
+
// no-op there but harmless.
|
|
248
|
+
id = id.toUpperCase();
|
|
181
249
|
const fixture = readFixture();
|
|
182
250
|
if (fixture) {
|
|
183
|
-
|
|
184
|
-
const
|
|
251
|
+
if (!fixture.ok) return fixture; // F1: structured error envelope
|
|
252
|
+
const want = id;
|
|
253
|
+
const match = fixture.advisories.find((rec) => {
|
|
185
254
|
const recId = (rec && rec.id) ? String(rec.id).toUpperCase() : null;
|
|
186
255
|
if (recId === want) return true;
|
|
187
256
|
const aliases = Array.isArray(rec?.aliases) ? rec.aliases.map((a) => String(a).toUpperCase()) : [];
|
|
@@ -205,9 +274,10 @@ async function fetchAdvisoriesForPackage(name, ecosystem, version, opts = {}) {
|
|
|
205
274
|
}
|
|
206
275
|
const fixture = readFixture();
|
|
207
276
|
if (fixture) {
|
|
277
|
+
if (!fixture.ok) return fixture; // F1: structured error envelope
|
|
208
278
|
// Best-effort fixture filtering: match any record whose `affected[]`
|
|
209
279
|
// contains the requested package + ecosystem (+ version when set).
|
|
210
|
-
const matches = fixture.filter((rec) => {
|
|
280
|
+
const matches = fixture.advisories.filter((rec) => {
|
|
211
281
|
const affected = Array.isArray(rec?.affected) ? rec.affected : [];
|
|
212
282
|
return affected.some((a) => {
|
|
213
283
|
const pkg = a?.package || {};
|
|
@@ -241,37 +311,112 @@ function pickCatalogKey(rec) {
|
|
|
241
311
|
}
|
|
242
312
|
|
|
243
313
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* no
|
|
314
|
+
* CVSS 3.1 base-score computation from a vector string. Implements Table 6
|
|
315
|
+
* of the FIRST CVSS 3.1 specification. Used when an OSV record carries a
|
|
316
|
+
* vector but no embedded numeric score (the common case for MAL-* records).
|
|
317
|
+
* Returns null on malformed input.
|
|
318
|
+
*
|
|
319
|
+
* Reference: https://www.first.org/cvss/v3.1/specification-document
|
|
320
|
+
*/
|
|
321
|
+
function cvss3BaseScore(vector) {
|
|
322
|
+
if (typeof vector !== "string") return null;
|
|
323
|
+
const m = vector.match(/^CVSS:3\.\d\/(.+)$/);
|
|
324
|
+
if (!m) return null;
|
|
325
|
+
const parts = m[1].split("/");
|
|
326
|
+
const metrics = {};
|
|
327
|
+
for (const p of parts) {
|
|
328
|
+
const [k, v] = p.split(":");
|
|
329
|
+
if (!k || !v) return null;
|
|
330
|
+
metrics[k] = v;
|
|
331
|
+
}
|
|
332
|
+
// Required metrics — bail if any are missing.
|
|
333
|
+
for (const k of ["AV", "AC", "PR", "UI", "S", "C", "I", "A"]) {
|
|
334
|
+
if (!metrics[k]) return null;
|
|
335
|
+
}
|
|
336
|
+
const AV_W = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
|
|
337
|
+
const AC_W = { L: 0.77, H: 0.44 };
|
|
338
|
+
const UI_W = { N: 0.85, R: 0.62 };
|
|
339
|
+
const CIA_W = { H: 0.56, L: 0.22, N: 0 };
|
|
340
|
+
// PR weights depend on Scope.
|
|
341
|
+
const PR_W_U = { N: 0.85, L: 0.62, H: 0.27 };
|
|
342
|
+
const PR_W_C = { N: 0.85, L: 0.68, H: 0.5 };
|
|
343
|
+
const scope = metrics.S;
|
|
344
|
+
if (scope !== "U" && scope !== "C") return null;
|
|
345
|
+
const av = AV_W[metrics.AV];
|
|
346
|
+
const ac = AC_W[metrics.AC];
|
|
347
|
+
const ui = UI_W[metrics.UI];
|
|
348
|
+
const pr = (scope === "C" ? PR_W_C : PR_W_U)[metrics.PR];
|
|
349
|
+
const c = CIA_W[metrics.C];
|
|
350
|
+
const i = CIA_W[metrics.I];
|
|
351
|
+
const a = CIA_W[metrics.A];
|
|
352
|
+
if ([av, ac, ui, pr, c, i, a].some((x) => x == null)) return null;
|
|
353
|
+
const iss = 1 - ((1 - c) * (1 - i) * (1 - a));
|
|
354
|
+
let impact;
|
|
355
|
+
if (scope === "U") {
|
|
356
|
+
impact = 6.42 * iss;
|
|
357
|
+
} else {
|
|
358
|
+
impact = 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15);
|
|
359
|
+
}
|
|
360
|
+
if (impact <= 0) return 0.0;
|
|
361
|
+
const exploitability = 8.22 * av * ac * pr * ui;
|
|
362
|
+
let base;
|
|
363
|
+
if (scope === "U") {
|
|
364
|
+
base = Math.min(impact + exploitability, 10);
|
|
365
|
+
} else {
|
|
366
|
+
base = Math.min(1.08 * (impact + exploitability), 10);
|
|
367
|
+
}
|
|
368
|
+
// roundUp1: round up to one decimal (CVSS 3.1 §7.1).
|
|
369
|
+
const rounded = Math.ceil(base * 10) / 10;
|
|
370
|
+
if (!Number.isFinite(rounded) || rounded < 0 || rounded > 10) return null;
|
|
371
|
+
return rounded;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Pull a numeric CVSS score + vector out of an OSV severity[] entry. CVSS
|
|
376
|
+
* vectors start with "CVSS:3.x/" or "CVSS:4.0/". When multiple vectors are
|
|
377
|
+
* present (e.g. both V3 and V4), the highest version wins regardless of
|
|
378
|
+
* array order. When the OSV record has no embedded numeric tail, the score
|
|
379
|
+
* is computed from the vector itself via cvss3BaseScore(). Returns null
|
|
380
|
+
* components when nothing parseable is present.
|
|
247
381
|
*/
|
|
248
382
|
function extractCvss(rec) {
|
|
249
383
|
const sev = Array.isArray(rec?.severity) ? rec.severity : [];
|
|
250
384
|
let score = null;
|
|
251
|
-
let
|
|
385
|
+
let bestVector = null;
|
|
386
|
+
let bestVersion = 0;
|
|
252
387
|
for (const s of sev) {
|
|
253
388
|
if (typeof s?.score !== "string") continue;
|
|
254
389
|
const v = s.score.trim();
|
|
255
|
-
// Bare numeric score
|
|
390
|
+
// Bare numeric score (no vector prefix).
|
|
256
391
|
const num = parseFloat(v);
|
|
257
392
|
if (!Number.isNaN(num) && num >= 0 && num <= 10 && !v.includes("/")) {
|
|
258
393
|
if (score == null) score = num;
|
|
259
394
|
continue;
|
|
260
395
|
}
|
|
261
|
-
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const m = v.match(/\/(\d+(?:\.\d+)?)$/);
|
|
268
|
-
if (m && score == null) {
|
|
269
|
-
const candidate = parseFloat(m[1]);
|
|
270
|
-
if (candidate >= 0 && candidate <= 10) score = candidate;
|
|
271
|
-
}
|
|
396
|
+
const m = v.match(/^CVSS:(\d+\.\d+)/);
|
|
397
|
+
if (!m) continue;
|
|
398
|
+
const ver = parseFloat(m[1]);
|
|
399
|
+
if (ver > bestVersion) {
|
|
400
|
+
bestVersion = ver;
|
|
401
|
+
bestVector = v;
|
|
272
402
|
}
|
|
273
403
|
}
|
|
274
|
-
|
|
404
|
+
// If we picked a vector, try to read an embedded score from the trailing
|
|
405
|
+
// fragment (some Snyk records carry it as ".../9.3"). Otherwise compute
|
|
406
|
+
// it from the vector for CVSS 3.x. CVSS 4.0 base-score derivation is
|
|
407
|
+
// intentionally not implemented here — that's a v0.13 follow-up.
|
|
408
|
+
if (bestVector && score == null) {
|
|
409
|
+
const tail = bestVector.match(/\/(\d+(?:\.\d+)?)$/);
|
|
410
|
+
if (tail) {
|
|
411
|
+
const candidate = parseFloat(tail[1]);
|
|
412
|
+
if (candidate >= 0 && candidate <= 10) score = candidate;
|
|
413
|
+
}
|
|
414
|
+
if (score == null && /^CVSS:3\./.test(bestVector)) {
|
|
415
|
+
const computed = cvss3BaseScore(bestVector);
|
|
416
|
+
if (computed != null) score = computed;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { score, vector: bestVector };
|
|
275
420
|
}
|
|
276
421
|
|
|
277
422
|
/**
|
|
@@ -373,6 +518,23 @@ function normalizeAdvisory(rec) {
|
|
|
373
518
|
// OSV.dev canonical advisory URL — used as the primary vendor advisory.
|
|
374
519
|
const osvUrl = `https://osv.dev/vulnerability/${encodeURIComponent(rec.id)}`;
|
|
375
520
|
|
|
521
|
+
// F6: dedupe verification_sources. OSV records frequently carry the
|
|
522
|
+
// canonical osv.dev URL in references[] as well, which would otherwise
|
|
523
|
+
// produce a duplicate alongside the prepended `osvUrl`.
|
|
524
|
+
const verification_sources = Array.from(new Set([
|
|
525
|
+
osvUrl,
|
|
526
|
+
...(/^CVE-/i.test(catalogKey) ? [`https://nvd.nist.gov/vuln/detail/${catalogKey}`] : []),
|
|
527
|
+
...refUrls.slice(0, 10),
|
|
528
|
+
]));
|
|
529
|
+
|
|
530
|
+
// F5: EPSS coverage does not extend to non-CVE identifiers. Surface this
|
|
531
|
+
// explicitly so curators know to re-query if MITRE later assigns a CVE
|
|
532
|
+
// id to the entry. Wording mirrors the MAL-2026-3083 catalog entry.
|
|
533
|
+
const isCveKey = /^CVE-/i.test(catalogKey);
|
|
534
|
+
const epss_note = isCveKey
|
|
535
|
+
? null
|
|
536
|
+
: "EPSS coverage does not extend to non-CVE identifiers. FIRST EPSS API only indexes CVE keys; MAL-* / SNYK-* / GHSA-* / RUSTSEC-* / etc. return no data. Re-query and populate epss_score when MITRE assigns a CVE id and the entry is renamed.";
|
|
537
|
+
|
|
376
538
|
return {
|
|
377
539
|
[catalogKey]: {
|
|
378
540
|
name: rec.summary || rec.id,
|
|
@@ -407,15 +569,12 @@ function normalizeAdvisory(rec) {
|
|
|
407
569
|
epss_score: null,
|
|
408
570
|
epss_percentile: null,
|
|
409
571
|
epss_date: null,
|
|
410
|
-
|
|
572
|
+
epss_note,
|
|
573
|
+
epss_source: isCveKey
|
|
411
574
|
? `https://api.first.org/data/v1/epss?cve=${catalogKey}`
|
|
412
575
|
: null,
|
|
413
576
|
source_verified: published || today,
|
|
414
|
-
verification_sources
|
|
415
|
-
osvUrl,
|
|
416
|
-
...(/^CVE-/i.test(catalogKey) ? [`https://nvd.nist.gov/vuln/detail/${catalogKey}`] : []),
|
|
417
|
-
...refUrls.slice(0, 10),
|
|
418
|
-
],
|
|
577
|
+
verification_sources,
|
|
419
578
|
vendor_advisories: [
|
|
420
579
|
{
|
|
421
580
|
vendor: "OSV.dev",
|
|
@@ -451,19 +610,26 @@ async function buildDiff(ctx) {
|
|
|
451
610
|
status: "ok",
|
|
452
611
|
diffs: [],
|
|
453
612
|
errors: 0,
|
|
613
|
+
unreachable_count: 0,
|
|
614
|
+
normalize_error_count: 0,
|
|
454
615
|
summary: "OSV: no ids requested (set ctx.osv_ids to seed a draft, or pass --advisory <MAL-...> for one-shot import).",
|
|
455
616
|
};
|
|
456
617
|
}
|
|
457
618
|
const existingKeys = new Set(Object.keys(ctx.cveCatalog || {}));
|
|
458
619
|
const diffs = [];
|
|
459
|
-
|
|
620
|
+
// F7: distinguish unreachable (fetch failed, network or 5xx) from
|
|
621
|
+
// normalize-rejected (record fetched but normalization produced null).
|
|
622
|
+
// Operators triaging a refresh-report want to know whether to chase a
|
|
623
|
+
// network outage or a malformed upstream record.
|
|
624
|
+
let unreachable = 0;
|
|
625
|
+
let normalizeErrors = 0;
|
|
460
626
|
for (const id of ids) {
|
|
461
627
|
const r = await fetchAdvisoryById(id);
|
|
462
|
-
if (!r.ok) {
|
|
628
|
+
if (!r.ok) { unreachable++; continue; }
|
|
463
629
|
const rec = r.advisories[0];
|
|
464
|
-
if (!rec) {
|
|
630
|
+
if (!rec) { unreachable++; continue; }
|
|
465
631
|
const normalized = normalizeAdvisory(rec);
|
|
466
|
-
if (!normalized) {
|
|
632
|
+
if (!normalized) { normalizeErrors++; continue; }
|
|
467
633
|
const key = Object.keys(normalized)[0];
|
|
468
634
|
if (existingKeys.has(key)) continue;
|
|
469
635
|
diffs.push({
|
|
@@ -475,11 +641,14 @@ async function buildDiff(ctx) {
|
|
|
475
641
|
source: "osv",
|
|
476
642
|
});
|
|
477
643
|
}
|
|
644
|
+
const errors = unreachable + normalizeErrors;
|
|
478
645
|
return {
|
|
479
646
|
status: errors === 0 ? "ok" : errors === ids.length ? "unreachable" : "partial",
|
|
480
647
|
diffs,
|
|
481
648
|
errors,
|
|
482
|
-
|
|
649
|
+
unreachable_count: unreachable,
|
|
650
|
+
normalize_error_count: normalizeErrors,
|
|
651
|
+
summary: `OSV fetched ${ids.length} id(s); ${diffs.length} new entry diff(s), ${unreachable} unreachable, ${normalizeErrors} normalize-rejected.`,
|
|
483
652
|
};
|
|
484
653
|
}
|
|
485
654
|
|
|
@@ -489,5 +658,7 @@ module.exports = {
|
|
|
489
658
|
normalizeAdvisory,
|
|
490
659
|
buildDiff,
|
|
491
660
|
isOsvId,
|
|
661
|
+
extractCvss,
|
|
662
|
+
cvss3BaseScore,
|
|
492
663
|
OSV_ID_PREFIXES,
|
|
493
664
|
};
|
package/manifest-snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
|
|
3
|
-
"_generated_at": "2026-05-
|
|
3
|
+
"_generated_at": "2026-05-13T21:19:34.827Z",
|
|
4
4
|
"atlas_version": "5.1.0",
|
|
5
5
|
"skill_count": 38,
|
|
6
6
|
"skills": [
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exceptd-security",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
4
4
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
|
|
5
5
|
"homepage": "https://exceptd.com",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"last_threat_review": "2026-05-01",
|
|
54
54
|
"signature": "GfdaqLFofMiou8doFBE68J+ll50MU3EfJh6N6mNL6RwjABmHsbyfOXCeEpR3NlhbDrYJaG4hpZg4PwhN+t9QAA==",
|
|
55
|
-
"signed_at": "2026-05-
|
|
55
|
+
"signed_at": "2026-05-13T21:19:34.408Z",
|
|
56
56
|
"cwe_refs": [
|
|
57
57
|
"CWE-125",
|
|
58
58
|
"CWE-362",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
],
|
|
117
117
|
"last_threat_review": "2026-05-01",
|
|
118
118
|
"signature": "m93naZQwujXBedWEjRN+88R1b2q/Dzs595Rz0ufsctsSVL2kiqlorzqqwL4mIXBDUM/HJAMRyFzPQoxOhh5qBw==",
|
|
119
|
-
"signed_at": "2026-05-
|
|
119
|
+
"signed_at": "2026-05-13T21:19:34.410Z",
|
|
120
120
|
"cwe_refs": [
|
|
121
121
|
"CWE-1039",
|
|
122
122
|
"CWE-1426",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
],
|
|
180
180
|
"last_threat_review": "2026-05-01",
|
|
181
181
|
"signature": "gvC+DFTp8TONJiYIq/Uvg/uTCWyQ2vjdU8hhNEjYqTghxwnNbryePTmvcxb1VZDBSv1r+kyE2MpHRBzSdLhxDQ==",
|
|
182
|
-
"signed_at": "2026-05-
|
|
182
|
+
"signed_at": "2026-05-13T21:19:34.411Z",
|
|
183
183
|
"cwe_refs": [
|
|
184
184
|
"CWE-22",
|
|
185
185
|
"CWE-345",
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"framework_gaps": [],
|
|
226
226
|
"last_threat_review": "2026-05-01",
|
|
227
227
|
"signature": "FqTRjHfEgw56pyHnyWzNtnhzDMEePBtmuamtW/iyX+h4yqbvP4Fyr7NRjRs3EgqT4j7oHuEZhV9Jt6ZTBgN4AA==",
|
|
228
|
-
"signed_at": "2026-05-
|
|
228
|
+
"signed_at": "2026-05-13T21:19:34.411Z"
|
|
229
229
|
},
|
|
230
230
|
{
|
|
231
231
|
"name": "compliance-theater",
|
|
@@ -256,7 +256,7 @@
|
|
|
256
256
|
],
|
|
257
257
|
"last_threat_review": "2026-05-01",
|
|
258
258
|
"signature": "69KnW3ol+BZAcIbex0JJo6/71BgBE4S4o9CxZPd5kgzm5kb85PwzE6KfnK40OaE1jz36FUYJgiQZo/T7RiDhAA==",
|
|
259
|
-
"signed_at": "2026-05-
|
|
259
|
+
"signed_at": "2026-05-13T21:19:34.412Z"
|
|
260
260
|
},
|
|
261
261
|
{
|
|
262
262
|
"name": "exploit-scoring",
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
],
|
|
286
286
|
"last_threat_review": "2026-05-01",
|
|
287
287
|
"signature": "k13lnnr4U4H58LOr2rj+ygF80RnVsdpAyGLXAdyLaY5oJFwfmEMMZQVLrcPhDx6cIQgP+Muzgtolhkh577kCCw==",
|
|
288
|
-
"signed_at": "2026-05-
|
|
288
|
+
"signed_at": "2026-05-13T21:19:34.412Z"
|
|
289
289
|
},
|
|
290
290
|
{
|
|
291
291
|
"name": "rag-pipeline-security",
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
],
|
|
323
323
|
"last_threat_review": "2026-05-01",
|
|
324
324
|
"signature": "X4avgw01bNVZjoiYDF+NN9qSOTjaH2I/7nRbPByoTLzMcORO7zTrZtqpGExJiV3Dmn0EtJ2dX07P65MqQIWHCA==",
|
|
325
|
-
"signed_at": "2026-05-
|
|
325
|
+
"signed_at": "2026-05-13T21:19:34.412Z",
|
|
326
326
|
"cwe_refs": [
|
|
327
327
|
"CWE-1395",
|
|
328
328
|
"CWE-1426"
|
|
@@ -379,7 +379,7 @@
|
|
|
379
379
|
],
|
|
380
380
|
"last_threat_review": "2026-05-01",
|
|
381
381
|
"signature": "SQWndklQmECBemQj42XTfdXM9+7iH+LgIb+qanQsUGT6dhQi7RsXlLeMxe74hVvwpDeUKsn22u23J4bWJh+HCw==",
|
|
382
|
-
"signed_at": "2026-05-
|
|
382
|
+
"signed_at": "2026-05-13T21:19:34.412Z",
|
|
383
383
|
"d3fend_refs": [
|
|
384
384
|
"D3-CA",
|
|
385
385
|
"D3-CSPP",
|
|
@@ -414,7 +414,7 @@
|
|
|
414
414
|
"framework_gaps": [],
|
|
415
415
|
"last_threat_review": "2026-05-01",
|
|
416
416
|
"signature": "Dc740zhVm0mtlbj80QfgPcAKFyLZbbDJx/gdNHyYui0XKC3YNxykjcbqKOXnfdLAKkeLwWjMqZefkQk49wueAQ==",
|
|
417
|
-
"signed_at": "2026-05-
|
|
417
|
+
"signed_at": "2026-05-13T21:19:34.413Z",
|
|
418
418
|
"cwe_refs": [
|
|
419
419
|
"CWE-1188"
|
|
420
420
|
]
|
|
@@ -442,7 +442,7 @@
|
|
|
442
442
|
"framework_gaps": [],
|
|
443
443
|
"last_threat_review": "2026-05-01",
|
|
444
444
|
"signature": "3w189PCSFqXTTqlzNc14EKcW7vZ/CtAkuKB2hE4ERdJX+0DNo5AOfrPLjOD1/LdZpPLayEjDTZ83WGtEsMu1Cw==",
|
|
445
|
-
"signed_at": "2026-05-
|
|
445
|
+
"signed_at": "2026-05-13T21:19:34.413Z"
|
|
446
446
|
},
|
|
447
447
|
{
|
|
448
448
|
"name": "global-grc",
|
|
@@ -474,7 +474,7 @@
|
|
|
474
474
|
"framework_gaps": [],
|
|
475
475
|
"last_threat_review": "2026-05-01",
|
|
476
476
|
"signature": "S/YXUpI/mcG2FpdUTgMsccWBtTaR5A4Ph4QFQw31S9w9Hn/z3sOFHLkb1B5YSwlg+mMOtSIxMdet1eLGSZkTDg==",
|
|
477
|
-
"signed_at": "2026-05-
|
|
477
|
+
"signed_at": "2026-05-13T21:19:34.414Z"
|
|
478
478
|
},
|
|
479
479
|
{
|
|
480
480
|
"name": "zeroday-gap-learn",
|
|
@@ -501,7 +501,7 @@
|
|
|
501
501
|
"framework_gaps": [],
|
|
502
502
|
"last_threat_review": "2026-05-01",
|
|
503
503
|
"signature": "84nQaJylnNIZ47BD7dSBryWwntprRc9zqoTb6i5K7jV6cq7bUm6fVlrCTlFGg/oWLMX3x9JHmc9hqZhgdzRMCw==",
|
|
504
|
-
"signed_at": "2026-05-
|
|
504
|
+
"signed_at": "2026-05-13T21:19:34.414Z"
|
|
505
505
|
},
|
|
506
506
|
{
|
|
507
507
|
"name": "pqc-first",
|
|
@@ -553,7 +553,7 @@
|
|
|
553
553
|
],
|
|
554
554
|
"last_threat_review": "2026-05-01",
|
|
555
555
|
"signature": "oEkK5bLS/G5RIHnxlNFJYdzhTJbKZnkJv+W4iS9UJ/uszZHgZGoxygELPc4kn3FowV5eE988SQYG4WKlXtNzCg==",
|
|
556
|
-
"signed_at": "2026-05-
|
|
556
|
+
"signed_at": "2026-05-13T21:19:34.414Z",
|
|
557
557
|
"cwe_refs": [
|
|
558
558
|
"CWE-327"
|
|
559
559
|
],
|
|
@@ -600,7 +600,7 @@
|
|
|
600
600
|
],
|
|
601
601
|
"last_threat_review": "2026-05-01",
|
|
602
602
|
"signature": "JCrqB0GmldDIYPpgC+U3DDzxpYWJa0QgEQF7L1T8kxY0U0bsa7cw87CNC5KGk1VZRNsCa3v/I4XR1E/T5GkpBA==",
|
|
603
|
-
"signed_at": "2026-05-
|
|
603
|
+
"signed_at": "2026-05-13T21:19:34.415Z"
|
|
604
604
|
},
|
|
605
605
|
{
|
|
606
606
|
"name": "security-maturity-tiers",
|
|
@@ -637,7 +637,7 @@
|
|
|
637
637
|
],
|
|
638
638
|
"last_threat_review": "2026-05-01",
|
|
639
639
|
"signature": "ctxS+0nGTJgJ4YroLpckhG+ryjYxfwvisSeGt2o8OF6eiQBlu/VbrOk03Jb+qkahgD0mLnSeBE4sjRwekGL9BQ==",
|
|
640
|
-
"signed_at": "2026-05-
|
|
640
|
+
"signed_at": "2026-05-13T21:19:34.415Z",
|
|
641
641
|
"cwe_refs": [
|
|
642
642
|
"CWE-1188"
|
|
643
643
|
]
|
|
@@ -672,7 +672,7 @@
|
|
|
672
672
|
"framework_gaps": [],
|
|
673
673
|
"last_threat_review": "2026-05-11",
|
|
674
674
|
"signature": "cdmQWTu9bFjeheH/B6pa88zCtPqVeEDDElnC/h7pUQ/JQSh9HuqCSiK/Jv2C4gaLA9h0dmKVe7NEFajshrZZDA==",
|
|
675
|
-
"signed_at": "2026-05-
|
|
675
|
+
"signed_at": "2026-05-13T21:19:34.415Z"
|
|
676
676
|
},
|
|
677
677
|
{
|
|
678
678
|
"name": "attack-surface-pentest",
|
|
@@ -743,7 +743,7 @@
|
|
|
743
743
|
"PTES revision incorporating AI-surface enumeration"
|
|
744
744
|
],
|
|
745
745
|
"signature": "6YqZpHsmUz1/aTyOPNDUgJquKaacOqEqTIELHc5wlaydDz4bYboutEu/YiTYy+wF/nYo2nOyuJfMdR8jsYwEDQ==",
|
|
746
|
-
"signed_at": "2026-05-
|
|
746
|
+
"signed_at": "2026-05-13T21:19:34.416Z"
|
|
747
747
|
},
|
|
748
748
|
{
|
|
749
749
|
"name": "fuzz-testing-strategy",
|
|
@@ -803,7 +803,7 @@
|
|
|
803
803
|
"OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
|
|
804
804
|
],
|
|
805
805
|
"signature": "+ELdD+1AY5DymBitH7wU65CS60NY1nDoLowJAFn7cE5Gr/5jy9BTkyxsm7PEXaSlXWMOkTf/HQ+uyzyxUVD/Bw==",
|
|
806
|
-
"signed_at": "2026-05-
|
|
806
|
+
"signed_at": "2026-05-13T21:19:34.416Z"
|
|
807
807
|
},
|
|
808
808
|
{
|
|
809
809
|
"name": "dlp-gap-analysis",
|
|
@@ -878,7 +878,7 @@
|
|
|
878
878
|
"Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
|
|
879
879
|
],
|
|
880
880
|
"signature": "H1N113M/YhnEPQptJ2B88wwAOB4eMPKuVABQ4DSlMjFMsX0ts1DBCupHHDoHcJHbi8vs+Wi10ekpL5SnsroZDQ==",
|
|
881
|
-
"signed_at": "2026-05-
|
|
881
|
+
"signed_at": "2026-05-13T21:19:34.416Z"
|
|
882
882
|
},
|
|
883
883
|
{
|
|
884
884
|
"name": "supply-chain-integrity",
|
|
@@ -955,7 +955,7 @@
|
|
|
955
955
|
"OpenSSF model-signing — emerging Sigstore-based signing standard for ML model weights; track for production adoption"
|
|
956
956
|
],
|
|
957
957
|
"signature": "TmV9ZBDYqp2pHIbNZKQYf+kZLNAiyagnH/pjK9+LiSd7OdFIN3KEpFHPUNzcivB/CT5q7nl7Tsmvc9UvGo8WDg==",
|
|
958
|
-
"signed_at": "2026-05-
|
|
958
|
+
"signed_at": "2026-05-13T21:19:34.416Z"
|
|
959
959
|
},
|
|
960
960
|
{
|
|
961
961
|
"name": "defensive-countermeasure-mapping",
|
|
@@ -1012,7 +1012,7 @@
|
|
|
1012
1012
|
],
|
|
1013
1013
|
"last_threat_review": "2026-05-11",
|
|
1014
1014
|
"signature": "XZigwq8X/csfrdG10O6Q1V5q0zUqSQGd3QrjRKkZ4fkaodG4mZahYuIQqxc8rU9jjtGAm9LtBXYB+I5csqj9Bw==",
|
|
1015
|
-
"signed_at": "2026-05-
|
|
1015
|
+
"signed_at": "2026-05-13T21:19:34.417Z"
|
|
1016
1016
|
},
|
|
1017
1017
|
{
|
|
1018
1018
|
"name": "identity-assurance",
|
|
@@ -1079,7 +1079,7 @@
|
|
|
1079
1079
|
"d3fend_refs": [],
|
|
1080
1080
|
"last_threat_review": "2026-05-11",
|
|
1081
1081
|
"signature": "pCvavMUh5wR/TFl03Vh6ggKJHWPjakOEPDh7IjaD/U3WLBGPF3eyLBzieNLrPDXxIBbCJqnt9E79Bd4e73xCCg==",
|
|
1082
|
-
"signed_at": "2026-05-
|
|
1082
|
+
"signed_at": "2026-05-13T21:19:34.417Z"
|
|
1083
1083
|
},
|
|
1084
1084
|
{
|
|
1085
1085
|
"name": "ot-ics-security",
|
|
@@ -1135,7 +1135,7 @@
|
|
|
1135
1135
|
"d3fend_refs": [],
|
|
1136
1136
|
"last_threat_review": "2026-05-11",
|
|
1137
1137
|
"signature": "3V0ZpDLRTmCBx5Li+9m3XKA+k9QR+l0aE55cRPMX+UTDRlStKSG5PgrSGcL2ZKJog9hKaUPmjIVh7kkn54SfAg==",
|
|
1138
|
-
"signed_at": "2026-05-
|
|
1138
|
+
"signed_at": "2026-05-13T21:19:34.417Z"
|
|
1139
1139
|
},
|
|
1140
1140
|
{
|
|
1141
1141
|
"name": "coordinated-vuln-disclosure",
|
|
@@ -1187,7 +1187,7 @@
|
|
|
1187
1187
|
"NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
|
|
1188
1188
|
],
|
|
1189
1189
|
"signature": "UCiNjncvhkZItmLQA/Sm1/NCsOiLMwdCjfUw+067v4NIxhaMMaqRrAeD3KgMyEtov7m2Hq2kfwYSt5+DQsYDCQ==",
|
|
1190
|
-
"signed_at": "2026-05-
|
|
1190
|
+
"signed_at": "2026-05-13T21:19:34.418Z"
|
|
1191
1191
|
},
|
|
1192
1192
|
{
|
|
1193
1193
|
"name": "threat-modeling-methodology",
|
|
@@ -1237,7 +1237,7 @@
|
|
|
1237
1237
|
"PASTA v2 updates incorporating AI/ML application threats"
|
|
1238
1238
|
],
|
|
1239
1239
|
"signature": "i8ZUFT7hwTQJyqYXWh8rchdEUNv1kk0bzkF3BHOANFwuVoM0+mukOPr+rhKdOWCPjTX72EG+0Mbs6hBTSfBBAg==",
|
|
1240
|
-
"signed_at": "2026-05-
|
|
1240
|
+
"signed_at": "2026-05-13T21:19:34.418Z"
|
|
1241
1241
|
},
|
|
1242
1242
|
{
|
|
1243
1243
|
"name": "webapp-security",
|
|
@@ -1311,7 +1311,7 @@
|
|
|
1311
1311
|
"d3fend_refs": [],
|
|
1312
1312
|
"last_threat_review": "2026-05-11",
|
|
1313
1313
|
"signature": "3d+Hs8yfERF/NUuVhO3YFYCY+bVn7aAGbNCCyGeqYM2VIt7q/nzNXGfbNfJbSPezClwutOyxbzPwXMj+TjmMDA==",
|
|
1314
|
-
"signed_at": "2026-05-
|
|
1314
|
+
"signed_at": "2026-05-13T21:19:34.418Z"
|
|
1315
1315
|
},
|
|
1316
1316
|
{
|
|
1317
1317
|
"name": "ai-risk-management",
|
|
@@ -1361,7 +1361,7 @@
|
|
|
1361
1361
|
"d3fend_refs": [],
|
|
1362
1362
|
"last_threat_review": "2026-05-11",
|
|
1363
1363
|
"signature": "runa3PkfcThZmv5I+F6D1hOR/kfBeVOcdDZHsJ/59WOjsPj4BPi4d9MtP1vV7G9qq9daGGz6YzZGnejjin4pCA==",
|
|
1364
|
-
"signed_at": "2026-05-
|
|
1364
|
+
"signed_at": "2026-05-13T21:19:34.418Z"
|
|
1365
1365
|
},
|
|
1366
1366
|
{
|
|
1367
1367
|
"name": "sector-healthcare",
|
|
@@ -1421,7 +1421,7 @@
|
|
|
1421
1421
|
"d3fend_refs": [],
|
|
1422
1422
|
"last_threat_review": "2026-05-11",
|
|
1423
1423
|
"signature": "58fEAPnojhmGSpEIIyWIwfj65A2KB4SMyUCoieJ28OZaUktUF+56wLHQOdAW6v2fSeiriFqZEqGyKyLryGGcBg==",
|
|
1424
|
-
"signed_at": "2026-05-
|
|
1424
|
+
"signed_at": "2026-05-13T21:19:34.419Z"
|
|
1425
1425
|
},
|
|
1426
1426
|
{
|
|
1427
1427
|
"name": "sector-financial",
|
|
@@ -1502,7 +1502,7 @@
|
|
|
1502
1502
|
"TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
|
|
1503
1503
|
],
|
|
1504
1504
|
"signature": "wByxWUburbU5PB7EHLDVAByCpMbhKRTWqSoVsBmxIDsz2jCiKb3ogJutFnAV2r2oXuQIUaffGvrvACIrJ2GlBQ==",
|
|
1505
|
-
"signed_at": "2026-05-
|
|
1505
|
+
"signed_at": "2026-05-13T21:19:34.419Z"
|
|
1506
1506
|
},
|
|
1507
1507
|
{
|
|
1508
1508
|
"name": "sector-federal-government",
|
|
@@ -1571,7 +1571,7 @@
|
|
|
1571
1571
|
"Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
|
|
1572
1572
|
],
|
|
1573
1573
|
"signature": "gdiwq/+AQxxNJ2t90dITyLd2ZWdDKHxbyS2I+AVRdM45VVKQEAY4XyheGZW8m5wdDF7N9X2ENIE5CRMfP5jnCA==",
|
|
1574
|
-
"signed_at": "2026-05-
|
|
1574
|
+
"signed_at": "2026-05-13T21:19:34.420Z"
|
|
1575
1575
|
},
|
|
1576
1576
|
{
|
|
1577
1577
|
"name": "sector-energy",
|
|
@@ -1636,7 +1636,7 @@
|
|
|
1636
1636
|
"ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
|
|
1637
1637
|
],
|
|
1638
1638
|
"signature": "3M6nshB0ZNfr3H/rpcy58+/yve91u8kiTOhwUBePTZ0lQeuzlXZZV6/36i/NTWKC4SwgMn8fsZCpLxA5llRaCg==",
|
|
1639
|
-
"signed_at": "2026-05-
|
|
1639
|
+
"signed_at": "2026-05-13T21:19:34.420Z"
|
|
1640
1640
|
},
|
|
1641
1641
|
{
|
|
1642
1642
|
"name": "api-security",
|
|
@@ -1705,7 +1705,7 @@
|
|
|
1705
1705
|
"d3fend_refs": [],
|
|
1706
1706
|
"last_threat_review": "2026-05-11",
|
|
1707
1707
|
"signature": "rLOJBYVELFSVT2zLEByuko5Z1+HwGY99pQOC1XIVHs9l7gmY7F8N8luU/EBvVDcnT9w6nPiC5YKmCy/50LbxBQ==",
|
|
1708
|
-
"signed_at": "2026-05-
|
|
1708
|
+
"signed_at": "2026-05-13T21:19:34.420Z"
|
|
1709
1709
|
},
|
|
1710
1710
|
{
|
|
1711
1711
|
"name": "cloud-security",
|
|
@@ -1786,7 +1786,7 @@
|
|
|
1786
1786
|
"CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
|
|
1787
1787
|
],
|
|
1788
1788
|
"signature": "PyA9IHcGNrCLC0AJW2jF5D5XheA60igeirjHb3IOz/HitBEg3t2g/siP6pAUImx3IUmwp9vh+A6k+KBuyHRgBQ==",
|
|
1789
|
-
"signed_at": "2026-05-
|
|
1789
|
+
"signed_at": "2026-05-13T21:19:34.421Z"
|
|
1790
1790
|
},
|
|
1791
1791
|
{
|
|
1792
1792
|
"name": "container-runtime-security",
|
|
@@ -1848,7 +1848,7 @@
|
|
|
1848
1848
|
"d3fend_refs": [],
|
|
1849
1849
|
"last_threat_review": "2026-05-11",
|
|
1850
1850
|
"signature": "/RQD2SZghTsrG7qurGb1uabyJ2IL9nmbqd412k8tqZw6nzdvvNFWcYDUGBACEQLbSlJvuaJeQdMHQH5ZzzkNDA==",
|
|
1851
|
-
"signed_at": "2026-05-
|
|
1851
|
+
"signed_at": "2026-05-13T21:19:34.421Z"
|
|
1852
1852
|
},
|
|
1853
1853
|
{
|
|
1854
1854
|
"name": "mlops-security",
|
|
@@ -1919,7 +1919,7 @@
|
|
|
1919
1919
|
"MITRE ATLAS v5.2 — track AML.T0010 sub-technique expansion and any new MLOps-pipeline-specific TTPs"
|
|
1920
1920
|
],
|
|
1921
1921
|
"signature": "BeACZFTsTRZyGuOEc7NPGnGUQ84ZrFbYRsg+yqJxCH7QOM69jYJTnExyAyxXfiJyoytzSffJQNK3h0E48rm0BQ==",
|
|
1922
|
-
"signed_at": "2026-05-
|
|
1922
|
+
"signed_at": "2026-05-13T21:19:34.421Z"
|
|
1923
1923
|
},
|
|
1924
1924
|
{
|
|
1925
1925
|
"name": "incident-response-playbook",
|
|
@@ -1981,7 +1981,7 @@
|
|
|
1981
1981
|
"NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
|
|
1982
1982
|
],
|
|
1983
1983
|
"signature": "oy63A4wX5g9rYq3wWYGSS9L7wndNaa1+wJPRPtiHMvQ6CNhTzN8kCs9Ur2SkT7U9BKYdfPLni0gGMjoeDbx+AA==",
|
|
1984
|
-
"signed_at": "2026-05-
|
|
1984
|
+
"signed_at": "2026-05-13T21:19:34.422Z"
|
|
1985
1985
|
},
|
|
1986
1986
|
{
|
|
1987
1987
|
"name": "email-security-anti-phishing",
|
|
@@ -2034,7 +2034,7 @@
|
|
|
2034
2034
|
"d3fend_refs": [],
|
|
2035
2035
|
"last_threat_review": "2026-05-11",
|
|
2036
2036
|
"signature": "EFhMVyQdiLRtYPHsMADrX3aDXyWd/F5sjnGoWtUTeTiIOmrhFMOCHsvpTPotfsLt0u3LgRK5ACgjgeON8PgQAg==",
|
|
2037
|
-
"signed_at": "2026-05-
|
|
2037
|
+
"signed_at": "2026-05-13T21:19:34.422Z"
|
|
2038
2038
|
},
|
|
2039
2039
|
{
|
|
2040
2040
|
"name": "age-gates-child-safety",
|
|
@@ -2102,7 +2102,7 @@
|
|
|
2102
2102
|
"US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
|
|
2103
2103
|
],
|
|
2104
2104
|
"signature": "MMWvg3lIf5ygm31zyf1E43t3W9MfRbMBBPrqlj1wOa8AxVJL8LICnAXfmyJ/TNJXwpF+rfZeDdoxXkql8wmtBA==",
|
|
2105
|
-
"signed_at": "2026-05-
|
|
2105
|
+
"signed_at": "2026-05-13T21:19:34.422Z"
|
|
2106
2106
|
}
|
|
2107
2107
|
]
|
|
2108
2108
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/exceptd-skills",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
4
4
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-security",
|
package/sbom.cdx.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:dac8abdb-9a54-4d36-9f4b-21e8dde200ea",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-
|
|
7
|
+
"timestamp": "2026-05-13T21:19:35.259Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"name": "hand-written",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.11",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.12.
|
|
19
|
+
"version": "0.12.11",
|
|
20
20
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
|
|
21
21
|
"licenses": [
|
|
22
22
|
{
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.11",
|
|
29
29
|
"externalReferences": [
|
|
30
30
|
{
|
|
31
31
|
"type": "distribution",
|
|
32
|
-
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.
|
|
32
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.11"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"type": "vcs",
|