@blamejs/exceptd-skills 0.12.41 → 0.13.1
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 +124 -0
- package/bin/exceptd.js +52 -44
- package/data/_indexes/_meta.json +49 -49
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +2 -2
- package/data/_indexes/chains.json +1531 -575
- package/data/_indexes/jurisdiction-map.json +15 -4
- package/data/_indexes/section-offsets.json +1244 -1244
- package/data/_indexes/token-budget.json +173 -173
- package/data/atlas-ttps.json +55 -11
- package/data/attack-techniques.json +124 -19
- package/data/cve-catalog.json +194 -27
- package/data/cwe-catalog.json +15 -5
- package/data/framework-control-gaps.json +32 -10
- package/data/playbooks/ai-api.json +5 -0
- package/data/playbooks/cicd-pipeline-compromise.json +970 -0
- package/data/playbooks/cloud-iam-incident.json +4 -1
- package/data/playbooks/cred-stores.json +10 -0
- package/data/playbooks/framework.json +16 -0
- package/data/playbooks/hardening.json +4 -0
- package/data/playbooks/identity-sso-compromise.json +951 -0
- package/data/playbooks/idp-incident.json +3 -0
- package/data/playbooks/kernel.json +6 -0
- package/data/playbooks/llm-tool-use-exfil.json +963 -0
- package/data/playbooks/mcp.json +6 -0
- package/data/playbooks/runtime.json +4 -0
- package/data/playbooks/sbom.json +13 -0
- package/data/playbooks/secrets.json +6 -0
- package/data/playbooks/webhook-callback-abuse.json +916 -0
- package/data/zeroday-lessons.json +178 -0
- package/lib/cross-ref-api.js +33 -13
- package/lib/cve-curation.js +12 -1
- package/lib/exit-codes.js +29 -0
- package/lib/lint-skills.js +24 -2
- package/lib/refresh-external.js +17 -1
- package/lib/scoring.js +55 -0
- package/lib/source-advisories.js +281 -0
- package/manifest.json +83 -83
- package/orchestrator/index.js +207 -24
- package/package.json +1 -1
- package/sbom.cdx.json +134 -79
- package/scripts/predeploy.js +7 -13
- package/scripts/refresh-reverse-refs.js +86 -0
- package/scripts/refresh-sbom.js +21 -4
- package/skills/age-gates-child-safety/skill.md +1 -5
- package/skills/ai-attack-surface/skill.md +11 -4
- package/skills/ai-c2-detection/skill.md +11 -2
- package/skills/ai-risk-management/skill.md +4 -2
- package/skills/api-security/skill.md +7 -8
- package/skills/attack-surface-pentest/skill.md +2 -2
- package/skills/cloud-iam-incident/skill.md +1 -5
- package/skills/cloud-security/skill.md +0 -4
- package/skills/compliance-theater/skill.md +10 -2
- package/skills/container-runtime-security/skill.md +1 -3
- package/skills/dlp-gap-analysis/skill.md +3 -4
- package/skills/email-security-anti-phishing/skill.md +1 -8
- package/skills/exploit-scoring/skill.md +7 -2
- package/skills/framework-gap-analysis/skill.md +1 -1
- package/skills/fuzz-testing-strategy/skill.md +1 -2
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +1 -3
- package/skills/idp-incident-response/skill.md +1 -4
- package/skills/incident-response-playbook/skill.md +1 -5
- package/skills/kernel-lpe-triage/skill.md +2 -2
- package/skills/mcp-agent-trust/skill.md +13 -3
- package/skills/mlops-security/skill.md +2 -3
- package/skills/ot-ics-security/skill.md +0 -3
- package/skills/policy-exception-gen/skill.md +11 -3
- package/skills/pqc-first/skill.md +4 -2
- package/skills/rag-pipeline-security/skill.md +2 -0
- package/skills/ransomware-response/skill.md +1 -5
- package/skills/researcher/skill.md +4 -3
- package/skills/sector-energy/skill.md +0 -4
- package/skills/sector-federal-government/skill.md +2 -3
- package/skills/sector-financial/skill.md +1 -4
- package/skills/sector-healthcare/skill.md +0 -5
- package/skills/sector-telecom/skill.md +0 -4
- package/skills/security-maturity-tiers/skill.md +1 -2
- package/skills/skill-update-loop/skill.md +4 -3
- package/skills/supply-chain-integrity/skill.md +4 -3
- package/skills/threat-model-currency/skill.md +1 -1
- package/skills/threat-modeling-methodology/skill.md +2 -1
- package/skills/webapp-security/skill.md +0 -5
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/source-advisories.js — primary-source advisory-feed polling.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists. The post-mortem on CVE-2026-46333 (ssh-keysign-pwn,
|
|
7
|
+
* disclosed 2026-05-14, missed by the toolkit at T+0 through T+3) found
|
|
8
|
+
* that the existing source set (kev, epss, nvd, rfc, pins, ghsa, osv)
|
|
9
|
+
* sits at the END of the disclosure pipeline. Qualys → kernel.org commit
|
|
10
|
+
* → distro advisory → NVD enrichment is sequential; the existing pollers
|
|
11
|
+
* only see the last step, which lags by 3-14 days.
|
|
12
|
+
*
|
|
13
|
+
* The 4 feeds below sit much earlier in the pipeline:
|
|
14
|
+
*
|
|
15
|
+
* Qualys TRU RSS — Qualys-disclosed CVEs at T+0 (the originator of
|
|
16
|
+
* the ssh-keysign-pwn class of disclosure)
|
|
17
|
+
* Red Hat RHSA RSS — RHEL security advisories at T+1, often before NVD
|
|
18
|
+
* Ubuntu USN RSS — Ubuntu security notices at T+1, often before NVD
|
|
19
|
+
* ZDI advisories — Zero Day Initiative + Pwn2Own disclosures at T+0
|
|
20
|
+
*
|
|
21
|
+
* Behaviour. Each call returns a structured REPORT — not a catalog mutation.
|
|
22
|
+
* Operators consume the report via `exceptd refresh --check-advisories` and
|
|
23
|
+
* decide which advisories warrant a `refresh --advisory <CVE-ID>` auto-import
|
|
24
|
+
* to seed a draft entry. The report is informational; nothing is auto-written
|
|
25
|
+
* to the catalog. This is the conservative-by-default contract: primary-source
|
|
26
|
+
* surfacing must not silently mutate the catalog without operator triage.
|
|
27
|
+
*
|
|
28
|
+
* Output shape:
|
|
29
|
+
* {
|
|
30
|
+
* status: 'ok' | 'partial' | 'unreachable',
|
|
31
|
+
* diffs: [
|
|
32
|
+
* { id: 'CVE-2026-46333', source: 'qualys', advisory_url: '...',
|
|
33
|
+
* disclosed_at: '2026-05-14', title: '...', in_catalog: false }
|
|
34
|
+
* ],
|
|
35
|
+
* summary: '4/4 feeds reachable; 3 new CVE references found'
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* Each diff is read-only — there is no `applyDiff` that writes the catalog.
|
|
39
|
+
* That's by design: a fresh advisory from a primary source has insufficient
|
|
40
|
+
* fields to satisfy Hard Rule #1 (CVSS / KEV / PoC / AI-discovery /
|
|
41
|
+
* active-exploitation / patch-availability). The operator routes the
|
|
42
|
+
* promising ones through `refresh --advisory <CVE-ID>` which goes through
|
|
43
|
+
* the existing GHSA / OSV / NVD enrichment path (those pollers are mature).
|
|
44
|
+
*
|
|
45
|
+
* Cache mode (--from-cache <dir>): expected cache layout is
|
|
46
|
+
* <dir>/advisories/<feed>.xml — caller passes `ctx.cacheDir`.
|
|
47
|
+
* Fixture mode: `ctx.fixtures.advisories = { qualys: '<xml>', ... }`.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
const path = require('path');
|
|
51
|
+
const fs = require('fs');
|
|
52
|
+
|
|
53
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
54
|
+
|
|
55
|
+
// Feed registry. Each entry has a kind (rss | json), a URL, and a parser.
|
|
56
|
+
// Parsers return [{ cve_ids: [...], title, link, published }, ...].
|
|
57
|
+
const FEEDS = [
|
|
58
|
+
{
|
|
59
|
+
name: 'qualys',
|
|
60
|
+
url: 'https://blog.qualys.com/category/vulnerability-research/feed',
|
|
61
|
+
kind: 'rss',
|
|
62
|
+
description: 'Qualys Threat Research Unit blog — originator of high-impact disclosures (ssh-keysign-pwn class)',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'rhsa',
|
|
66
|
+
url: 'https://access.redhat.com/security/data/csaf/v2/advisories/2026/index.txt',
|
|
67
|
+
kind: 'csaf-index',
|
|
68
|
+
description: 'Red Hat CSAF v2 advisory index — RHEL security advisories with NVD-class enrichment at T+1',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'usn',
|
|
72
|
+
url: 'https://ubuntu.com/security/notices/rss.xml',
|
|
73
|
+
kind: 'rss',
|
|
74
|
+
description: 'Ubuntu USN RSS — Ubuntu security notices, typically published 1-2 days post-disclosure',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'zdi',
|
|
78
|
+
url: 'https://www.zerodayinitiative.com/rss/published/',
|
|
79
|
+
kind: 'rss',
|
|
80
|
+
description: 'Zero Day Initiative — vendor-acknowledged advisories from ZDI + Pwn2Own pipeline',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Permissive CVE-ID matcher. The official format is CVE-YYYY-NNNN+ but
|
|
85
|
+
// some feeds embed CVEs in arbitrary surrounding markup AND occasionally
|
|
86
|
+
// emit lowercase "cve-yyyy-nnnn" in URLs or filenames. Case-insensitive
|
|
87
|
+
// match, then uppercase + dedupe in extractCveIds().
|
|
88
|
+
const CVE_RE = /CVE-(?:19|20)\d{2}-\d{4,7}/gi;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract CVE IDs from a string blob. De-duplicates within the blob.
|
|
92
|
+
*/
|
|
93
|
+
function extractCveIds(text) {
|
|
94
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
95
|
+
const matches = text.match(CVE_RE);
|
|
96
|
+
if (!matches) return [];
|
|
97
|
+
return [...new Set(matches.map((s) => s.toUpperCase()))];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Lightweight RSS / Atom parser. Avoids pulling in a dependency for what
|
|
102
|
+
* is effectively `<item>` / `<entry>` extraction + `<title>` / `<link>` /
|
|
103
|
+
* `<pubDate>` / `<published>` / `<description>` / `<content>` text grabs.
|
|
104
|
+
*
|
|
105
|
+
* Returns [{ title, link, published, body }, ...].
|
|
106
|
+
*/
|
|
107
|
+
function parseRssAtom(xml) {
|
|
108
|
+
if (typeof xml !== 'string') return [];
|
|
109
|
+
const items = [];
|
|
110
|
+
// Try Atom <entry>...</entry> first.
|
|
111
|
+
const atomEntryRe = /<entry\b[\s\S]*?<\/entry>/g;
|
|
112
|
+
const rssItemRe = /<item\b[\s\S]*?<\/item>/g;
|
|
113
|
+
const blocks = (xml.match(atomEntryRe) || xml.match(rssItemRe) || []);
|
|
114
|
+
for (const block of blocks) {
|
|
115
|
+
const title = matchInner(block, 'title') || '';
|
|
116
|
+
const link = matchInner(block, 'link') || matchAttr(block, 'link', 'href') || '';
|
|
117
|
+
const published = matchInner(block, 'pubDate') || matchInner(block, 'published') || matchInner(block, 'updated') || '';
|
|
118
|
+
const description = matchInner(block, 'description') || matchInner(block, 'content') || matchInner(block, 'summary') || '';
|
|
119
|
+
items.push({ title: stripCdata(title), link: stripCdata(link), published: stripCdata(published), body: stripCdata(description) });
|
|
120
|
+
}
|
|
121
|
+
return items;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function matchInner(block, tag) {
|
|
125
|
+
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
|
|
126
|
+
const m = block.match(re);
|
|
127
|
+
return m ? m[1].trim() : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function matchAttr(block, tag, attr) {
|
|
131
|
+
const re = new RegExp(`<${tag}[^>]*\\b${attr}=["']([^"']+)["']`, 'i');
|
|
132
|
+
const m = block.match(re);
|
|
133
|
+
return m ? m[1] : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stripCdata(s) {
|
|
137
|
+
if (typeof s !== 'string') return '';
|
|
138
|
+
return s.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* CSAF index parser — Red Hat ships a plain-text index of advisory JSON
|
|
143
|
+
* files under data/csaf/v2/advisories/YYYY/index.txt. Each line is a
|
|
144
|
+
* relative filename. We don't fetch the per-advisory JSON in v0.13.1
|
|
145
|
+
* (would blow the polling budget); we surface the advisory IDs that
|
|
146
|
+
* mention CVE-YYYY-NNNN inline.
|
|
147
|
+
*/
|
|
148
|
+
function parseCsafIndex(text) {
|
|
149
|
+
if (typeof text !== 'string') return [];
|
|
150
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
151
|
+
return lines.map((line) => {
|
|
152
|
+
const cves = extractCveIds(line);
|
|
153
|
+
return { title: line.trim(), link: '', published: '', body: '', cves_from_filename: cves };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fetch a feed body. In fixture / cache modes, read from disk.
|
|
159
|
+
*/
|
|
160
|
+
async function fetchFeed(feed, ctx) {
|
|
161
|
+
if (ctx.fixtures && ctx.fixtures.advisories && ctx.fixtures.advisories[feed.name]) {
|
|
162
|
+
return { ok: true, body: ctx.fixtures.advisories[feed.name] };
|
|
163
|
+
}
|
|
164
|
+
if (ctx.cacheDir) {
|
|
165
|
+
const ext = feed.kind === 'csaf-index' ? '.txt' : '.xml';
|
|
166
|
+
const p = path.join(ctx.cacheDir, 'advisories', `${feed.name}${ext}`);
|
|
167
|
+
if (!fs.existsSync(p)) return { ok: false, error: `cache miss: ${p}` };
|
|
168
|
+
return { ok: true, body: fs.readFileSync(p, 'utf8') };
|
|
169
|
+
}
|
|
170
|
+
if (typeof fetch !== 'function') return { ok: false, error: 'fetch() not available — Node 18+ required' };
|
|
171
|
+
try {
|
|
172
|
+
const ac = new AbortController();
|
|
173
|
+
const timer = setTimeout(() => ac.abort(), 8000);
|
|
174
|
+
const r = await fetch(feed.url, { signal: ac.signal, headers: { 'User-Agent': 'exceptd-advisories-poller/0.13.1 (+https://exceptd.com)' } });
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
if (!r.ok) return { ok: false, error: `HTTP ${r.status}` };
|
|
177
|
+
return { ok: true, body: await r.text() };
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { ok: false, error: e.message || String(e) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Walk one feed: fetch, parse, extract CVE IDs, compare to local catalog.
|
|
185
|
+
* Returns { diffs, errors, status }.
|
|
186
|
+
*/
|
|
187
|
+
async function checkFeed(feed, ctx) {
|
|
188
|
+
const res = await fetchFeed(feed, ctx);
|
|
189
|
+
if (!res.ok) return { diffs: [], errors: 1, status: 'unreachable', _why: res.error };
|
|
190
|
+
let items;
|
|
191
|
+
if (feed.kind === 'csaf-index') {
|
|
192
|
+
items = parseCsafIndex(res.body);
|
|
193
|
+
// Flatten cves_from_filename onto cve_ids field uniformly.
|
|
194
|
+
items = items.map((it) => ({ ...it, cve_ids: it.cves_from_filename || [] }));
|
|
195
|
+
} else {
|
|
196
|
+
items = parseRssAtom(res.body);
|
|
197
|
+
items = items.map((it) => ({ ...it, cve_ids: extractCveIds(`${it.title} ${it.body} ${it.link}`) }));
|
|
198
|
+
}
|
|
199
|
+
const diffs = [];
|
|
200
|
+
for (const it of items) {
|
|
201
|
+
for (const cveId of it.cve_ids) {
|
|
202
|
+
const inCatalog = !!ctx.cveCatalog[cveId];
|
|
203
|
+
if (!inCatalog) {
|
|
204
|
+
diffs.push({
|
|
205
|
+
id: cveId,
|
|
206
|
+
source: feed.name,
|
|
207
|
+
advisory_url: it.link || feed.url,
|
|
208
|
+
disclosed_at: it.published || null,
|
|
209
|
+
title: it.title.slice(0, 200),
|
|
210
|
+
in_catalog: false,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { diffs, errors: 0, status: 'ok' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The exported SOURCE definition, matching the shape ALL_SOURCES expects.
|
|
220
|
+
*/
|
|
221
|
+
const ADVISORIES_SOURCE = {
|
|
222
|
+
name: 'advisories',
|
|
223
|
+
description: 'Primary-source advisory feeds (Qualys TRU, Red Hat RHSA, Ubuntu USN, Zero Day Initiative / ZDI) — surfaces CVE IDs disclosed at T+0 to T+1 that lag NVD enrichment. Report-only — does not auto-write the catalog.',
|
|
224
|
+
applies_to: 'data/cve-catalog.json',
|
|
225
|
+
async fetchDiff(ctx) {
|
|
226
|
+
const results = await Promise.all(FEEDS.map((feed) => checkFeed(feed, ctx)));
|
|
227
|
+
const allDiffs = [];
|
|
228
|
+
let unreachable = 0;
|
|
229
|
+
for (const r of results) {
|
|
230
|
+
allDiffs.push(...r.diffs);
|
|
231
|
+
if (r.status === 'unreachable') unreachable++;
|
|
232
|
+
}
|
|
233
|
+
// Deduplicate by CVE-ID across feeds — multiple advisories for the
|
|
234
|
+
// same CVE collapse to one entry with sources[] array of contributing
|
|
235
|
+
// feed names.
|
|
236
|
+
const byCve = new Map();
|
|
237
|
+
for (const d of allDiffs) {
|
|
238
|
+
if (!byCve.has(d.id)) {
|
|
239
|
+
byCve.set(d.id, { ...d, sources: [d.source], advisory_urls: [d.advisory_url] });
|
|
240
|
+
} else {
|
|
241
|
+
const existing = byCve.get(d.id);
|
|
242
|
+
if (!existing.sources.includes(d.source)) existing.sources.push(d.source);
|
|
243
|
+
if (!existing.advisory_urls.includes(d.advisory_url)) existing.advisory_urls.push(d.advisory_url);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const diffs = Array.from(byCve.values()).map((d) => {
|
|
247
|
+
delete d.source;
|
|
248
|
+
delete d.advisory_url;
|
|
249
|
+
return d;
|
|
250
|
+
});
|
|
251
|
+
const status =
|
|
252
|
+
unreachable === 0 ? 'ok' :
|
|
253
|
+
unreachable === FEEDS.length ? 'unreachable' : 'partial';
|
|
254
|
+
return {
|
|
255
|
+
status,
|
|
256
|
+
diffs,
|
|
257
|
+
errors: unreachable,
|
|
258
|
+
summary: `${FEEDS.length - unreachable}/${FEEDS.length} feeds reachable; ${diffs.length} new CVE references found across primary advisory sources`,
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
// Report-only: no applyDiff. Operators route promising CVE IDs through
|
|
262
|
+
// `exceptd refresh --advisory <CVE-ID>` (GHSA / OSV / NVD enrichment).
|
|
263
|
+
applyDiff(_ctx, _diffs) {
|
|
264
|
+
return {
|
|
265
|
+
updated: 0,
|
|
266
|
+
added: 0,
|
|
267
|
+
drift_updated: 0,
|
|
268
|
+
errors: [],
|
|
269
|
+
note: 'ADVISORIES_SOURCE is report-only. Route promising IDs through `exceptd refresh --advisory <CVE-ID>` to auto-import a draft via the GHSA / OSV / NVD enrichment pipeline.',
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
ADVISORIES_SOURCE,
|
|
276
|
+
// Exposed for tests + future schedule-agent reuse:
|
|
277
|
+
FEEDS,
|
|
278
|
+
extractCveIds,
|
|
279
|
+
parseRssAtom,
|
|
280
|
+
parseCsafIndex,
|
|
281
|
+
};
|