@blamejs/exceptd-skills 0.9.4 → 0.10.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/AGENTS.md +45 -0
- package/CHANGELOG.md +131 -0
- package/README.md +30 -5
- package/bin/exceptd.js +403 -1
- package/data/_indexes/_meta.json +3 -3
- package/data/_indexes/currency.json +138 -138
- package/data/playbooks/ai-api.json +763 -0
- package/data/playbooks/containers.json +766 -0
- package/data/playbooks/cred-stores.json +715 -0
- package/data/playbooks/crypto.json +726 -0
- package/data/playbooks/framework.json +725 -0
- package/data/playbooks/hardening.json +672 -0
- package/data/playbooks/kernel.json +549 -0
- package/data/playbooks/mcp.json +727 -0
- package/data/playbooks/runtime.json +649 -0
- package/data/playbooks/sbom.json +893 -0
- package/data/playbooks/secrets.json +690 -0
- package/lib/cross-ref-api.js +224 -0
- package/lib/playbook-runner.js +826 -0
- package/lib/schemas/playbook.schema.json +652 -0
- package/lib/verify.js +45 -0
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/dispatcher.js +13 -1
- package/orchestrator/index.js +119 -5
- package/orchestrator/pipeline.js +8 -2
- package/orchestrator/scanner.js +191 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/scripts/builders/currency.js +5 -3
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-reference API — pure read-only knowledge queries over data/ + data/_indexes/.
|
|
5
|
+
*
|
|
6
|
+
* This is the knowledge layer the host AI calls into during the ANALYZE phase
|
|
7
|
+
* of every directive. No probes, no shellouts, no network. Every function takes
|
|
8
|
+
* an identifier and returns correlated catalog entries from CVE / CWE / ATLAS /
|
|
9
|
+
* ATT&CK / D3FEND / framework-gaps / global-frameworks / RFC / zero-day-lessons,
|
|
10
|
+
* plus pre-computed indexes (xref, chains, recipes, theater-fingerprints).
|
|
11
|
+
*
|
|
12
|
+
* Catalogs are loaded lazily and cached for the lifetime of the process.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const ROOT = path.join(__dirname, '..');
|
|
19
|
+
const DATA_DIR = process.env.EXCEPTD_DATA_DIR || path.join(ROOT, 'data');
|
|
20
|
+
const INDEX_DIR = path.join(DATA_DIR, '_indexes');
|
|
21
|
+
|
|
22
|
+
const _cache = new Map();
|
|
23
|
+
|
|
24
|
+
function loadCatalog(filename) {
|
|
25
|
+
if (_cache.has(filename)) return _cache.get(filename);
|
|
26
|
+
const full = path.join(DATA_DIR, filename);
|
|
27
|
+
if (!fs.existsSync(full)) {
|
|
28
|
+
_cache.set(filename, {});
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
32
|
+
_cache.set(filename, parsed);
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadIndex(filename) {
|
|
37
|
+
if (_cache.has('idx:' + filename)) return _cache.get('idx:' + filename);
|
|
38
|
+
const full = path.join(INDEX_DIR, filename);
|
|
39
|
+
if (!fs.existsSync(full)) {
|
|
40
|
+
_cache.set('idx:' + filename, {});
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
44
|
+
_cache.set('idx:' + filename, parsed);
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function entries(catalog) {
|
|
49
|
+
return Object.entries(catalog).filter(([k]) => !k.startsWith('_'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- public API ---
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Full correlation for a CVE ID. Returns the catalog entry plus everything
|
|
56
|
+
* that references it across skills, framework gaps, theater fingerprints,
|
|
57
|
+
* recipes, and zero-day lessons.
|
|
58
|
+
*/
|
|
59
|
+
function byCve(cveId) {
|
|
60
|
+
const catalog = loadCatalog('cve-catalog.json');
|
|
61
|
+
const entry = catalog[cveId];
|
|
62
|
+
if (!entry) return { found: false, cve_id: cveId };
|
|
63
|
+
|
|
64
|
+
const xref = loadIndex('xref.json');
|
|
65
|
+
const recipes = loadIndex('recipes.json');
|
|
66
|
+
const theaterFp = loadIndex('theater-fingerprints.json');
|
|
67
|
+
const gaps = loadCatalog('framework-control-gaps.json');
|
|
68
|
+
const lessons = loadCatalog('zeroday-lessons.json');
|
|
69
|
+
|
|
70
|
+
const skills = (xref[cveId] || xref.cves?.[cveId] || []).slice();
|
|
71
|
+
const matchingRecipes = entries(recipes).filter(([, r]) =>
|
|
72
|
+
Array.isArray(r.triggered_by) && r.triggered_by.includes(cveId)
|
|
73
|
+
).map(([id]) => id);
|
|
74
|
+
const theater = entries(theaterFp).filter(([, t]) =>
|
|
75
|
+
Array.isArray(t.cve_refs) && t.cve_refs.includes(cveId)
|
|
76
|
+
).map(([id, t]) => ({ id, distinguisher: t.distinguisher || t.test }));
|
|
77
|
+
const framework_gaps = entries(gaps).filter(([, g]) =>
|
|
78
|
+
Array.isArray(g.cve_refs) && g.cve_refs.includes(cveId)
|
|
79
|
+
).map(([id, g]) => ({ id, framework: g.framework, control: g.control, status: g.status }));
|
|
80
|
+
const lessons_learned = entries(lessons).filter(([, l]) =>
|
|
81
|
+
Array.isArray(l.cve_refs) && l.cve_refs.includes(cveId)
|
|
82
|
+
).map(([id]) => id);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
found: true,
|
|
86
|
+
cve_id: cveId,
|
|
87
|
+
entry,
|
|
88
|
+
rwep_score: entry.rwep_score,
|
|
89
|
+
cisa_kev: !!entry.cisa_kev,
|
|
90
|
+
active_exploitation: entry.active_exploitation,
|
|
91
|
+
ai_discovered: !!entry.ai_discovered,
|
|
92
|
+
atlas_refs: entry.atlas_refs || [],
|
|
93
|
+
attack_refs: entry.attack_refs || [],
|
|
94
|
+
skills,
|
|
95
|
+
framework_gaps,
|
|
96
|
+
theater_tests: theater,
|
|
97
|
+
recipes: matchingRecipes,
|
|
98
|
+
zeroday_lessons: lessons_learned
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function byCwe(cweId) {
|
|
103
|
+
const catalog = loadCatalog('cwe-catalog.json');
|
|
104
|
+
const entry = catalog[cweId];
|
|
105
|
+
if (!entry) return { found: false, cwe_id: cweId };
|
|
106
|
+
const xref = loadIndex('xref.json');
|
|
107
|
+
const skills = (xref.cwes?.[cweId] || []).slice();
|
|
108
|
+
const relatedCves = entries(loadCatalog('cve-catalog.json'))
|
|
109
|
+
.filter(([, c]) => Array.isArray(c.cwe_refs) && c.cwe_refs.includes(cweId))
|
|
110
|
+
.map(([id]) => id);
|
|
111
|
+
return { found: true, cwe_id: cweId, entry, skills, related_cves: relatedCves };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function byTtp(ttpId) {
|
|
115
|
+
const atlas = loadCatalog('atlas-ttps.json');
|
|
116
|
+
const xref = loadIndex('xref.json');
|
|
117
|
+
const entry = atlas[ttpId] || null;
|
|
118
|
+
const skills = (xref.ttps?.[ttpId] || []).slice();
|
|
119
|
+
const relatedCves = entries(loadCatalog('cve-catalog.json'))
|
|
120
|
+
.filter(([, c]) =>
|
|
121
|
+
(Array.isArray(c.atlas_refs) && c.atlas_refs.includes(ttpId)) ||
|
|
122
|
+
(Array.isArray(c.attack_refs) && c.attack_refs.includes(ttpId))
|
|
123
|
+
)
|
|
124
|
+
.map(([id]) => id);
|
|
125
|
+
const d3fend = entries(loadCatalog('d3fend-catalog.json'))
|
|
126
|
+
.filter(([, d]) => Array.isArray(d.counters) && d.counters.includes(ttpId))
|
|
127
|
+
.map(([id]) => id);
|
|
128
|
+
return { found: !!entry, ttp_id: ttpId, entry, skills, related_cves: relatedCves, d3fend_countermeasures: d3fend };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function bySkill(skillName) {
|
|
132
|
+
const xref = loadIndex('xref.json');
|
|
133
|
+
const summary = loadIndex('summary-cards.json');
|
|
134
|
+
const card = summary[skillName] || summary.skills?.[skillName] || null;
|
|
135
|
+
const cveRefs = Object.entries(xref.cves || {})
|
|
136
|
+
.filter(([, skills]) => Array.isArray(skills) && skills.includes(skillName))
|
|
137
|
+
.map(([cve]) => cve);
|
|
138
|
+
const ttpRefs = Object.entries(xref.ttps || {})
|
|
139
|
+
.filter(([, skills]) => Array.isArray(skills) && skills.includes(skillName))
|
|
140
|
+
.map(([ttp]) => ttp);
|
|
141
|
+
return { skill: skillName, summary_card: card, cve_refs: cveRefs, ttp_refs: ttpRefs };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function byFramework(frameworkId, scenario) {
|
|
145
|
+
const gaps = loadCatalog('framework-control-gaps.json');
|
|
146
|
+
const global = loadCatalog('global-frameworks.json');
|
|
147
|
+
const matching = entries(gaps).filter(([, g]) => {
|
|
148
|
+
if (g.framework !== frameworkId && g.framework !== 'ALL') return false;
|
|
149
|
+
if (scenario && Array.isArray(g.scenarios) && !g.scenarios.includes(scenario)) return false;
|
|
150
|
+
return true;
|
|
151
|
+
}).map(([id, g]) => ({ id, ...g }));
|
|
152
|
+
const fwMeta = global[frameworkId] || null;
|
|
153
|
+
return { framework: frameworkId, scenario: scenario || null, framework_meta: fwMeta, gaps: matching, gap_count: matching.length };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Given a detected signal shape, return recipes whose triggered_by matches
|
|
158
|
+
* any of the signals. Used by the analyze phase to chain into multi-step
|
|
159
|
+
* workflows the agent should walk through next.
|
|
160
|
+
*/
|
|
161
|
+
function recipesFor(signals) {
|
|
162
|
+
const recipes = loadIndex('recipes.json');
|
|
163
|
+
const sigSet = new Set(signals);
|
|
164
|
+
return entries(recipes)
|
|
165
|
+
.filter(([, r]) => Array.isArray(r.triggered_by) && r.triggered_by.some(t => sigSet.has(t)))
|
|
166
|
+
.map(([id, r]) => ({ id, name: r.name, skills: r.skills, steps: r.steps }));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Theater-fingerprint lookup — given a finding shape, return the specific test
|
|
171
|
+
* that distinguishes paper compliance from actual security (AGENTS.md Hard
|
|
172
|
+
* Rule #6). Drives the validate phase when emit.theater_check = true.
|
|
173
|
+
*/
|
|
174
|
+
function theaterTestsFor({ cveIds = [], frameworkIds = [], skillIds = [] }) {
|
|
175
|
+
const fp = loadIndex('theater-fingerprints.json');
|
|
176
|
+
const matches = [];
|
|
177
|
+
for (const [id, t] of entries(fp)) {
|
|
178
|
+
const cveMatch = cveIds.some(c => (t.cve_refs || []).includes(c));
|
|
179
|
+
const fwMatch = frameworkIds.some(f => (t.framework_refs || []).includes(f));
|
|
180
|
+
const skillMatch = skillIds.some(s => (t.skill_refs || []).includes(s));
|
|
181
|
+
if (cveMatch || fwMatch || skillMatch) {
|
|
182
|
+
matches.push({ id, distinguisher: t.distinguisher || t.test, applies_when: t.applies_when });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return matches;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Global-first framework correlation — given a finding's CVE/TTP set, return
|
|
190
|
+
* the relevant gaps across EU (NIS2/DORA/EU AI Act) + UK (CAF) + AU (ISM /
|
|
191
|
+
* Essential 8) + ISO 27001:2022 + NIST. Satisfies AGENTS.md Hard Rule #5.
|
|
192
|
+
*/
|
|
193
|
+
function globalFrameworkContext({ cveIds = [], ttpIds = [] }) {
|
|
194
|
+
const gaps = loadCatalog('framework-control-gaps.json');
|
|
195
|
+
const cveSet = new Set(cveIds);
|
|
196
|
+
const ttpSet = new Set(ttpIds);
|
|
197
|
+
const grouped = {};
|
|
198
|
+
for (const [id, g] of entries(gaps)) {
|
|
199
|
+
const cveHit = (g.cve_refs || []).some(c => cveSet.has(c));
|
|
200
|
+
const ttpHit = (g.ttp_refs || []).some(t => ttpSet.has(t));
|
|
201
|
+
if (!cveHit && !ttpHit) continue;
|
|
202
|
+
const fw = g.framework || 'unspecified';
|
|
203
|
+
grouped[fw] = grouped[fw] || [];
|
|
204
|
+
grouped[fw].push({ id, control: g.control, status: g.status, scenarios: g.scenarios });
|
|
205
|
+
}
|
|
206
|
+
return grouped;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function clearCache() { _cache.clear(); }
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
byCve,
|
|
213
|
+
byCwe,
|
|
214
|
+
byTtp,
|
|
215
|
+
bySkill,
|
|
216
|
+
byFramework,
|
|
217
|
+
recipesFor,
|
|
218
|
+
theaterTestsFor,
|
|
219
|
+
globalFrameworkContext,
|
|
220
|
+
clearCache,
|
|
221
|
+
// Lower-level access (engine uses these directly)
|
|
222
|
+
_loadCatalog: loadCatalog,
|
|
223
|
+
_loadIndex: loadIndex,
|
|
224
|
+
};
|