@blamejs/exceptd-skills 0.10.1 → 0.10.3
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 +51 -0
- package/CHANGELOG.md +72 -0
- package/bin/exceptd.js +468 -37
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/crypto-codebase.json +1387 -0
- package/data/playbooks/kernel.json +1 -1
- package/data/playbooks/library-author.json +1792 -0
- package/lib/framework-gap.js +17 -1
- package/lib/playbook-runner.js +146 -11
- package/lib/prefetch.js +9 -1
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/index.js +98 -8
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
- package/sources/README.md +170 -0
- package/sources/validators/atlas-validator.js +158 -0
- package/sources/validators/cve-validator.js +277 -0
- package/sources/validators/index.js +86 -0
- package/sources/validators/rfc-validator.js +165 -0
- package/sources/validators/version-pin-validator.js +144 -0
package/orchestrator/index.js
CHANGED
|
@@ -161,8 +161,13 @@ Examples:
|
|
|
161
161
|
// --- command implementations ---
|
|
162
162
|
|
|
163
163
|
async function runScan() {
|
|
164
|
-
|
|
164
|
+
const jsonOut = process.argv.includes('--json');
|
|
165
|
+
if (!jsonOut) console.log('[orchestrator] Scanning environment...\n');
|
|
165
166
|
const result = await scan();
|
|
167
|
+
if (jsonOut) {
|
|
168
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
166
171
|
|
|
167
172
|
console.log('Host:', JSON.stringify(result.host, null, 2));
|
|
168
173
|
console.log('\nFindings by domain:');
|
|
@@ -190,10 +195,16 @@ async function runScan() {
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
async function runDispatch() {
|
|
193
|
-
|
|
198
|
+
const jsonOut = process.argv.includes('--json');
|
|
199
|
+
if (!jsonOut) console.log('[orchestrator] Scanning then dispatching...\n');
|
|
194
200
|
const scanResult = await scan();
|
|
195
201
|
const plan = dispatch(scanResult.findings);
|
|
196
202
|
|
|
203
|
+
if (jsonOut) {
|
|
204
|
+
process.stdout.write(JSON.stringify({ scan: scanResult, dispatch: plan }) + '\n');
|
|
205
|
+
return plan;
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
console.log(`Dispatch plan — ${plan.plan.length} skills to invoke:\n`);
|
|
198
209
|
|
|
199
210
|
for (const item of plan.plan) {
|
|
@@ -233,7 +244,8 @@ function runSkillContext(skillName) {
|
|
|
233
244
|
|
|
234
245
|
const context = getSkillContext(skillName);
|
|
235
246
|
if (!context) {
|
|
236
|
-
|
|
247
|
+
// Unified error shape across the CLI surface — see v0.10.3 bug #18.
|
|
248
|
+
process.stderr.write(JSON.stringify({ ok: false, error: `Skill not found: ${skillName}`, verb: "skill", hint: "Run `exceptd plan` or check skills/ for available skill IDs." }) + "\n");
|
|
237
249
|
process.exit(1);
|
|
238
250
|
}
|
|
239
251
|
|
|
@@ -266,9 +278,15 @@ function runPipeline(triggerType, payload) {
|
|
|
266
278
|
}
|
|
267
279
|
|
|
268
280
|
function runCurrency() {
|
|
281
|
+
const jsonOut = process.argv.includes('--json');
|
|
269
282
|
const result = runCurrencyNow();
|
|
270
283
|
const { currency_report, action_required, critical_count } = currencyCheck();
|
|
271
284
|
|
|
285
|
+
if (jsonOut) {
|
|
286
|
+
process.stdout.write(JSON.stringify({ currency_report, action_required, critical_count, generated_at: new Date().toISOString() }) + '\n');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
console.log(`\nSkill currency check — ${new Date().toISOString()}\n`);
|
|
273
291
|
console.log('Score | Days | Skill');
|
|
274
292
|
console.log('------|------|-----');
|
|
@@ -383,13 +401,33 @@ async function runValidateCves(rawArgs = []) {
|
|
|
383
401
|
process.exit(2);
|
|
384
402
|
}
|
|
385
403
|
|
|
386
|
-
|
|
404
|
+
// --since <ISO|YYYY-MM-DD>: scope-limit validation to CVEs whose
|
|
405
|
+
// last_updated (or cisa_kev_date when missing) is on or after the given
|
|
406
|
+
// date. Cuts upstream calls for fleet operators running cron jobs.
|
|
407
|
+
let sinceDate = null;
|
|
408
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
409
|
+
if (rawArgs[i] === '--since' && rawArgs[i + 1]) sinceDate = rawArgs[i + 1];
|
|
410
|
+
else if (rawArgs[i].startsWith('--since=')) sinceDate = rawArgs[i].slice('--since='.length);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let cveIds = Object.keys(catalog).filter(k => /^CVE-\d{4}-\d{4,7}$/.test(k));
|
|
414
|
+
if (sinceDate) {
|
|
415
|
+
const since = sinceDate.length === 10 ? `${sinceDate}T00:00:00Z` : sinceDate;
|
|
416
|
+
const before = cveIds.length;
|
|
417
|
+
cveIds = cveIds.filter(id => {
|
|
418
|
+
const e = catalog[id];
|
|
419
|
+
const stamp = e.last_updated || e.cisa_kev_date || e.first_seen;
|
|
420
|
+
if (!stamp) return false;
|
|
421
|
+
return stamp >= since;
|
|
422
|
+
});
|
|
423
|
+
console.log(`[validate-cves] --since ${sinceDate} filtered ${before} → ${cveIds.length} CVE(s).`);
|
|
424
|
+
}
|
|
387
425
|
|
|
388
426
|
console.log(`\nCVE Validation — ${new Date().toISOString()}`);
|
|
389
427
|
const modeStr = offline
|
|
390
428
|
? 'offline (local view only)'
|
|
391
429
|
: (cacheDir ? `live with cache (${path.relative(path.join(__dirname, '..'), cacheDir)})` : 'live (NVD + CISA KEV)');
|
|
392
|
-
console.log(`${cveIds.length} CVEs in catalog. Mode: ${modeStr}`);
|
|
430
|
+
console.log(`${cveIds.length} CVEs in catalog. Mode: ${modeStr}${sinceDate ? ` · since=${sinceDate}` : ''}`);
|
|
393
431
|
console.log(`Fail-on-drift: ${noFail ? 'disabled' : 'enabled'}\n`);
|
|
394
432
|
|
|
395
433
|
// --- Header (fixed-width; works with the existing currency command's style)
|
|
@@ -436,7 +474,23 @@ async function runValidateCves(rawArgs = []) {
|
|
|
436
474
|
// is set. Cache-resolved CVEs short-circuit the network fetch; missing
|
|
437
475
|
// entries fall through to the live validator. Both paths produce the
|
|
438
476
|
// same ValidationResult shape.
|
|
439
|
-
|
|
477
|
+
//
|
|
478
|
+
// Graceful fallback when sources/validators isn't shipped (matches the
|
|
479
|
+
// pattern validate-rfcs uses below). Pre-v0.10.3 this crashed with
|
|
480
|
+
// MODULE_NOT_FOUND in installed npm packages because sources/ wasn't
|
|
481
|
+
// in the files allowlist.
|
|
482
|
+
let validateAllCves;
|
|
483
|
+
try {
|
|
484
|
+
({ validateAllCves } = require('../sources/validators'));
|
|
485
|
+
} catch (e) {
|
|
486
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
487
|
+
console.warn('[validate-cves] validator module unavailable (MODULE_NOT_FOUND); falling back to offline mode.');
|
|
488
|
+
console.log(`\n[validate-cves] offline mode (forced by missing validators) — ${cveIds.length} entries listed from local catalog.`);
|
|
489
|
+
process.exit(0);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
throw e;
|
|
493
|
+
}
|
|
440
494
|
let report;
|
|
441
495
|
if (cacheDir && fs.existsSync(cacheDir)) {
|
|
442
496
|
report = await validateAllCvesPreferCache(catalog, cacheDir);
|
|
@@ -568,13 +622,29 @@ async function runValidateRfcs(rawArgs = []) {
|
|
|
568
622
|
process.exit(2);
|
|
569
623
|
}
|
|
570
624
|
|
|
571
|
-
|
|
625
|
+
// --since <ISO|YYYY-MM-DD>: scope-limit (parity with validate-cves).
|
|
626
|
+
let sinceDate = null;
|
|
627
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
628
|
+
if (rawArgs[i] === '--since' && rawArgs[i + 1]) sinceDate = rawArgs[i + 1];
|
|
629
|
+
else if (rawArgs[i].startsWith('--since=')) sinceDate = rawArgs[i].slice('--since='.length);
|
|
630
|
+
}
|
|
631
|
+
let ids = Object.keys(refs).filter(k => !k.startsWith('_'));
|
|
632
|
+
if (sinceDate) {
|
|
633
|
+
const since = sinceDate.length === 10 ? `${sinceDate}T00:00:00Z` : sinceDate;
|
|
634
|
+
const before = ids.length;
|
|
635
|
+
ids = ids.filter(id => {
|
|
636
|
+
const e = refs[id];
|
|
637
|
+
const stamp = e.last_verified || e.published || e.last_updated;
|
|
638
|
+
return stamp && stamp >= since;
|
|
639
|
+
});
|
|
640
|
+
console.log(`[validate-rfcs] --since ${sinceDate} filtered ${before} → ${ids.length} entry(ies).`);
|
|
641
|
+
}
|
|
572
642
|
|
|
573
643
|
console.log(`\nRFC Validation — ${new Date().toISOString()}`);
|
|
574
644
|
const modeStr = offline
|
|
575
645
|
? 'offline (local view only)'
|
|
576
646
|
: (cacheDir ? `live with cache (${path.relative(path.join(__dirname, '..'), cacheDir)})` : 'live (IETF Datatracker)');
|
|
577
|
-
console.log(`${ids.length} RFC / draft entries in catalog. Mode: ${modeStr}`);
|
|
647
|
+
console.log(`${ids.length} RFC / draft entries in catalog. Mode: ${modeStr}${sinceDate ? ` · since=${sinceDate}` : ''}`);
|
|
578
648
|
console.log(`Fail-on-drift: ${noFail ? 'disabled' : 'enabled'}\n`);
|
|
579
649
|
|
|
580
650
|
const header = 'ID | Status | Errata | Last verified | Live status';
|
|
@@ -753,6 +823,26 @@ function runWatchlist(rawArgs = []) {
|
|
|
753
823
|
}
|
|
754
824
|
}
|
|
755
825
|
|
|
826
|
+
const jsonOut = rawArgs.includes('--json');
|
|
827
|
+
if (jsonOut) {
|
|
828
|
+
const out = {
|
|
829
|
+
generated_at: new Date().toISOString(),
|
|
830
|
+
skills_scanned: skills.length,
|
|
831
|
+
parse_errors: parseErrors,
|
|
832
|
+
mode: byskill ? 'by-skill' : 'by-item',
|
|
833
|
+
};
|
|
834
|
+
if (byskill) {
|
|
835
|
+
out.by_skill = Object.fromEntries([...skillToItems.entries()]
|
|
836
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
837
|
+
.map(([k, v]) => [k, v]));
|
|
838
|
+
} else {
|
|
839
|
+
out.by_item = Object.fromEntries([...itemToSkills.entries()]
|
|
840
|
+
.sort(([a], [b]) => a.localeCompare(b)));
|
|
841
|
+
}
|
|
842
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
756
846
|
console.log(`\nForward-Watch Aggregator — ${new Date().toISOString()}`);
|
|
757
847
|
console.log(`Skills scanned: ${skills.length} parse errors: ${parseErrors}`);
|
|
758
848
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/exceptd-skills",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"lib/",
|
|
49
49
|
"orchestrator/",
|
|
50
50
|
"scripts/",
|
|
51
|
+
"sources/validators/",
|
|
51
52
|
"vendor/",
|
|
52
53
|
"agents/",
|
|
53
54
|
"data/",
|
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:9c80f3b8-1fb8-46a8-b68f-e1b6a0ddedb7",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-
|
|
7
|
+
"timestamp": "2026-05-12T14:06:23.001Z",
|
|
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.10.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.10.3",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.10.
|
|
19
|
+
"version": "0.10.3",
|
|
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.10.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.10.3",
|
|
29
29
|
"externalReferences": [
|
|
30
30
|
{
|
|
31
31
|
"type": "distribution",
|
|
32
|
-
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.10.
|
|
32
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.10.3"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"type": "vcs",
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Sources
|
|
2
|
+
|
|
3
|
+
The sources directory is the data quality gate for exceptd Security. Every claim in every skill must trace to a primary source. Bad data in produces bad analysis out — this directory makes source integrity a first-class concern.
|
|
4
|
+
|
|
5
|
+
## The Problem: Data Corruption in Security Intelligence
|
|
6
|
+
|
|
7
|
+
Security intelligence has several common failure modes:
|
|
8
|
+
- **Stale data**: A CVE is marked as "no public PoC" when a PoC went public six months ago
|
|
9
|
+
- **Misattribution**: A CVSS score copied from a secondary source that applied the wrong vector
|
|
10
|
+
- **Fabricated details**: AI-summarized threat intel that introduced plausible-but-wrong specifics
|
|
11
|
+
- **Framework version drift**: A control ID that changed in a framework revision but wasn't updated in skills
|
|
12
|
+
- **Dead links**: Source URLs that return 404 — removing the ability to verify
|
|
13
|
+
|
|
14
|
+
The sources system prevents these failures by:
|
|
15
|
+
1. Maintaining a registry of authoritative primary sources per data type
|
|
16
|
+
2. Providing validators that check data against primary sources
|
|
17
|
+
3. Tracking source verification dates and flagging stale verifications
|
|
18
|
+
4. Making multi-agent research verifiable and auditable
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Directory Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
sources/
|
|
26
|
+
├── README.md # This file
|
|
27
|
+
├── index.json # Source registry — authoritative sources per data type
|
|
28
|
+
├── SOURCES.md # Guide for adding and verifying sources
|
|
29
|
+
├── validators/
|
|
30
|
+
│ ├── cve-validator.js # Cross-check CVE data against NVD API
|
|
31
|
+
│ ├── kev-validator.js # Verify CISA KEV status against official feed
|
|
32
|
+
│ ├── atlas-validator.js # Verify ATLAS TTP IDs against mitre.org
|
|
33
|
+
│ └── framework-validator.js # Verify framework control IDs
|
|
34
|
+
└── feeds/
|
|
35
|
+
├── cisa-kev-snapshot.json # Snapshot of CISA KEV at last verification
|
|
36
|
+
├── atlas-version.json # Current ATLAS version metadata
|
|
37
|
+
└── nvd-recent.json # Recent NVD entries (last 30 days)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Primary Sources by Data Type
|
|
43
|
+
|
|
44
|
+
### CVE Data
|
|
45
|
+
|
|
46
|
+
| Field | Authoritative Source | Update Frequency |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| CVSS score + vector | NVD (nvd.nist.gov/vuln/detail/CVE-XXXX) | On NVD analysis |
|
|
49
|
+
| CISA KEV status | CISA KEV catalog (cisa.gov/known-exploited-vulnerabilities-catalog) | Real-time feed |
|
|
50
|
+
| PoC availability | NVD references + researcher advisories | Monitor CVE references |
|
|
51
|
+
| Active exploitation | CISA KEV, threat intelligence, incident reports | Monitor |
|
|
52
|
+
| Affected versions | Vendor advisory (Red Hat, Ubuntu, etc.) | On vendor advisory |
|
|
53
|
+
| Patch availability | Vendor advisory | On vendor advisory |
|
|
54
|
+
| Live patch support | kpatch.com, ubuntu.com/security/livepatch, suse.com/products/live-patching | On vendor announcement |
|
|
55
|
+
|
|
56
|
+
**Never use as primary source:** Wikipedia, news articles, blog posts, AI-generated summaries, secondary aggregators without NVD cross-reference.
|
|
57
|
+
|
|
58
|
+
### ATLAS TTPs
|
|
59
|
+
|
|
60
|
+
| Field | Authoritative Source |
|
|
61
|
+
|---|---|
|
|
62
|
+
| TTP ID | atlas.mitre.org (canonical IDs may change between versions) |
|
|
63
|
+
| TTP name | atlas.mitre.org/techniques/ |
|
|
64
|
+
| TTP version | atlas.mitre.org/resources/changelog |
|
|
65
|
+
|
|
66
|
+
**ATLAS version pinning:** All skills reference a specific ATLAS version. When ATLAS updates, TTP IDs must be re-verified. The `atlas-validator.js` checks all skill `atlas_refs` against the current published ATLAS.
|
|
67
|
+
|
|
68
|
+
### Framework Controls
|
|
69
|
+
|
|
70
|
+
| Framework | Authoritative Source |
|
|
71
|
+
|---|---|
|
|
72
|
+
| NIST 800-53 Rev 5 | csrc.nist.gov/publications/detail/sp/800-53/rev-5/final |
|
|
73
|
+
| ISO 27001:2022 | iso.org/standard/27001 (requires purchase for full text) |
|
|
74
|
+
| SOC 2 | aicpa.org (TSC 2017) |
|
|
75
|
+
| PCI DSS 4.0 | pcisecuritystandards.org/document_library |
|
|
76
|
+
| NIS2 | eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022L2555 |
|
|
77
|
+
| DORA | eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32022R2554 |
|
|
78
|
+
| EU AI Act | eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32024R1689 |
|
|
79
|
+
| EU CRA | Official Journal of EU |
|
|
80
|
+
| NCSC CAF | ncsc.gov.uk/collection/cyber-assessment-framework |
|
|
81
|
+
| ASD ISM | cyber.gov.au/resources-business-and-government/essential-cyber-security/ism |
|
|
82
|
+
| ASD Essential 8 | cyber.gov.au/resources-business-and-government/essential-cyber-security/essential-eight |
|
|
83
|
+
| MAS TRM | mas.gov.sg/regulation/guidelines/technology-risk-management-guidelines |
|
|
84
|
+
| CIS Controls v8 | cisecurity.org/controls/v8 |
|
|
85
|
+
| CSA CCM v4 | cloudsecurityalliance.org/research/cloud-controls-matrix |
|
|
86
|
+
|
|
87
|
+
### PQC Standards
|
|
88
|
+
|
|
89
|
+
| Standard | Authoritative Source |
|
|
90
|
+
|---|---|
|
|
91
|
+
| FIPS 203 (ML-KEM) | csrc.nist.gov/pubs/fips/203/final |
|
|
92
|
+
| FIPS 204 (ML-DSA) | csrc.nist.gov/pubs/fips/204/final |
|
|
93
|
+
| FIPS 205 (SLH-DSA) | csrc.nist.gov/pubs/fips/205/final |
|
|
94
|
+
| FIPS 206 (HQC, pending) | csrc.nist.gov/projects/post-quantum-cryptography |
|
|
95
|
+
| OpenSSL 3.5 release notes | github.com/openssl/openssl/blob/master/CHANGES.md |
|
|
96
|
+
| CNSA 2.0 | cnss.gov |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Source Verification Requirement
|
|
101
|
+
|
|
102
|
+
Every entry in `data/cve-catalog.json` must have a `source_verified` field:
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"source_verified": "2026-05-01",
|
|
106
|
+
"verification_sources": [
|
|
107
|
+
"https://nvd.nist.gov/vuln/detail/CVE-2026-31431",
|
|
108
|
+
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A `source_verified` date older than 90 days triggers a reverification requirement in the skill-update-loop.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Multi-Agent Research Protocol
|
|
118
|
+
|
|
119
|
+
When agents research new threat intelligence, they must:
|
|
120
|
+
1. Identify primary sources (from the registry above)
|
|
121
|
+
2. Record what was found at each source and when
|
|
122
|
+
3. Cross-reference across at least 2 independent sources for critical claims
|
|
123
|
+
4. Flag any claim that could only be verified from a single source
|
|
124
|
+
5. Record the agent ID and timestamp in the `source_verified` audit trail
|
|
125
|
+
|
|
126
|
+
See `agents/threat-researcher.md` for the research agent protocol.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Bad Data Prevention
|
|
131
|
+
|
|
132
|
+
These categories of sources are **rejected** for skill data:
|
|
133
|
+
|
|
134
|
+
| Source Type | Why Rejected |
|
|
135
|
+
|---|---|
|
|
136
|
+
| AI-generated summaries without primary source citation | Plausible hallucination risk |
|
|
137
|
+
| News articles | Often inaccurate on technical details, not updated when details change |
|
|
138
|
+
| Blog posts | No editorial standard, often repost errors from other blogs |
|
|
139
|
+
| Wikipedia | Community-edited, not authoritative for CVE details or framework text |
|
|
140
|
+
| Secondary aggregators without NVD cross-reference | May lag or misquote NVD |
|
|
141
|
+
| Social media / X posts | Not citable, not stable |
|
|
142
|
+
| Forum posts | Not authoritative |
|
|
143
|
+
|
|
144
|
+
The only exception: researcher/discoverer announcements about their own research (e.g., Hyunwoo Kim's Dirty Frag disclosure) may be used as a source alongside NVD, since the researcher is the primary source for their own findings.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Validators
|
|
149
|
+
|
|
150
|
+
Real validation against primary sources lives in `sources/validators/`. These are
|
|
151
|
+
zero-dependency Node 24 modules (stdlib `fetch`, `AbortController`, `fs/promises`
|
|
152
|
+
only). Every network call has a 10s timeout and degrades to an `unreachable`
|
|
153
|
+
status rather than throwing — the validators are safe to run in airgapped CI.
|
|
154
|
+
|
|
155
|
+
| Module | Purpose | Upstream |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| [`validators/cve-validator.js`](validators/cve-validator.js) | Cross-check one CVE's CVSS score, vector, and KEV status against NVD and the CISA KEV feed. Caches the KEV feed once per process. | NVD `services.nvd.nist.gov` + CISA KEV JSON |
|
|
158
|
+
| [`validators/atlas-validator.js`](validators/atlas-validator.js) | Confirm the pinned MITRE ATLAS version (in `manifest.json` and `sources/index.json`) matches the latest upstream release. | GitHub releases for `mitre-atlas/atlas-data`, raw `ATLAS.yaml` fallback |
|
|
159
|
+
| [`validators/index.js`](validators/index.js) | Barrel export plus `validateAllCves(catalog)` for catalog-wide aggregation with bounded concurrency. | — |
|
|
160
|
+
|
|
161
|
+
The orchestrator wires the CVE validator into the CLI:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
node orchestrator/index.js validate-cves # live cross-check, non-zero exit on drift
|
|
165
|
+
node orchestrator/index.js validate-cves --offline # local view only, no network
|
|
166
|
+
node orchestrator/index.js validate-cves --no-fail # report drift but always exit 0
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Feed snapshots are written under `sources/feeds/`; see `sources/feeds/README.md`
|
|
170
|
+
for the cache contract and freshness thresholds.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* atlas-validator.js — Confirm pinned MITRE ATLAS version against upstream.
|
|
5
|
+
*
|
|
6
|
+
* Zero npm dependencies. Node 24 stdlib only.
|
|
7
|
+
*
|
|
8
|
+
* MITRE ATLAS does not (as of v5.x) publish a stable machine-readable changelog JSON.
|
|
9
|
+
* The canonical source-of-truth for releases is the public GitHub repo:
|
|
10
|
+
* https://raw.githubusercontent.com/mitre-atlas/atlas-data/main/dist/ATLAS.yaml
|
|
11
|
+
* which carries an `id: ATLAS` / `version: x.y.z` header. The GitHub releases API
|
|
12
|
+
* also lists tagged versions:
|
|
13
|
+
* https://api.github.com/repos/mitre-atlas/atlas-data/releases/latest
|
|
14
|
+
*
|
|
15
|
+
* We prefer the releases API (lightweight JSON, no YAML parsing), fall back to the
|
|
16
|
+
* raw YAML version line, and finally report unreachable if both fail. Both are
|
|
17
|
+
* read-only public endpoints; no auth is required.
|
|
18
|
+
*
|
|
19
|
+
* Exported:
|
|
20
|
+
* validateAtlasVersion(opts?) -> Promise<{
|
|
21
|
+
* pinned: string|null,
|
|
22
|
+
* pinned_sources: { manifest: string|null, index: string|null },
|
|
23
|
+
* latest: string|null,
|
|
24
|
+
* drift: boolean,
|
|
25
|
+
* status: 'match'|'drift'|'unreachable'|'unknown',
|
|
26
|
+
* fetched_from: string|null,
|
|
27
|
+
* error: string|null
|
|
28
|
+
* }>
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs/promises');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
35
|
+
const USER_AGENT = 'exceptd-security/atlas-validator (+https://exceptd.com)';
|
|
36
|
+
|
|
37
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
38
|
+
const MANIFEST_PATH = path.join(REPO_ROOT, 'manifest.json');
|
|
39
|
+
const SOURCES_INDEX_PATH = path.join(REPO_ROOT, 'sources', 'index.json');
|
|
40
|
+
|
|
41
|
+
const GH_RELEASE_URL = 'https://api.github.com/repos/mitre-atlas/atlas-data/releases/latest';
|
|
42
|
+
const RAW_ATLAS_YAML = 'https://raw.githubusercontent.com/mitre-atlas/atlas-data/main/dist/ATLAS.yaml';
|
|
43
|
+
|
|
44
|
+
async function timedFetch(url, accept = 'application/json') {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
headers: { 'User-Agent': USER_AGENT, Accept: accept },
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
53
|
+
const body = accept.includes('json') ? await res.json() : await res.text();
|
|
54
|
+
return { ok: true, body };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const code = err.name === 'AbortError' ? 'timeout' : (err.code || 'network_error');
|
|
57
|
+
return { ok: false, error: `${code}: ${err.message}` };
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeVersion(v) {
|
|
64
|
+
if (!v || typeof v !== 'string') return null;
|
|
65
|
+
// Strip leading "v" / "ATLAS-v" prefixes; trim.
|
|
66
|
+
return v.trim().replace(/^ATLAS[-_ ]?/i, '').replace(/^v/i, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readPinnedVersions() {
|
|
70
|
+
const out = { manifest: null, index: null };
|
|
71
|
+
try {
|
|
72
|
+
const manifest = JSON.parse(await fs.readFile(MANIFEST_PATH, 'utf8'));
|
|
73
|
+
out.manifest = normalizeVersion(
|
|
74
|
+
manifest?._meta?.atlas_version || manifest?.atlas_version || null
|
|
75
|
+
);
|
|
76
|
+
} catch { /* leave null */ }
|
|
77
|
+
try {
|
|
78
|
+
const idx = JSON.parse(await fs.readFile(SOURCES_INDEX_PATH, 'utf8'));
|
|
79
|
+
out.index = normalizeVersion(idx?.sources?.atlas?.current_version || null);
|
|
80
|
+
} catch { /* leave null */ }
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function fetchLatestFromGithubReleases() {
|
|
85
|
+
const res = await timedFetch(GH_RELEASE_URL);
|
|
86
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
87
|
+
const tag = res.body?.tag_name || res.body?.name || null;
|
|
88
|
+
const version = normalizeVersion(tag);
|
|
89
|
+
if (!version) return { ok: false, error: 'no tag_name in response' };
|
|
90
|
+
return { ok: true, version, source: 'github-releases' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchLatestFromRawYaml() {
|
|
94
|
+
const res = await timedFetch(RAW_ATLAS_YAML, 'text/yaml');
|
|
95
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
96
|
+
// Naive YAML scrape: look for a top-level `version:` line within the first 200 lines.
|
|
97
|
+
const text = String(res.body).split(/\r?\n/).slice(0, 200).join('\n');
|
|
98
|
+
const match = text.match(/^version:\s*['"]?([0-9]+(?:\.[0-9]+){1,2})['"]?\s*$/m);
|
|
99
|
+
if (!match) return { ok: false, error: 'version line not found in ATLAS.yaml' };
|
|
100
|
+
return { ok: true, version: normalizeVersion(match[1]), source: 'raw-yaml' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function validateAtlasVersion(_opts = {}) {
|
|
104
|
+
const pinned_sources = await readPinnedVersions();
|
|
105
|
+
// Canonical pinned value: prefer manifest._meta or top-level, then sources/index.json.
|
|
106
|
+
const pinned = pinned_sources.manifest || pinned_sources.index || null;
|
|
107
|
+
|
|
108
|
+
// Cross-check that the two pinned locations agree.
|
|
109
|
+
const pinnedDisagree =
|
|
110
|
+
pinned_sources.manifest &&
|
|
111
|
+
pinned_sources.index &&
|
|
112
|
+
pinned_sources.manifest !== pinned_sources.index;
|
|
113
|
+
|
|
114
|
+
// Try GitHub releases first, fall back to raw YAML.
|
|
115
|
+
let upstream = await fetchLatestFromGithubReleases();
|
|
116
|
+
if (!upstream.ok) {
|
|
117
|
+
const fallback = await fetchLatestFromRawYaml();
|
|
118
|
+
if (fallback.ok) upstream = fallback;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!upstream.ok) {
|
|
122
|
+
return {
|
|
123
|
+
pinned,
|
|
124
|
+
pinned_sources,
|
|
125
|
+
latest: null,
|
|
126
|
+
drift: pinnedDisagree === true, // internal drift is still reportable offline
|
|
127
|
+
status: 'unreachable',
|
|
128
|
+
fetched_from: null,
|
|
129
|
+
error: upstream.error,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const latest = upstream.version;
|
|
134
|
+
if (!pinned) {
|
|
135
|
+
return {
|
|
136
|
+
pinned: null,
|
|
137
|
+
pinned_sources,
|
|
138
|
+
latest,
|
|
139
|
+
drift: true,
|
|
140
|
+
status: 'unknown',
|
|
141
|
+
fetched_from: upstream.source,
|
|
142
|
+
error: 'no pinned ATLAS version found in manifest.json or sources/index.json',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const drift = pinned !== latest || pinnedDisagree === true;
|
|
147
|
+
return {
|
|
148
|
+
pinned,
|
|
149
|
+
pinned_sources,
|
|
150
|
+
latest,
|
|
151
|
+
drift,
|
|
152
|
+
status: drift ? 'drift' : 'match',
|
|
153
|
+
fetched_from: upstream.source,
|
|
154
|
+
error: null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { validateAtlasVersion };
|