@blamejs/exceptd-skills 0.11.14 → 0.12.0
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 +110 -0
- package/bin/exceptd.js +17 -0
- package/data/_indexes/_meta.json +7 -7
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +2 -2
- package/data/_indexes/chains.json +17 -0
- package/data/_indexes/section-offsets.json +25 -25
- package/data/_indexes/token-budget.json +9 -9
- package/data/cve-catalog.json +114 -0
- package/data/playbooks/mcp.json +17 -4
- package/data/playbooks/sbom.json +121 -4
- package/data/zeroday-lessons.json +93 -0
- package/keys/public.pem +1 -1
- package/lib/cve-curation.js +274 -0
- package/lib/refresh-external.js +172 -1
- package/lib/source-ghsa.js +259 -0
- package/lib/validate-cve-catalog.js +22 -5
- package/manifest-snapshot.json +1 -1
- package/manifest.json +40 -40
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/skills/supply-chain-integrity/skill.md +2 -1
package/lib/refresh-external.js
CHANGED
|
@@ -50,15 +50,20 @@ function parseArgs(argv) {
|
|
|
50
50
|
fromFixture: null, // path to fixture dir
|
|
51
51
|
fromCache: null, // path to .cache/upstream dir (or default if --from-cache passed bare)
|
|
52
52
|
swarm: false, // fan-out sources across worker threads
|
|
53
|
+
advisory: null, // v0.12.0: single-advisory seed (CVE-* or GHSA-*)
|
|
53
54
|
help: false,
|
|
54
55
|
quiet: false,
|
|
56
|
+
json: false,
|
|
55
57
|
};
|
|
56
58
|
for (let i = 2; i < argv.length; i++) {
|
|
57
59
|
const a = argv[i];
|
|
58
60
|
if (a === "--apply") out.apply = true;
|
|
59
61
|
else if (a === "--quiet") out.quiet = true;
|
|
60
62
|
else if (a === "--swarm") out.swarm = true;
|
|
63
|
+
else if (a === "--json") out.json = true;
|
|
61
64
|
else if (a === "--help" || a === "-h") out.help = true;
|
|
65
|
+
else if (a === "--advisory") { out.advisory = argv[++i]; }
|
|
66
|
+
else if (a.startsWith("--advisory=")) { out.advisory = a.slice("--advisory=".length); }
|
|
62
67
|
else if (a === "--from-cache") {
|
|
63
68
|
// accept either --from-cache <path> or --from-cache (default path)
|
|
64
69
|
const next = argv[i + 1];
|
|
@@ -101,6 +106,26 @@ Modes:
|
|
|
101
106
|
--indexes-only rebuild data/_indexes/ only; no network. Equivalent to
|
|
102
107
|
\`exceptd refresh --indexes-only\`.
|
|
103
108
|
--swarm fan out sources across worker threads. Best with --from-cache.
|
|
109
|
+
--advisory <id> (v0.12.0) seed a single catalog entry from a CVE or GHSA ID.
|
|
110
|
+
Fetches from GitHub Advisory Database (covers npm + PyPI +
|
|
111
|
+
Maven + Go + ...) and writes a DRAFT to data/cve-catalog.json
|
|
112
|
+
marked with _auto_imported: true. Editorial fields
|
|
113
|
+
(framework_control_gaps, iocs, atlas_refs, attack_refs)
|
|
114
|
+
remain null pending review via:
|
|
115
|
+
exceptd run cve-curation --advisory <id>
|
|
116
|
+
Examples:
|
|
117
|
+
exceptd refresh --advisory CVE-2026-45321
|
|
118
|
+
exceptd refresh --advisory GHSA-xxxx-xxxx-xxxx --apply
|
|
119
|
+
|
|
120
|
+
Sources (default = all):
|
|
121
|
+
kev CISA Known Exploited Vulnerabilities
|
|
122
|
+
epss FIRST EPSS exploit-prediction scores
|
|
123
|
+
nvd NIST NVD per-CVE feed
|
|
124
|
+
rfc IETF Datatracker per-RFC
|
|
125
|
+
pins Upstream version-pin drift (MITRE ATLAS/ATT&CK/D3FEND/CWE) — report only
|
|
126
|
+
ghsa (v0.12.0) GitHub Advisory Database — npm/PyPI/Maven/etc. Lands new CVE
|
|
127
|
+
IDs as DRAFTS (_auto_imported: true); catalog validator treats drafts
|
|
128
|
+
as warnings, not errors. Editorial review still required.
|
|
104
129
|
|
|
105
130
|
Air-gap workflow:
|
|
106
131
|
1. On a connected host: \`exceptd refresh --prefetch\`
|
|
@@ -439,12 +464,58 @@ const PINS_SOURCE = {
|
|
|
439
464
|
},
|
|
440
465
|
};
|
|
441
466
|
|
|
467
|
+
/**
|
|
468
|
+
* v0.12.0: GHSA (GitHub Advisory Database) source. Covers npm, PyPI,
|
|
469
|
+
* RubyGems, Maven, NuGet, Go, Composer, Swift, Erlang, Pub, Rust — all
|
|
470
|
+
* in one feed, updated within hours of disclosure. Much faster than KEV
|
|
471
|
+
* (variable, often days) or NVD (~10 days).
|
|
472
|
+
*
|
|
473
|
+
* Apply path: new CVE IDs from GHSA land in data/cve-catalog.json as
|
|
474
|
+
* DRAFTS (`_auto_imported: true` + `_draft: true`). The strict catalog
|
|
475
|
+
* validator treats drafts as warnings, not errors, so the nightly
|
|
476
|
+
* auto-PR pipeline can ship them without blocking on editorial review.
|
|
477
|
+
* Framework gaps + IoCs + ATLAS/ATT&CK refs require human or AI-assisted
|
|
478
|
+
* synthesis via `exceptd run cve-curation --advisory <id>`.
|
|
479
|
+
*/
|
|
480
|
+
const GHSA_SOURCE = {
|
|
481
|
+
name: "ghsa",
|
|
482
|
+
description: "GitHub Advisory Database — multi-ecosystem disclosure feed (npm, PyPI, Maven, Go, etc.)",
|
|
483
|
+
applies_to: "data/cve-catalog.json",
|
|
484
|
+
async fetchDiff(ctx) {
|
|
485
|
+
if (ctx.fixtures?.ghsa) return synthesizeFromFixture(ctx, "ghsa");
|
|
486
|
+
if (ctx.cacheDir) {
|
|
487
|
+
// Cache parity: ghsa cache layout is .cache/upstream/ghsa/<published-date>.json
|
|
488
|
+
// For v0.12.0 we fall through to live fetch — caching is a v0.13 follow-up.
|
|
489
|
+
}
|
|
490
|
+
const ghsa = require("./source-ghsa");
|
|
491
|
+
return ghsa.buildDiff(ctx);
|
|
492
|
+
},
|
|
493
|
+
async applyDiff(ctx, diffs) {
|
|
494
|
+
const ghsa = require("./source-ghsa");
|
|
495
|
+
let updated = 0;
|
|
496
|
+
const errors = [];
|
|
497
|
+
for (const d of diffs) {
|
|
498
|
+
if (d.field !== "_new_entry") continue;
|
|
499
|
+
if (!d.after || !d.id) continue;
|
|
500
|
+
if (ctx.cveCatalog[d.id]) continue; // never overwrite existing entries
|
|
501
|
+
try {
|
|
502
|
+
ctx.cveCatalog[d.id] = d.after;
|
|
503
|
+
updated++;
|
|
504
|
+
} catch (e) {
|
|
505
|
+
errors.push(`${d.id}: ${e.message}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return { updated, errors };
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
442
512
|
const ALL_SOURCES = {
|
|
443
513
|
kev: KEV_SOURCE,
|
|
444
514
|
epss: EPSS_SOURCE,
|
|
445
515
|
nvd: NVD_SOURCE,
|
|
446
516
|
rfc: RFC_SOURCE,
|
|
447
517
|
pins: PINS_SOURCE,
|
|
518
|
+
ghsa: GHSA_SOURCE,
|
|
448
519
|
};
|
|
449
520
|
|
|
450
521
|
// --- Cache-mode helpers ------------------------------------------------
|
|
@@ -653,7 +724,7 @@ function loadCtx(opts) {
|
|
|
653
724
|
cacheDir: null,
|
|
654
725
|
};
|
|
655
726
|
if (opts.fromFixture) {
|
|
656
|
-
ctx.fixtures = { dir: path.resolve(opts.fromFixture), kev: true, epss: true, nvd: true, rfc: true, pins: true };
|
|
727
|
+
ctx.fixtures = { dir: path.resolve(opts.fromFixture), kev: true, epss: true, nvd: true, rfc: true, pins: true, ghsa: true };
|
|
657
728
|
} else if (opts.fromCache) {
|
|
658
729
|
const abs = path.resolve(opts.fromCache);
|
|
659
730
|
ctx.cacheDir = abs;
|
|
@@ -694,6 +765,97 @@ function chosenSources(opts) {
|
|
|
694
765
|
return out;
|
|
695
766
|
}
|
|
696
767
|
|
|
768
|
+
/**
|
|
769
|
+
* v0.12.0: single-advisory seed. Operator types
|
|
770
|
+
* exceptd refresh --advisory CVE-2026-45321
|
|
771
|
+
* or
|
|
772
|
+
* exceptd refresh --advisory GHSA-xxxx-xxxx-xxxx --apply
|
|
773
|
+
*
|
|
774
|
+
* Tool fetches from GHSA (covers npm, PyPI, etc.), normalizes to the
|
|
775
|
+
* exceptd catalog draft shape, and either prints the seed (default) or
|
|
776
|
+
* writes it to data/cve-catalog.json (--apply). Always exits non-zero
|
|
777
|
+
* when a draft is produced, signaling that editorial review is needed.
|
|
778
|
+
*/
|
|
779
|
+
async function seedSingleAdvisory(opts) {
|
|
780
|
+
const ghsa = require("./source-ghsa");
|
|
781
|
+
const id = opts.advisory;
|
|
782
|
+
const result = await ghsa.fetchAdvisoryById(id, {});
|
|
783
|
+
if (!result.ok) {
|
|
784
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: ${result.error}`, source: result.source, hint: "Verify the ID format (CVE-YYYY-NNNN or GHSA-*) and network reachability. Set EXCEPTD_GHSA_FIXTURE for offline testing." };
|
|
785
|
+
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
786
|
+
else process.stderr.write(`[refresh --advisory] ${err.error}\n hint: ${err.hint}\n`);
|
|
787
|
+
process.exitCode = 2;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const advisory = result.advisories[0];
|
|
791
|
+
if (!advisory) {
|
|
792
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: no matching advisory found`, source: result.source };
|
|
793
|
+
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
794
|
+
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
795
|
+
process.exitCode = 2;
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const normalized = ghsa.normalizeAdvisory(advisory);
|
|
799
|
+
if (!normalized) {
|
|
800
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: advisory has no CVE ID (GHSA-only entries are not imported into the CVE catalog in v0.12)`, ghsa_id: advisory.ghsa_id || null };
|
|
801
|
+
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
802
|
+
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
803
|
+
process.exitCode = 2;
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const cveId = Object.keys(normalized)[0];
|
|
807
|
+
|
|
808
|
+
if (!opts.apply) {
|
|
809
|
+
// Print the draft to stdout — operator pipes to jq / inspects /
|
|
810
|
+
// commits manually. Exit 3 = "draft produced, not applied."
|
|
811
|
+
const output = {
|
|
812
|
+
ok: true,
|
|
813
|
+
verb: "refresh",
|
|
814
|
+
mode: "advisory-seed-dry-run",
|
|
815
|
+
advisory_id: id,
|
|
816
|
+
cve_id: cveId,
|
|
817
|
+
draft: normalized[cveId],
|
|
818
|
+
hint: "Re-run with --apply to write this draft into data/cve-catalog.json. After apply, run `exceptd run cve-curation --advisory " + cveId + "` to surface editorial proposals (framework gaps, IoCs, ATLAS/ATT&CK refs).",
|
|
819
|
+
};
|
|
820
|
+
if (opts.json) process.stdout.write(JSON.stringify(output) + "\n");
|
|
821
|
+
else {
|
|
822
|
+
process.stdout.write(`[refresh --advisory] ${cveId} draft prepared (not applied).\n`);
|
|
823
|
+
process.stdout.write(` Run with --apply to write into data/cve-catalog.json.\n`);
|
|
824
|
+
process.stdout.write(` Then: exceptd run cve-curation --advisory ${cveId}\n`);
|
|
825
|
+
}
|
|
826
|
+
process.exitCode = 3;
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Apply: write to cve-catalog.json with the _auto_imported flag.
|
|
831
|
+
const catalogPath = ABS("data/cve-catalog.json");
|
|
832
|
+
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
833
|
+
if (catalog[cveId] && !catalog[cveId]._auto_imported && !catalog[cveId]._draft) {
|
|
834
|
+
// Refuse to overwrite a human-curated entry.
|
|
835
|
+
const err = { ok: false, verb: "refresh", error: `${cveId} already present in catalog and is human-curated (not a draft). Refusing to overwrite. Edit manually if intentional.`, existing_last_updated: catalog[cveId].last_updated };
|
|
836
|
+
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
837
|
+
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
838
|
+
process.exitCode = 4;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
catalog[cveId] = normalized[cveId];
|
|
842
|
+
fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2) + "\n", "utf8");
|
|
843
|
+
const output = {
|
|
844
|
+
ok: true,
|
|
845
|
+
verb: "refresh",
|
|
846
|
+
mode: "advisory-seed-applied",
|
|
847
|
+
advisory_id: id,
|
|
848
|
+
cve_id: cveId,
|
|
849
|
+
written_to: "data/cve-catalog.json",
|
|
850
|
+
is_draft: true,
|
|
851
|
+
hint: "Draft written. Required next steps before this entry passes the strict catalog gate: (1) `exceptd run cve-curation --advisory " + cveId + "` to surface editorial proposals; (2) human review + fill in framework_control_gaps, atlas_refs, attack_refs, iocs; (3) add matching entry to data/zeroday-lessons.json; (4) remove `_auto_imported` and `_draft` flags.",
|
|
852
|
+
};
|
|
853
|
+
if (opts.json) process.stdout.write(JSON.stringify(output) + "\n");
|
|
854
|
+
else process.stdout.write(`[refresh --advisory] ${cveId} draft written to data/cve-catalog.json.\n Next: exceptd run cve-curation --advisory ${cveId}\n`);
|
|
855
|
+
// Exit 3 even on successful write — "draft applied, editorial step pending."
|
|
856
|
+
process.exitCode = 3;
|
|
857
|
+
}
|
|
858
|
+
|
|
697
859
|
async function main() {
|
|
698
860
|
const opts = parseArgs(process.argv);
|
|
699
861
|
if (opts.help) {
|
|
@@ -701,6 +863,15 @@ async function main() {
|
|
|
701
863
|
process.exit(0);
|
|
702
864
|
}
|
|
703
865
|
|
|
866
|
+
// v0.12.0: `--advisory <id>` short-circuits the normal source loop and
|
|
867
|
+
// seeds a single CVE catalog entry from GHSA. Exits non-zero ("draft
|
|
868
|
+
// written, please review") so CI pipelines surface the needed editorial
|
|
869
|
+
// step. Operator must run `--apply` for the write to land; without it,
|
|
870
|
+
// the seed is printed to stdout for review.
|
|
871
|
+
if (opts.advisory) {
|
|
872
|
+
return seedSingleAdvisory(opts);
|
|
873
|
+
}
|
|
874
|
+
|
|
704
875
|
const ctx = loadCtx(opts);
|
|
705
876
|
const sources = chosenSources(opts);
|
|
706
877
|
const log = (s) => opts.quiet || console.log(s);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/source-ghsa.js
|
|
5
|
+
*
|
|
6
|
+
* GitHub Advisory Database fetcher. The GHSA covers npm, PyPI, RubyGems,
|
|
7
|
+
* Maven, NuGet, Go, Composer, Swift, Erlang, Pub, and Rust ecosystems in
|
|
8
|
+
* one feed and is updated within hours of disclosure — much faster than
|
|
9
|
+
* NVD (~10 days) or KEV (variable, often days).
|
|
10
|
+
*
|
|
11
|
+
* Endpoint: GET https://api.github.com/advisories
|
|
12
|
+
* - Unauthenticated: 60 req/hr (sufficient for nightly refresh)
|
|
13
|
+
* - Authenticated: 5000 req/hr (set GITHUB_TOKEN env var)
|
|
14
|
+
*
|
|
15
|
+
* Returns drafts — every imported entry carries `_auto_imported: true`
|
|
16
|
+
* + `_draft: true` so the strict catalog validator treats them as
|
|
17
|
+
* warnings, not errors. Editorial fields (framework_control_gaps,
|
|
18
|
+
* iocs, atlas_refs) are left null until a human or AI assistant
|
|
19
|
+
* fills them in via the seven-phase playbook flow.
|
|
20
|
+
*
|
|
21
|
+
* Honors EXCEPTD_GHSA_FIXTURE env var for offline testing — value is a
|
|
22
|
+
* path to a JSON array matching the api.github.com/advisories shape.
|
|
23
|
+
*
|
|
24
|
+
* Zero npm deps. Node 24 stdlib only.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const https = require("https");
|
|
28
|
+
const fs = require("fs");
|
|
29
|
+
|
|
30
|
+
const GHSA_HOST = "api.github.com";
|
|
31
|
+
const GHSA_PATH = "/advisories?per_page=50&type=reviewed&sort=published&direction=desc";
|
|
32
|
+
const REQUEST_TIMEOUT_MS = 10000;
|
|
33
|
+
const USER_AGENT = "exceptd-security/source-ghsa (+https://exceptd.com)";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a page of advisories (default: latest 50).
|
|
37
|
+
*
|
|
38
|
+
* Returns:
|
|
39
|
+
* { ok: true, advisories: [...], source: "github-api" | "fixture", rate_limit?: { remaining, reset } }
|
|
40
|
+
* { ok: false, error, source: "offline" }
|
|
41
|
+
*/
|
|
42
|
+
async function fetchAdvisories({ timeoutMs = REQUEST_TIMEOUT_MS, path = GHSA_PATH, token = null } = {}) {
|
|
43
|
+
if (process.env.EXCEPTD_GHSA_FIXTURE) {
|
|
44
|
+
try {
|
|
45
|
+
const arr = JSON.parse(fs.readFileSync(process.env.EXCEPTD_GHSA_FIXTURE, "utf8"));
|
|
46
|
+
return { ok: true, advisories: Array.isArray(arr) ? arr : [arr], source: "fixture" };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ok: false, error: `fixture: ${e.message}`, source: "offline" };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const headers = {
|
|
54
|
+
"Accept": "application/vnd.github+json",
|
|
55
|
+
"User-Agent": USER_AGENT,
|
|
56
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
57
|
+
};
|
|
58
|
+
if (token || process.env.GITHUB_TOKEN) {
|
|
59
|
+
headers.Authorization = `Bearer ${token || process.env.GITHUB_TOKEN}`;
|
|
60
|
+
}
|
|
61
|
+
const req = https.get({
|
|
62
|
+
host: GHSA_HOST,
|
|
63
|
+
path,
|
|
64
|
+
headers,
|
|
65
|
+
timeout: timeoutMs,
|
|
66
|
+
}, (res) => {
|
|
67
|
+
if (res.statusCode !== 200) {
|
|
68
|
+
res.resume();
|
|
69
|
+
return resolve({ ok: false, error: `GHSA returned HTTP ${res.statusCode}`, source: "offline" });
|
|
70
|
+
}
|
|
71
|
+
const chunks = [];
|
|
72
|
+
res.on("data", (c) => chunks.push(c));
|
|
73
|
+
res.on("end", () => {
|
|
74
|
+
try {
|
|
75
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
76
|
+
const advisories = Array.isArray(body) ? body : (body ? [body] : []);
|
|
77
|
+
resolve({
|
|
78
|
+
ok: true,
|
|
79
|
+
advisories,
|
|
80
|
+
source: "github-api",
|
|
81
|
+
rate_limit: {
|
|
82
|
+
remaining: parseInt(res.headers["x-ratelimit-remaining"], 10) || null,
|
|
83
|
+
reset: parseInt(res.headers["x-ratelimit-reset"], 10) || null,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
} catch (e) {
|
|
87
|
+
resolve({ ok: false, error: `parse: ${e.message}`, source: "offline" });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
req.on("timeout", () => req.destroy(new Error("timeout")));
|
|
92
|
+
req.on("error", (e) => resolve({ ok: false, error: e.message, source: "offline" }));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fetch a single advisory by ID — accepts CVE-* or GHSA-* identifiers.
|
|
98
|
+
*
|
|
99
|
+
* GHSA-IDs hit /advisories/<ghsa-id> directly. CVE-IDs require a search
|
|
100
|
+
* since the API is keyed by GHSA. We fall back to a query-string search.
|
|
101
|
+
*/
|
|
102
|
+
async function fetchAdvisoryById(id, opts = {}) {
|
|
103
|
+
if (!id || typeof id !== "string") {
|
|
104
|
+
return { ok: false, error: "id is required (CVE-* or GHSA-*)", source: "offline" };
|
|
105
|
+
}
|
|
106
|
+
if (process.env.EXCEPTD_GHSA_FIXTURE) {
|
|
107
|
+
const r = await fetchAdvisories(opts);
|
|
108
|
+
if (!r.ok) return r;
|
|
109
|
+
const match = r.advisories.find(a =>
|
|
110
|
+
(a.ghsa_id && a.ghsa_id.toUpperCase() === id.toUpperCase()) ||
|
|
111
|
+
(a.cve_id && a.cve_id.toUpperCase() === id.toUpperCase())
|
|
112
|
+
);
|
|
113
|
+
if (!match) return { ok: false, error: `${id} not in fixture`, source: "fixture" };
|
|
114
|
+
return { ok: true, advisories: [match], source: "fixture" };
|
|
115
|
+
}
|
|
116
|
+
if (/^GHSA-/i.test(id)) {
|
|
117
|
+
return fetchAdvisories({ ...opts, path: `/advisories/${id.toLowerCase()}` });
|
|
118
|
+
}
|
|
119
|
+
if (/^CVE-\d{4}-\d+$/i.test(id)) {
|
|
120
|
+
return fetchAdvisories({ ...opts, path: `/advisories?cve_id=${encodeURIComponent(id.toUpperCase())}` });
|
|
121
|
+
}
|
|
122
|
+
return { ok: false, error: `unrecognized id format (expected CVE-YYYY-NNNN or GHSA-*): ${id}`, source: "offline" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalize a GHSA advisory object to the exceptd catalog draft shape.
|
|
127
|
+
* Fields the GHSA carries authoritatively: cve_id, ghsa_id, summary,
|
|
128
|
+
* severity, cvss, vulnerabilities (package + version range), published_at,
|
|
129
|
+
* references. Editorial fields (framework_control_gaps, iocs, atlas_refs,
|
|
130
|
+
* attack_refs, rwep_factors) are LEFT NULL — drafts. The seven-phase
|
|
131
|
+
* playbook flow OR a human reviewer fills these in.
|
|
132
|
+
*
|
|
133
|
+
* Returns null if the advisory lacks a CVE ID (we don't import GHSA-only
|
|
134
|
+
* advisories into the CVE catalog — they belong in a separate GHSA index
|
|
135
|
+
* which is a v0.13 design).
|
|
136
|
+
*/
|
|
137
|
+
function normalizeAdvisory(adv) {
|
|
138
|
+
if (!adv || !adv.cve_id) return null;
|
|
139
|
+
|
|
140
|
+
const ecosystems = new Set();
|
|
141
|
+
const affected = [];
|
|
142
|
+
const ecosystemPackages = [];
|
|
143
|
+
for (const v of (adv.vulnerabilities || [])) {
|
|
144
|
+
if (v?.package?.ecosystem) ecosystems.add(v.package.ecosystem);
|
|
145
|
+
if (v?.package?.name) {
|
|
146
|
+
ecosystemPackages.push(`${v.package.ecosystem || "?"}:${v.package.name}`);
|
|
147
|
+
if (v.vulnerable_version_range) {
|
|
148
|
+
affected.push(`${v.package.name} ${v.vulnerable_version_range}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cvssScore = adv.cvss?.score ?? null;
|
|
154
|
+
const cvssVector = adv.cvss?.vector_string || null;
|
|
155
|
+
const severity = (adv.severity || "").toLowerCase();
|
|
156
|
+
// Derive a coarse type from package ecosystem when nothing better available.
|
|
157
|
+
const inferredType = ecosystems.has("npm") ? "supply-chain-npm"
|
|
158
|
+
: ecosystems.has("pip") ? "supply-chain-pypi"
|
|
159
|
+
: ecosystems.has("maven") ? "supply-chain-maven"
|
|
160
|
+
: ecosystems.has("rubygems") ? "supply-chain-gem"
|
|
161
|
+
: "supply-chain-other";
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
[adv.cve_id]: {
|
|
165
|
+
name: adv.summary || adv.cve_id,
|
|
166
|
+
type: inferredType,
|
|
167
|
+
cvss_score: cvssScore,
|
|
168
|
+
cvss_vector: cvssVector,
|
|
169
|
+
cisa_kev: false,
|
|
170
|
+
cisa_kev_date: null,
|
|
171
|
+
cisa_kev_pending: severity === "critical",
|
|
172
|
+
cisa_kev_pending_reason: severity === "critical"
|
|
173
|
+
? `GHSA severity critical (CVSS ${cvssScore}). KEV listing typically follows for critical advisories with confirmed exploitation; verify before publish.`
|
|
174
|
+
: null,
|
|
175
|
+
poc_available: null,
|
|
176
|
+
poc_description: null,
|
|
177
|
+
ai_discovered: null,
|
|
178
|
+
ai_assisted_weaponization: null,
|
|
179
|
+
active_exploitation: severity === "critical" ? "suspected" : "unknown",
|
|
180
|
+
affected: ecosystemPackages.join(", ") || null,
|
|
181
|
+
affected_versions: affected,
|
|
182
|
+
vector: null,
|
|
183
|
+
complexity: null,
|
|
184
|
+
patch_available: null,
|
|
185
|
+
patch_required_reboot: false,
|
|
186
|
+
live_patch_available: null,
|
|
187
|
+
live_patch_tools: [],
|
|
188
|
+
framework_control_gaps: null,
|
|
189
|
+
atlas_refs: [],
|
|
190
|
+
attack_refs: [],
|
|
191
|
+
rwep_score: null,
|
|
192
|
+
rwep_factors: null,
|
|
193
|
+
rwep_notes: "Auto-imported from GHSA. RWEP factors require editorial review before this entry passes the strict catalog gate.",
|
|
194
|
+
epss_score: null,
|
|
195
|
+
epss_percentile: null,
|
|
196
|
+
epss_date: null,
|
|
197
|
+
epss_source: adv.cve_id ? `https://api.first.org/data/v1/epss?cve=${adv.cve_id}` : null,
|
|
198
|
+
source_verified: new Date().toISOString().slice(0, 10),
|
|
199
|
+
verification_sources: [
|
|
200
|
+
...(adv.html_url ? [adv.html_url] : []),
|
|
201
|
+
...(adv.cve_id ? [`https://nvd.nist.gov/vuln/detail/${adv.cve_id}`] : []),
|
|
202
|
+
...(adv.references || []).slice(0, 10),
|
|
203
|
+
],
|
|
204
|
+
vendor_advisories: [
|
|
205
|
+
{
|
|
206
|
+
vendor: "GitHub Security Advisories",
|
|
207
|
+
advisory_id: adv.ghsa_id || null,
|
|
208
|
+
url: adv.html_url || `https://github.com/advisories?query=${encodeURIComponent(adv.cve_id)}`,
|
|
209
|
+
severity: severity || null,
|
|
210
|
+
published_date: (adv.published_at || "").slice(0, 10) || null,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
iocs: null,
|
|
214
|
+
_auto_imported: true,
|
|
215
|
+
_draft: true,
|
|
216
|
+
_draft_reason: "Imported from GHSA on " + new Date().toISOString().slice(0, 10) + ". Editorial fields (framework_control_gaps, atlas_refs, attack_refs, iocs, vector, complexity, rwep_factors) require human review. Run `exceptd run sbom --evidence -` against an affected repo to gather IoCs; consult MITRE ATLAS + ATT&CK catalogs for refs.",
|
|
217
|
+
_source_ghsa_id: adv.ghsa_id || null,
|
|
218
|
+
_source_published_at: adv.published_at || null,
|
|
219
|
+
last_updated: new Date().toISOString().slice(0, 10),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Build a refresh diff for the existing refresh-external orchestrator.
|
|
226
|
+
* Compares the latest 50 advisories' CVE IDs against the local catalog;
|
|
227
|
+
* any CVE ID not in the catalog becomes an "add" diff.
|
|
228
|
+
*/
|
|
229
|
+
async function buildDiff(ctx) {
|
|
230
|
+
const result = await fetchAdvisories({});
|
|
231
|
+
if (!result.ok) {
|
|
232
|
+
return { status: "unreachable", diffs: [], errors: 1, summary: `GHSA fetch failed: ${result.error}` };
|
|
233
|
+
}
|
|
234
|
+
const existing = new Set(Object.keys(ctx.cveCatalog || {}).filter(k => /^CVE-/.test(k)));
|
|
235
|
+
const diffs = [];
|
|
236
|
+
for (const adv of result.advisories) {
|
|
237
|
+
if (!adv.cve_id) continue;
|
|
238
|
+
if (existing.has(adv.cve_id)) continue;
|
|
239
|
+
const normalized = normalizeAdvisory(adv);
|
|
240
|
+
if (!normalized) continue;
|
|
241
|
+
diffs.push({
|
|
242
|
+
id: adv.cve_id,
|
|
243
|
+
field: "_new_entry",
|
|
244
|
+
before: null,
|
|
245
|
+
after: normalized[adv.cve_id],
|
|
246
|
+
severity: adv.severity || null,
|
|
247
|
+
source: "ghsa",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
status: "ok",
|
|
252
|
+
diffs,
|
|
253
|
+
errors: 0,
|
|
254
|
+
summary: `GHSA returned ${result.advisories.length} reviewed advisories; ${diffs.length} new CVE ID(s) not yet in local catalog.`,
|
|
255
|
+
rate_limit: result.rate_limit || null,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { fetchAdvisories, fetchAdvisoryById, normalizeAdvisory, buildDiff };
|
|
@@ -172,14 +172,30 @@ function main() {
|
|
|
172
172
|
const lessonKeys = new Set(Object.keys(lessons).filter((k) => !k.startsWith('_')));
|
|
173
173
|
|
|
174
174
|
let failed = 0;
|
|
175
|
+
let drafts = 0;
|
|
175
176
|
for (const key of cveKeys) {
|
|
176
177
|
const entry = catalog[key];
|
|
178
|
+
// v0.12.0: GHSA-imported drafts are flagged `_auto_imported: true` +
|
|
179
|
+
// `_draft: true`. They pass validation as WARNINGS (printed but not
|
|
180
|
+
// exit-failing) so the nightly auto-PR pipeline can ship them while
|
|
181
|
+
// editorial fields await human or AI-assisted enrichment via
|
|
182
|
+
// `exceptd run cve-curation --advisory <id>`.
|
|
183
|
+
const isDraft = entry && (entry._auto_imported === true || entry._draft === true);
|
|
177
184
|
const errors = validate(entry, schema, 'cve', key);
|
|
178
|
-
if (!lessonKeys.has(key)) {
|
|
185
|
+
if (!lessonKeys.has(key) && !isDraft) {
|
|
179
186
|
errors.push(
|
|
180
187
|
`${key}: missing matching entry in data/zeroday-lessons.json (rule #6: zero-day learning is live)`,
|
|
181
188
|
);
|
|
182
189
|
}
|
|
190
|
+
if (isDraft) {
|
|
191
|
+
drafts++;
|
|
192
|
+
if (!opts.quiet) {
|
|
193
|
+
console.log(`DRAFT ${key} (auto-imported — needs editorial review)`);
|
|
194
|
+
for (const e of errors) console.log(` - [warn] ${e}`);
|
|
195
|
+
}
|
|
196
|
+
// Drafts don't increment `failed` — they're warnings, not errors.
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
183
199
|
if (errors.length === 0) {
|
|
184
200
|
if (!opts.quiet) console.log(`PASS ${key}`);
|
|
185
201
|
} else {
|
|
@@ -203,10 +219,11 @@ function main() {
|
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
const total = cveKeys.length;
|
|
206
|
-
const passed = total - failed;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
const passed = total - failed - drafts;
|
|
223
|
+
const summary = `\n${passed}/${total} CVE entries validated` +
|
|
224
|
+
(drafts ? `, ${drafts} draft(s) (auto-imported)` : '') +
|
|
225
|
+
(failed ? `, ${failed} failed` : '') + '.';
|
|
226
|
+
console.log(summary);
|
|
210
227
|
process.exit(failed === 0 ? 0 : 1);
|
|
211
228
|
}
|
|
212
229
|
|
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-13T02:31:32.493Z",
|
|
4
4
|
"atlas_version": "5.1.0",
|
|
5
5
|
"skill_count": 38,
|
|
6
6
|
"skills": [
|