@blamejs/exceptd-skills 0.9.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/AGENTS.md +232 -0
- package/ARCHITECTURE.md +267 -0
- package/CHANGELOG.md +616 -0
- package/CONTEXT.md +203 -0
- package/LICENSE +200 -0
- package/NOTICE +82 -0
- package/README.md +307 -0
- package/SECURITY.md +73 -0
- package/agents/README.md +81 -0
- package/agents/report-generator.md +156 -0
- package/agents/skill-updater.md +102 -0
- package/agents/source-validator.md +119 -0
- package/agents/threat-researcher.md +149 -0
- package/bin/exceptd.js +183 -0
- package/data/_indexes/_meta.json +88 -0
- package/data/_indexes/activity-feed.json +362 -0
- package/data/_indexes/catalog-summaries.json +229 -0
- package/data/_indexes/chains.json +7135 -0
- package/data/_indexes/currency.json +359 -0
- package/data/_indexes/did-ladders.json +451 -0
- package/data/_indexes/frequency.json +2072 -0
- package/data/_indexes/handoff-dag.json +476 -0
- package/data/_indexes/jurisdiction-clocks.json +967 -0
- package/data/_indexes/jurisdiction-map.json +536 -0
- package/data/_indexes/recipes.json +319 -0
- package/data/_indexes/section-offsets.json +3656 -0
- package/data/_indexes/stale-content.json +14 -0
- package/data/_indexes/summary-cards.json +1736 -0
- package/data/_indexes/theater-fingerprints.json +381 -0
- package/data/_indexes/token-budget.json +2137 -0
- package/data/_indexes/trigger-table.json +1374 -0
- package/data/_indexes/xref.json +818 -0
- package/data/atlas-ttps.json +282 -0
- package/data/cve-catalog.json +496 -0
- package/data/cwe-catalog.json +1017 -0
- package/data/d3fend-catalog.json +738 -0
- package/data/dlp-controls.json +1039 -0
- package/data/exploit-availability.json +67 -0
- package/data/framework-control-gaps.json +1255 -0
- package/data/global-frameworks.json +2913 -0
- package/data/rfc-references.json +324 -0
- package/data/zeroday-lessons.json +377 -0
- package/keys/public.pem +3 -0
- package/lib/framework-gap.js +328 -0
- package/lib/job-queue.js +195 -0
- package/lib/lint-skills.js +536 -0
- package/lib/prefetch.js +372 -0
- package/lib/refresh-external.js +713 -0
- package/lib/schemas/cve-catalog.schema.json +151 -0
- package/lib/schemas/manifest.schema.json +106 -0
- package/lib/schemas/skill-frontmatter.schema.json +113 -0
- package/lib/scoring.js +149 -0
- package/lib/sign.js +197 -0
- package/lib/ttp-mapper.js +80 -0
- package/lib/validate-catalog-meta.js +198 -0
- package/lib/validate-cve-catalog.js +213 -0
- package/lib/validate-indexes.js +83 -0
- package/lib/validate-package.js +162 -0
- package/lib/validate-vendor.js +85 -0
- package/lib/verify.js +216 -0
- package/lib/worker-pool.js +84 -0
- package/manifest-snapshot.json +1833 -0
- package/manifest.json +2108 -0
- package/orchestrator/README.md +124 -0
- package/orchestrator/dispatcher.js +140 -0
- package/orchestrator/event-bus.js +146 -0
- package/orchestrator/index.js +874 -0
- package/orchestrator/pipeline.js +201 -0
- package/orchestrator/scanner.js +327 -0
- package/orchestrator/scheduler.js +137 -0
- package/package.json +113 -0
- package/sbom.cdx.json +158 -0
- package/scripts/audit-cross-skill.js +261 -0
- package/scripts/audit-perf.js +160 -0
- package/scripts/bootstrap.js +205 -0
- package/scripts/build-indexes.js +721 -0
- package/scripts/builders/activity-feed.js +79 -0
- package/scripts/builders/catalog-summaries.js +67 -0
- package/scripts/builders/currency.js +109 -0
- package/scripts/builders/cwe-chains.js +105 -0
- package/scripts/builders/did-ladders.js +149 -0
- package/scripts/builders/frequency.js +89 -0
- package/scripts/builders/jurisdiction-clocks.js +126 -0
- package/scripts/builders/recipes.js +159 -0
- package/scripts/builders/section-offsets.js +162 -0
- package/scripts/builders/stale-content.js +171 -0
- package/scripts/builders/summary-cards.js +166 -0
- package/scripts/builders/theater-fingerprints.js +198 -0
- package/scripts/builders/token-budget.js +96 -0
- package/scripts/check-manifest-snapshot.js +217 -0
- package/scripts/predeploy.js +267 -0
- package/scripts/refresh-manifest-snapshot.js +57 -0
- package/scripts/refresh-sbom.js +222 -0
- package/skills/age-gates-child-safety/skill.md +456 -0
- package/skills/ai-attack-surface/skill.md +282 -0
- package/skills/ai-c2-detection/skill.md +440 -0
- package/skills/ai-risk-management/skill.md +311 -0
- package/skills/api-security/skill.md +287 -0
- package/skills/attack-surface-pentest/skill.md +381 -0
- package/skills/cloud-security/skill.md +384 -0
- package/skills/compliance-theater/skill.md +365 -0
- package/skills/container-runtime-security/skill.md +379 -0
- package/skills/coordinated-vuln-disclosure/skill.md +473 -0
- package/skills/defensive-countermeasure-mapping/skill.md +300 -0
- package/skills/dlp-gap-analysis/skill.md +337 -0
- package/skills/email-security-anti-phishing/skill.md +206 -0
- package/skills/exploit-scoring/skill.md +331 -0
- package/skills/framework-gap-analysis/skill.md +374 -0
- package/skills/fuzz-testing-strategy/skill.md +313 -0
- package/skills/global-grc/skill.md +564 -0
- package/skills/identity-assurance/skill.md +272 -0
- package/skills/incident-response-playbook/skill.md +546 -0
- package/skills/kernel-lpe-triage/skill.md +303 -0
- package/skills/mcp-agent-trust/skill.md +326 -0
- package/skills/mlops-security/skill.md +325 -0
- package/skills/ot-ics-security/skill.md +340 -0
- package/skills/policy-exception-gen/skill.md +437 -0
- package/skills/pqc-first/skill.md +546 -0
- package/skills/rag-pipeline-security/skill.md +294 -0
- package/skills/researcher/skill.md +310 -0
- package/skills/sector-energy/skill.md +409 -0
- package/skills/sector-federal-government/skill.md +302 -0
- package/skills/sector-financial/skill.md +398 -0
- package/skills/sector-healthcare/skill.md +373 -0
- package/skills/security-maturity-tiers/skill.md +464 -0
- package/skills/skill-update-loop/skill.md +463 -0
- package/skills/supply-chain-integrity/skill.md +318 -0
- package/skills/threat-model-currency/skill.md +404 -0
- package/skills/threat-modeling-methodology/skill.md +312 -0
- package/skills/webapp-security/skill.md +281 -0
- package/skills/zeroday-gap-learn/skill.md +350 -0
- package/vendor/blamejs/LICENSE +201 -0
- package/vendor/blamejs/README.md +54 -0
- package/vendor/blamejs/_PROVENANCE.json +54 -0
- package/vendor/blamejs/retry.js +335 -0
- package/vendor/blamejs/worker-pool.js +418 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/activity-feed.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/activity-feed.json` — a "what changed when" feed
|
|
6
|
+
* across skills and catalogs, sorted by date. Lightweight RSS for the
|
|
7
|
+
* skill corpus that consumers can poll without diff-ing the manifest.
|
|
8
|
+
*
|
|
9
|
+
* Combines:
|
|
10
|
+
* - per-skill last_threat_review
|
|
11
|
+
* - per-catalog _meta.last_updated
|
|
12
|
+
* - manifest threat_review_date + atlas_version_date when present
|
|
13
|
+
*
|
|
14
|
+
* Output sorted descending by date.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
function buildActivityFeed({ root, manifest, skills, catalogFiles }) {
|
|
21
|
+
const events = [];
|
|
22
|
+
|
|
23
|
+
for (const s of skills) {
|
|
24
|
+
if (s.last_threat_review) {
|
|
25
|
+
events.push({
|
|
26
|
+
date: s.last_threat_review,
|
|
27
|
+
type: "skill_review",
|
|
28
|
+
artifact: s.name,
|
|
29
|
+
path: s.path,
|
|
30
|
+
note: s.description || null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const f of catalogFiles) {
|
|
36
|
+
const abs = path.join(root, f);
|
|
37
|
+
try {
|
|
38
|
+
const j = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
39
|
+
const meta = j._meta || {};
|
|
40
|
+
const when = meta.last_updated || meta.last_verified || null;
|
|
41
|
+
if (when) {
|
|
42
|
+
events.push({
|
|
43
|
+
date: when,
|
|
44
|
+
type: "catalog_update",
|
|
45
|
+
artifact: f,
|
|
46
|
+
path: f,
|
|
47
|
+
schema_version: meta.schema_version || null,
|
|
48
|
+
entry_count: Object.keys(j).filter((k) => !k.startsWith("_")).length,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// skip non-JSON or malformed; build-indexes runs after lint so this
|
|
53
|
+
// is unlikely.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (manifest.threat_review_date) {
|
|
58
|
+
events.push({
|
|
59
|
+
date: manifest.threat_review_date,
|
|
60
|
+
type: "manifest_review",
|
|
61
|
+
artifact: "manifest.json",
|
|
62
|
+
path: "manifest.json",
|
|
63
|
+
note: `manifest threat_review_date — ${manifest.skills.length} skills, ${catalogFiles.length} catalogs`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
events.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
_meta: {
|
|
71
|
+
schema_version: "1.0.0",
|
|
72
|
+
note: "Per-artifact 'last changed' feed sorted descending by date. Skill events from manifest.last_threat_review; catalog events from data/<catalog>.json _meta.last_updated.",
|
|
73
|
+
event_count: events.length,
|
|
74
|
+
},
|
|
75
|
+
events,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { buildActivityFeed };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/catalog-summaries.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/catalog-summaries.json` — for each data/<catalog>.json
|
|
6
|
+
* file, a compact summary: purpose, entry count, version pin (where applicable),
|
|
7
|
+
* source confidence, TLP, last-updated date. Consumers can load this single
|
|
8
|
+
* file (~3-4 KB) instead of every _meta block to learn what catalogs are
|
|
9
|
+
* available and how fresh they are.
|
|
10
|
+
*
|
|
11
|
+
* Curated human-readable purpose strings: keep these in lockstep with the
|
|
12
|
+
* canonical catalog README in `docs/data-catalogs.md` if/when added.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
|
|
18
|
+
const CATALOG_PURPOSES = {
|
|
19
|
+
"cve-catalog.json": "Per-CVE record (CVSS, EPSS, CISA KEV, RWEP, AI-discovery, vendor advisories, framework gaps, ATLAS/ATT&CK mappings). Cross-validated against NVD + CISA KEV + FIRST EPSS via validate-cves.",
|
|
20
|
+
"cwe-catalog.json": "MITRE CWE entries used by the project (subset with skill citations), with severity hint and category. Pinned to a CWE catalog version.",
|
|
21
|
+
"atlas-ttps.json": "MITRE ATLAS TTPs (AML.T0xxx) cited by skills, with tactic, name, description. Pinned to ATLAS v5.1.0 (November 2025).",
|
|
22
|
+
"d3fend-catalog.json": "MITRE D3FEND countermeasures (D3-xxx) keyed by id, with tactic + name. Pinned to D3FEND v1.0.0 release.",
|
|
23
|
+
"framework-control-gaps.json": "Per-control framework gap declarations: SI-2, A.8.8, PCI 6.3.3, etc. Each entry names the control, the lag, the evidence CVE, and remediation guidance.",
|
|
24
|
+
"global-frameworks.json": "Multi-jurisdiction framework registry: 34 jurisdictions × applicable frameworks × patch_sla / notification_sla / critical_controls / framework_gaps. Cross-cutting authority for jurisdiction-clocks index.",
|
|
25
|
+
"exploit-availability.json": "Per-CVE exploit availability: PoC public status, weaponization signal, AI-assist status, blast-radius. Project-curated (B2 Admiralty confidence) with source citations.",
|
|
26
|
+
"zeroday-lessons.json": "Distilled lessons from notable zero-days and campaigns (SesameOp, Copy Fail, Dirty Frag, Copilot RCE, Windsurf MCP). Each entry: technique, distinguishing characteristic, what it means for the framework lag.",
|
|
27
|
+
"rfc-references.json": "IETF RFCs + active Internet-Drafts cited by skills (TLS, IPsec, PQ crypto migration, HTTP/3, CT). Cross-validated against IETF Datatracker via validate-rfcs.",
|
|
28
|
+
"dlp-controls.json": "DLP control inventory: per-pattern definitions for the dlp-gap-analysis skill, jurisdiction-tagged so a deployment can scope by applicable laws.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function buildCatalogSummaries({ root, catalogFiles }) {
|
|
32
|
+
const summaries = {};
|
|
33
|
+
for (const rel of catalogFiles) {
|
|
34
|
+
const base = path.basename(rel);
|
|
35
|
+
const abs = path.join(root, rel);
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
39
|
+
} catch (err) {
|
|
40
|
+
summaries[base] = { error: `parse_error: ${err.message}` };
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const meta = parsed._meta || {};
|
|
44
|
+
const entries = Object.keys(parsed).filter((k) => !k.startsWith("_"));
|
|
45
|
+
summaries[base] = {
|
|
46
|
+
path: rel,
|
|
47
|
+
purpose: CATALOG_PURPOSES[base] || null,
|
|
48
|
+
schema_version: meta.schema_version || null,
|
|
49
|
+
last_updated: meta.last_updated || meta.last_verified || null,
|
|
50
|
+
tlp: meta.tlp || null,
|
|
51
|
+
source_confidence_default: meta.source_confidence?.default || null,
|
|
52
|
+
freshness_policy: meta.freshness_policy || null,
|
|
53
|
+
entry_count: entries.length,
|
|
54
|
+
sample_keys: entries.slice(0, 5),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
_meta: {
|
|
59
|
+
schema_version: "1.0.0",
|
|
60
|
+
note: "Per-catalog compact summary so AI consumers can discover available data without loading every _meta block. Purpose strings are curated in scripts/builders/catalog-summaries.js.",
|
|
61
|
+
catalog_count: Object.keys(summaries).length,
|
|
62
|
+
},
|
|
63
|
+
catalogs: summaries,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { buildCatalogSummaries };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/currency.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/currency.json` — pre-computed currency scores for
|
|
6
|
+
* every skill against a deterministic reference date (manifest's
|
|
7
|
+
* `threat_review_date`). Saves the watchlist/scheduler from re-running
|
|
8
|
+
* `orchestrator currency` to produce the same answer.
|
|
9
|
+
*
|
|
10
|
+
* The reference date is deterministic so the file hash stays stable until
|
|
11
|
+
* skills or the reference change — which is what `validate-indexes`
|
|
12
|
+
* requires. The orchestrator's interactive `currency` command remains the
|
|
13
|
+
* source-of-truth for live decay; this index is the snapshot view.
|
|
14
|
+
*
|
|
15
|
+
* Decay formula matches pipeline.js _currencyScore() exactly:
|
|
16
|
+
* >180 days → -30, >90 → -20, >60 → -10, >30 → -5
|
|
17
|
+
* -5 per forward_watch entry
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
|
|
23
|
+
function currencyScore(daysSinceReview, forwardWatchCount) {
|
|
24
|
+
let score = 100;
|
|
25
|
+
if (daysSinceReview > 180) score -= 30;
|
|
26
|
+
else if (daysSinceReview > 90) score -= 20;
|
|
27
|
+
else if (daysSinceReview > 60) score -= 10;
|
|
28
|
+
else if (daysSinceReview > 30) score -= 5;
|
|
29
|
+
score -= forwardWatchCount * 5;
|
|
30
|
+
return Math.max(0, score);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function currencyLabel(score) {
|
|
34
|
+
if (score >= 90) return "current";
|
|
35
|
+
if (score >= 70) return "acceptable";
|
|
36
|
+
if (score >= 50) return "stale";
|
|
37
|
+
return "critical_stale";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseFrontmatterForwardWatchCount(body) {
|
|
41
|
+
const m = body.match(/^---\n([\s\S]*?)\n---/);
|
|
42
|
+
if (!m) return 0;
|
|
43
|
+
const fm = m[1];
|
|
44
|
+
// Counts top-level "forward_watch:" list items. Lines starting with " - "
|
|
45
|
+
// immediately after a "forward_watch:" line until the next non-indented key.
|
|
46
|
+
const lines = fm.split(/\r?\n/);
|
|
47
|
+
let inFw = false;
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (/^forward_watch:\s*$/.test(line)) {
|
|
51
|
+
inFw = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (inFw) {
|
|
55
|
+
if (/^\s*-\s+/.test(line)) {
|
|
56
|
+
count++;
|
|
57
|
+
} else if (/^[a-zA-Z_]/.test(line)) {
|
|
58
|
+
// New top-level key — end of forward_watch block.
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildCurrency({ root, manifest, skills }) {
|
|
67
|
+
const ref = manifest.threat_review_date || "2026-05-01";
|
|
68
|
+
const refDate = new Date(ref + "T00:00:00Z");
|
|
69
|
+
|
|
70
|
+
const rows = [];
|
|
71
|
+
for (const s of skills) {
|
|
72
|
+
const body = fs.readFileSync(path.join(root, s.path), "utf8");
|
|
73
|
+
const fwCount = parseFrontmatterForwardWatchCount(body);
|
|
74
|
+
const reviewDate = new Date((s.last_threat_review || "2020-01-01") + "T00:00:00Z");
|
|
75
|
+
const days = Math.floor((refDate - reviewDate) / 86400000);
|
|
76
|
+
const score = currencyScore(days, fwCount);
|
|
77
|
+
rows.push({
|
|
78
|
+
skill: s.name,
|
|
79
|
+
last_threat_review: s.last_threat_review || null,
|
|
80
|
+
days_since_review: days,
|
|
81
|
+
currency_score: score,
|
|
82
|
+
currency_label: currencyLabel(score),
|
|
83
|
+
forward_watch_count: fwCount,
|
|
84
|
+
action_required: score < 70,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
rows.sort((a, b) => a.currency_score - b.currency_score || a.skill.localeCompare(b.skill));
|
|
88
|
+
|
|
89
|
+
const summary = {
|
|
90
|
+
current: rows.filter((r) => r.currency_label === "current").length,
|
|
91
|
+
acceptable: rows.filter((r) => r.currency_label === "acceptable").length,
|
|
92
|
+
stale: rows.filter((r) => r.currency_label === "stale").length,
|
|
93
|
+
critical_stale: rows.filter((r) => r.currency_label === "critical_stale").length,
|
|
94
|
+
action_required: rows.filter((r) => r.action_required).length,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
_meta: {
|
|
99
|
+
schema_version: "1.0.0",
|
|
100
|
+
reference_date: ref,
|
|
101
|
+
note: "Pre-computed skill currency snapshot. Reference date is manifest.threat_review_date (deterministic). Re-runs of build-indexes against the same inputs produce byte-identical output. The orchestrator `currency` command produces a real-time view against today's date.",
|
|
102
|
+
decay_formula: "100 base; -30/-20/-10/-5 at 180/90/60/30-day thresholds; -5 per forward_watch entry. Label thresholds: ≥90 current, ≥70 acceptable, ≥50 stale, <50 critical_stale.",
|
|
103
|
+
},
|
|
104
|
+
summary,
|
|
105
|
+
skills: rows,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { buildCurrency };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/cwe-chains.js
|
|
4
|
+
*
|
|
5
|
+
* Builds the CWE side of `data/_indexes/chains.json`. The existing chains
|
|
6
|
+
* object is keyed by CVE-id; this builder produces CWE-id entries with the
|
|
7
|
+
* same hydrated cross-walk shape so AI consumers can start from a CWE and
|
|
8
|
+
* see every skill / catalog dimension that touches the weakness class.
|
|
9
|
+
*
|
|
10
|
+
* Per-CWE shape:
|
|
11
|
+
* {
|
|
12
|
+
* name: CWE catalog entry name
|
|
13
|
+
* category: CWE catalog category (memory-safety / injection / etc.)
|
|
14
|
+
* referencing_skills: every skill listing this CWE in cwe_refs
|
|
15
|
+
* chain: {
|
|
16
|
+
* atlas, attack_refs, framework_gaps, d3fend, rfc_refs, dlp_refs
|
|
17
|
+
* }
|
|
18
|
+
* related_cves: CVEs in cve-catalog.json whose framework gaps
|
|
19
|
+
* surface via skills that also cite this CWE
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* The CWE → CVE link is indirect — CWEs aren't currently stamped on each
|
|
23
|
+
* CVE entry directly. The skill bodies are the connective tissue. So we
|
|
24
|
+
* compute it the same way the CVE chains builder does: skills cite CWEs,
|
|
25
|
+
* skills cite framework_gaps, framework_gaps surface evidence_cves. The
|
|
26
|
+
* intersection through the skill graph gives the related-CVE set.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
function buildCweChains({ skills, cweCatalog, atlasTtps, cveCatalog, frameworkGaps, d3fendCatalog, rfcCatalog }) {
|
|
30
|
+
const cweIds = Object.keys(cweCatalog).filter((k) => !k.startsWith("_"));
|
|
31
|
+
const out = {};
|
|
32
|
+
|
|
33
|
+
for (const cweId of cweIds) {
|
|
34
|
+
const cweEntry = cweCatalog[cweId] || {};
|
|
35
|
+
|
|
36
|
+
const referencingSkills = skills
|
|
37
|
+
.filter((s) => (s.cwe_refs || []).includes(cweId))
|
|
38
|
+
.map((s) => s.name);
|
|
39
|
+
|
|
40
|
+
// Aggregate cross-refs from those skills.
|
|
41
|
+
const accum = {
|
|
42
|
+
atlas_refs: new Set(),
|
|
43
|
+
attack_refs: new Set(),
|
|
44
|
+
framework_gaps: new Set(),
|
|
45
|
+
d3fend_refs: new Set(),
|
|
46
|
+
rfc_refs: new Set(),
|
|
47
|
+
dlp_refs: new Set(),
|
|
48
|
+
};
|
|
49
|
+
for (const name of referencingSkills) {
|
|
50
|
+
const s = skills.find((x) => x.name === name);
|
|
51
|
+
if (!s) continue;
|
|
52
|
+
for (const field of Object.keys(accum)) {
|
|
53
|
+
for (const v of s[field] || []) accum[field].add(v);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Hydrate the cross-walk dimensions for the AI consumer.
|
|
58
|
+
const hydrated = {
|
|
59
|
+
atlas: [...accum.atlas_refs].sort().map((a) => ({
|
|
60
|
+
id: a,
|
|
61
|
+
name: atlasTtps[a]?.name,
|
|
62
|
+
tactic: atlasTtps[a]?.tactic,
|
|
63
|
+
})),
|
|
64
|
+
attack_refs: [...accum.attack_refs].sort(),
|
|
65
|
+
framework_gaps: [...accum.framework_gaps].sort().map((f) => ({
|
|
66
|
+
id: f,
|
|
67
|
+
framework: frameworkGaps[f]?.framework,
|
|
68
|
+
control_name: frameworkGaps[f]?.control_name,
|
|
69
|
+
})),
|
|
70
|
+
d3fend: [...accum.d3fend_refs].sort().map((d) => ({
|
|
71
|
+
id: d,
|
|
72
|
+
name: d3fendCatalog[d]?.name,
|
|
73
|
+
tactic: d3fendCatalog[d]?.tactic,
|
|
74
|
+
})),
|
|
75
|
+
rfc_refs: [...accum.rfc_refs].sort().map((r) => ({
|
|
76
|
+
id: r,
|
|
77
|
+
title: rfcCatalog[r]?.title,
|
|
78
|
+
status: rfcCatalog[r]?.status,
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Related CVEs: walk evidence_cves on the framework_gaps that the
|
|
83
|
+
// referencing skills cite. Inner join via the skill graph.
|
|
84
|
+
const relatedCves = new Set();
|
|
85
|
+
for (const gap of accum.framework_gaps) {
|
|
86
|
+
for (const ev of (frameworkGaps[gap]?.evidence_cves || [])) {
|
|
87
|
+
if (cveCatalog[ev]) relatedCves.add(ev);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
out[cweId] = {
|
|
92
|
+
name: cweEntry.name,
|
|
93
|
+
category: cweEntry.category,
|
|
94
|
+
severity_hint: cweEntry.severity_hint,
|
|
95
|
+
referencing_skills: referencingSkills,
|
|
96
|
+
skill_count: referencingSkills.length,
|
|
97
|
+
chain: hydrated,
|
|
98
|
+
related_cves: [...relatedCves].sort(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { buildCweChains };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/did-ladders.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/did-ladders.json` — canonical defense-in-depth
|
|
6
|
+
* ladders per high-frequency attack class. Each ladder is a layered
|
|
7
|
+
* sequence of controls (perimeter → identity → workload → data →
|
|
8
|
+
* detection) with a cross-reference to the source skill(s) that
|
|
9
|
+
* operationalize each layer and the D3FEND countermeasures backing each
|
|
10
|
+
* layer.
|
|
11
|
+
*
|
|
12
|
+
* Curated content, validated against the manifest: every referenced skill
|
|
13
|
+
* and D3FEND id must exist in their respective catalogs or the build
|
|
14
|
+
* fails. This is the only place where the project's DiD knowledge is
|
|
15
|
+
* laid out flat across attack classes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const LADDERS = [
|
|
19
|
+
{
|
|
20
|
+
id: "prompt-injection",
|
|
21
|
+
attack_class: "Prompt Injection (Direct + Indirect)",
|
|
22
|
+
primary_ttps: ["AML.T0051", "AML.T0054"],
|
|
23
|
+
layers: [
|
|
24
|
+
{ layer: "Perimeter / Input", control: "Input source allowlist + retrieval provenance metadata", source_skill: "rag-pipeline-security", d3fend: ["D3-CA", "D3-CSPP"] },
|
|
25
|
+
{ layer: "Model / Reasoning", control: "Prompt sanitization, instruction hierarchy enforcement, refusal calibration", source_skill: "ai-attack-surface", d3fend: ["D3-IOPR"] },
|
|
26
|
+
{ layer: "Identity / Tool Auth", control: "Per-tool least-privilege scopes; user-confirmation gate for destructive tool calls", source_skill: "mcp-agent-trust", d3fend: ["D3-CBAN", "D3-MFA"] },
|
|
27
|
+
{ layer: "Action / Egress", control: "Network egress allowlist for agentic outbound calls; DLP on tool outputs", source_skill: "ai-c2-detection", d3fend: ["D3-NTA", "D3-NTPM"] },
|
|
28
|
+
{ layer: "Detection", control: "Behavioral anomaly on tool-call sequences; canary tokens in retrieved context", source_skill: "ai-c2-detection", d3fend: ["D3-RPA", "D3-NTA"] },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "kernel-lpe",
|
|
33
|
+
attack_class: "Kernel Local Privilege Escalation",
|
|
34
|
+
primary_ttps: ["T1068", "T1548.001"],
|
|
35
|
+
layers: [
|
|
36
|
+
{ layer: "Build / Kernel", control: "Up-to-date kernel + KASLR + control-flow integrity; live-patch capability deployed", source_skill: "kernel-lpe-triage", d3fend: ["D3-ASLR", "D3-EAL"] },
|
|
37
|
+
{ layer: "Workload Isolation", control: "User namespaces with no_new_privs; seccomp profiles narrowing syscall surface", source_skill: "container-runtime-security", d3fend: ["D3-PSEP", "D3-SCP"] },
|
|
38
|
+
{ layer: "Identity / Access", control: "No long-lived shell on production hosts; broker access via PAM/JIT", source_skill: "identity-assurance", d3fend: ["D3-MFA", "D3-CBAN"] },
|
|
39
|
+
{ layer: "Detection", control: "Audit on uid=0 transitions; eBPF on suspicious capability acquisitions", source_skill: "ai-c2-detection", d3fend: ["D3-PHRA", "D3-PA"] },
|
|
40
|
+
{ layer: "Patch Velocity", control: "≤72h to live-patch for CISA KEV LPE class; reboot-on-window for cold patch", source_skill: "kernel-lpe-triage", d3fend: ["D3-EAL"] },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "ai-c2",
|
|
45
|
+
attack_class: "AI-API as C2 (PROMPTFLUX / SesameOp / AI egress abuse)",
|
|
46
|
+
primary_ttps: ["AML.T0096", "T1041"],
|
|
47
|
+
layers: [
|
|
48
|
+
{ layer: "Perimeter / Egress", control: "Egress allowlist by destination + identity-aware proxy on AI API endpoints", source_skill: "ai-c2-detection", d3fend: ["D3-NTPM", "D3-NI"] },
|
|
49
|
+
{ layer: "Identity", control: "Service-account per AI use-case; deny human creds on AI API destinations", source_skill: "identity-assurance", d3fend: ["D3-CBAN", "D3-MFA"] },
|
|
50
|
+
{ layer: "Behavioral Detection", control: "Baseline AI-API call volume + content-length profile; alert on encoded blobs in prompts", source_skill: "ai-c2-detection", d3fend: ["D3-NTA", "D3-CSPP"] },
|
|
51
|
+
{ layer: "Data", control: "DLP on prompt content to AI APIs (token-redaction + PII patterns)", source_skill: "dlp-gap-analysis", d3fend: ["D3-MENCR"] },
|
|
52
|
+
{ layer: "IR", control: "Triage playbook for AI-as-C2 incidents; provider-side audit-log pull procedure", source_skill: "incident-response-playbook", d3fend: ["D3-IOPR"] },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "ransomware",
|
|
57
|
+
attack_class: "Ransomware (Double-Extortion + OT Impact)",
|
|
58
|
+
primary_ttps: ["T1486", "T1567"],
|
|
59
|
+
layers: [
|
|
60
|
+
{ layer: "Phishing Defense", control: "AI-generated phishing baseline; mailflow detonation + URL rewrite", source_skill: "email-security-anti-phishing", d3fend: ["D3-CA", "D3-DA"] },
|
|
61
|
+
{ layer: "Identity / MFA", control: "MFA on every privileged path; deny legacy auth; conditional access by device posture", source_skill: "identity-assurance", d3fend: ["D3-MFA", "D3-CBAN"] },
|
|
62
|
+
{ layer: "Lateral Movement Block", control: "Network segmentation by zone; deny SMB cross-segment; admin-tier isolation", source_skill: "incident-response-playbook", d3fend: ["D3-NI", "D3-NTPM"] },
|
|
63
|
+
{ layer: "Data", control: "Immutable backups (3-2-1-1-0); offline restore-time tested quarterly", source_skill: "incident-response-playbook", d3fend: ["D3-FE"] },
|
|
64
|
+
{ layer: "OT Impact", control: "OT/IT segmentation per IEC 62443 zones; manual-fallback procedures rehearsed", source_skill: "ot-ics-security", d3fend: ["D3-NI"] },
|
|
65
|
+
{ layer: "Detection", control: "Behavioral on file-rename rates + canary files; T1486 ATT&CK detection", source_skill: "ai-c2-detection", d3fend: ["D3-FAPA", "D3-PHRA"] },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "supply-chain",
|
|
70
|
+
attack_class: "Supply Chain Compromise (Build + Dependency + MCP)",
|
|
71
|
+
primary_ttps: ["AML.T0010", "T1195"],
|
|
72
|
+
layers: [
|
|
73
|
+
{ layer: "Source", control: "Signed commits + branch-protection enforcement; trusted-builder allowlist", source_skill: "supply-chain-integrity", d3fend: ["D3-EHB", "D3-CBAN"] },
|
|
74
|
+
{ layer: "Build", control: "Hermetic builds + SLSA Level 3+ provenance attestation", source_skill: "supply-chain-integrity", d3fend: ["D3-EHB"] },
|
|
75
|
+
{ layer: "Dependency", control: "SBOM + signed attestation per dep; transitive-dep advisory loop", source_skill: "supply-chain-integrity", d3fend: ["D3-EHB", "D3-CA"] },
|
|
76
|
+
{ layer: "MCP / Tooling", control: "MCP server allowlist on engineering workstations; signed plugin manifests", source_skill: "mcp-agent-trust", d3fend: ["D3-EAL", "D3-EHB"] },
|
|
77
|
+
{ layer: "Runtime", control: "Workload identity attestation on deploy; deny-unsigned-image admission control", source_skill: "container-runtime-security", d3fend: ["D3-EHB", "D3-CBAN"] },
|
|
78
|
+
{ layer: "Detection", control: "Anomalous build outputs; new outbound destinations from build hosts", source_skill: "ai-c2-detection", d3fend: ["D3-NTA"] },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "bola",
|
|
83
|
+
attack_class: "Broken Object Level Authorization (BOLA / API auth bypass)",
|
|
84
|
+
primary_ttps: ["T1190"],
|
|
85
|
+
layers: [
|
|
86
|
+
{ layer: "API Gateway", control: "Per-route policy with object-scoped authorization; reject opaque IDs without claim binding", source_skill: "api-security", d3fend: ["D3-CSPP"] },
|
|
87
|
+
{ layer: "Service", control: "Authorization at the resource handler, never at the gateway alone", source_skill: "api-security", d3fend: ["D3-PA"] },
|
|
88
|
+
{ layer: "Identity", control: "OAuth scopes + per-tenant claim with cryptographic binding to identity", source_skill: "identity-assurance", d3fend: ["D3-CBAN", "D3-MFA"] },
|
|
89
|
+
{ layer: "Detection", control: "Per-user request-pattern baselining; alert on bulk-enumeration over /resource/:id", source_skill: "api-security", d3fend: ["D3-NTA", "D3-CSPP"] },
|
|
90
|
+
{ layer: "Testing", control: "BOLA in pen-test scope, every release; fuzz IDOR primitives", source_skill: "fuzz-testing-strategy", d3fend: ["D3-PA"] },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "model-exfiltration",
|
|
95
|
+
attack_class: "Model Weight / Training Data Exfiltration",
|
|
96
|
+
primary_ttps: ["AML.T0043", "AML.T0017"],
|
|
97
|
+
layers: [
|
|
98
|
+
{ layer: "Storage", control: "Model weights in encrypted-at-rest object store; key access via KMS audit", source_skill: "mlops-security", d3fend: ["D3-FE", "D3-MENCR"] },
|
|
99
|
+
{ layer: "Access Control", control: "ML-engineer workstations get read-only via signed checkouts; production weights gated", source_skill: "identity-assurance", d3fend: ["D3-CBAN", "D3-MFA"] },
|
|
100
|
+
{ layer: "Egress", control: "DLP on weight-file size patterns + chunked upload to public buckets", source_skill: "dlp-gap-analysis", d3fend: ["D3-NTA", "D3-NTPM"] },
|
|
101
|
+
{ layer: "Detection", control: "S3/GCS audit-log anomaly on model-bucket GET-rates; alert on cross-tenant key use", source_skill: "ai-c2-detection", d3fend: ["D3-FAPA", "D3-NTA"] },
|
|
102
|
+
{ layer: "Provenance", control: "Watermarked model weights for high-value models", source_skill: "mlops-security", d3fend: ["D3-EAL"] },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "bec",
|
|
107
|
+
attack_class: "Business Email Compromise (AI-generated)",
|
|
108
|
+
primary_ttps: ["T1566", "T1583.001"],
|
|
109
|
+
layers: [
|
|
110
|
+
{ layer: "Email Gateway", control: "SPF + DKIM + DMARC enforcement at p=reject; impersonation-domain detection", source_skill: "email-security-anti-phishing", d3fend: ["D3-DA", "D3-CA"] },
|
|
111
|
+
{ layer: "User Awareness", control: "AI-generated-content-aware training (grammar heuristics are dead)", source_skill: "email-security-anti-phishing", d3fend: ["D3-CSPP"] },
|
|
112
|
+
{ layer: "Wire-Fraud Controls", control: "Out-of-band confirmation for any payment change; vendor-bank-change verification", source_skill: "sector-financial", d3fend: ["D3-CBAN"] },
|
|
113
|
+
{ layer: "Detection", control: "Mailflow behavioral anomaly on first-time-correspondent + payment-context", source_skill: "email-security-anti-phishing", d3fend: ["D3-NTA", "D3-CSPP"] },
|
|
114
|
+
{ layer: "Recovery", control: "BEC IR playbook with FBI IC3 reporting + bank-recall window (<72h)", source_skill: "incident-response-playbook", d3fend: ["D3-IOPR"] },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
function buildDidLadders({ skills, d3fendCatalog }) {
|
|
120
|
+
const skillNames = new Set(skills.map((s) => s.name));
|
|
121
|
+
const d3Ids = new Set(Object.keys(d3fendCatalog).filter((k) => !k.startsWith("_")));
|
|
122
|
+
const errors = [];
|
|
123
|
+
for (const ladder of LADDERS) {
|
|
124
|
+
for (const layer of ladder.layers) {
|
|
125
|
+
if (!skillNames.has(layer.source_skill)) {
|
|
126
|
+
errors.push(`ladder ${ladder.id} layer ${layer.layer}: unknown source_skill "${layer.source_skill}"`);
|
|
127
|
+
}
|
|
128
|
+
for (const d of layer.d3fend || []) {
|
|
129
|
+
if (!d3Ids.has(d)) {
|
|
130
|
+
errors.push(`ladder ${ladder.id} layer ${layer.layer}: unknown D3FEND ref "${d}"`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (errors.length) {
|
|
136
|
+
throw new Error("did-ladders.js: " + errors.join("; "));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
_meta: {
|
|
141
|
+
schema_version: "1.0.0",
|
|
142
|
+
note: "Canonical defense-in-depth ladder per high-frequency attack class. Each layer references the source skill that operationalizes it and the D3FEND countermeasures backing it.",
|
|
143
|
+
ladder_count: LADDERS.length,
|
|
144
|
+
},
|
|
145
|
+
ladders: LADDERS,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { buildDidLadders };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/frequency.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/frequency.json` — citation-count tables. For each
|
|
6
|
+
* catalog (CWE, ATLAS, ATT&CK, D3FEND, framework gaps, RFC, DLP), counts
|
|
7
|
+
* how many skills cite each entry. Surfaces which entries are load-bearing
|
|
8
|
+
* (cited by many) vs. orphan-adjacent (cited by ≤1).
|
|
9
|
+
*
|
|
10
|
+
* Per-field shape:
|
|
11
|
+
* {
|
|
12
|
+
* <entry_id>: { count, skills: [name, ...] }
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Plus rollups:
|
|
16
|
+
* - top_cited: top 10 entries per field
|
|
17
|
+
* - orphan_adjacent: entries cited by exactly one skill
|
|
18
|
+
* - uncited: catalog entries with zero skill citations (flagged for review)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function buildFrequency({ skills, catalogs }) {
|
|
22
|
+
const fields = ["cwe_refs", "d3fend_refs", "framework_gaps", "atlas_refs", "attack_refs", "rfc_refs", "dlp_refs"];
|
|
23
|
+
const counts = {};
|
|
24
|
+
for (const f of fields) counts[f] = {};
|
|
25
|
+
|
|
26
|
+
for (const s of skills) {
|
|
27
|
+
for (const f of fields) {
|
|
28
|
+
for (const v of s[f] || []) {
|
|
29
|
+
if (!counts[f][v]) counts[f][v] = { count: 0, skills: [] };
|
|
30
|
+
counts[f][v].count++;
|
|
31
|
+
counts[f][v].skills.push(s.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const f of fields) {
|
|
36
|
+
for (const k of Object.keys(counts[f])) counts[f][k].skills.sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function topN(field, n = 10) {
|
|
40
|
+
return Object.entries(counts[field])
|
|
41
|
+
.sort((a, b) => b[1].count - a[1].count || a[0].localeCompare(b[0]))
|
|
42
|
+
.slice(0, n)
|
|
43
|
+
.map(([id, info]) => ({ id, count: info.count, skills: info.skills }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const orphanAdjacent = {};
|
|
47
|
+
for (const f of fields) {
|
|
48
|
+
orphanAdjacent[f] = Object.entries(counts[f])
|
|
49
|
+
.filter(([, info]) => info.count === 1)
|
|
50
|
+
.map(([id]) => id)
|
|
51
|
+
.sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Uncited: catalog has an entry but zero skill cites it.
|
|
55
|
+
const uncited = {};
|
|
56
|
+
const catalogFieldMap = {
|
|
57
|
+
cwe_refs: catalogs.cwe,
|
|
58
|
+
atlas_refs: catalogs.atlas,
|
|
59
|
+
d3fend_refs: catalogs.d3fend,
|
|
60
|
+
framework_gaps: catalogs.frameworkGaps,
|
|
61
|
+
rfc_refs: catalogs.rfc,
|
|
62
|
+
dlp_refs: catalogs.dlp,
|
|
63
|
+
};
|
|
64
|
+
for (const [field, cat] of Object.entries(catalogFieldMap)) {
|
|
65
|
+
if (!cat) continue;
|
|
66
|
+
const inCatalog = Object.keys(cat).filter((k) => !k.startsWith("_"));
|
|
67
|
+
uncited[field] = inCatalog.filter((id) => !counts[field][id]).sort();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// attack_refs has no catalog file (uses MITRE upstream directly), so no
|
|
71
|
+
// uncited table for it — only counts.
|
|
72
|
+
|
|
73
|
+
const topCited = {};
|
|
74
|
+
for (const f of fields) topCited[f] = topN(f);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
_meta: {
|
|
78
|
+
schema_version: "1.0.0",
|
|
79
|
+
note: "Citation-count tables per catalog field. top_cited surfaces load-bearing entries; orphan_adjacent identifies entries cited by exactly one skill; uncited identifies catalog entries with zero skill references (review whether they should be culled or whether a skill should pick them up).",
|
|
80
|
+
fields_indexed: fields,
|
|
81
|
+
},
|
|
82
|
+
counts,
|
|
83
|
+
top_cited: topCited,
|
|
84
|
+
orphan_adjacent: orphanAdjacent,
|
|
85
|
+
uncited,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { buildFrequency };
|