@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,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/jurisdiction-clocks.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/jurisdiction-clocks.json` — the normalized
|
|
6
|
+
* jurisdiction × obligation × clock matrix. Today consumers asking
|
|
7
|
+
* "what's the breach-notification clock in jurisdiction X?" have to
|
|
8
|
+
* scan `data/global-frameworks.json` and pull `notification_sla` off
|
|
9
|
+
* specific framework entries. This index flattens the dimension.
|
|
10
|
+
*
|
|
11
|
+
* Obligation types covered:
|
|
12
|
+
* - breach_notification (hours from awareness)
|
|
13
|
+
* - patch_sla (hours from disclosure for Critical/High)
|
|
14
|
+
* - incident_reporting (regulator + clock + trigger)
|
|
15
|
+
*
|
|
16
|
+
* Per-jurisdiction shape:
|
|
17
|
+
* {
|
|
18
|
+
* jurisdiction_name:
|
|
19
|
+
* frameworks: {
|
|
20
|
+
* <fwName>: {
|
|
21
|
+
* authority,
|
|
22
|
+
* breach_notification: { hours, trigger, stages?, source }
|
|
23
|
+
* patch_sla: { hours, note?, source }
|
|
24
|
+
* incident_reporting: { hours, trigger, source }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* fastest_breach_notification: { hours, framework } // null when none specified
|
|
28
|
+
* fastest_patch_sla: { hours, framework }
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
function buildJurisdictionClocks({ globalFrameworks }) {
|
|
33
|
+
const out = {};
|
|
34
|
+
const jurisdictionCodes = Object.keys(globalFrameworks).filter(
|
|
35
|
+
(k) => !k.startsWith("_") && k !== "GLOBAL"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
for (const code of jurisdictionCodes) {
|
|
39
|
+
const j = globalFrameworks[code];
|
|
40
|
+
const frameworks = j.frameworks || {};
|
|
41
|
+
const flat = {};
|
|
42
|
+
let fastestNotif = null;
|
|
43
|
+
let fastestPatch = null;
|
|
44
|
+
|
|
45
|
+
for (const [fwName, fw] of Object.entries(frameworks)) {
|
|
46
|
+
const entry = {};
|
|
47
|
+
if (typeof fw.notification_sla === "number" || fw.notification_trigger) {
|
|
48
|
+
entry.breach_notification = {
|
|
49
|
+
hours: typeof fw.notification_sla === "number" ? fw.notification_sla : null,
|
|
50
|
+
trigger: fw.notification_trigger || null,
|
|
51
|
+
stages: fw.notification_stages || null,
|
|
52
|
+
source: fw.source || null,
|
|
53
|
+
authority: fw.authority || null,
|
|
54
|
+
};
|
|
55
|
+
if (typeof fw.notification_sla === "number") {
|
|
56
|
+
if (!fastestNotif || fw.notification_sla < fastestNotif.hours) {
|
|
57
|
+
fastestNotif = { hours: fw.notification_sla, framework: fwName };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (typeof fw.patch_sla === "number") {
|
|
62
|
+
entry.patch_sla = {
|
|
63
|
+
hours: fw.patch_sla,
|
|
64
|
+
note: fw.patch_sla_note || null,
|
|
65
|
+
source: fw.source || null,
|
|
66
|
+
authority: fw.authority || null,
|
|
67
|
+
};
|
|
68
|
+
if (!fastestPatch || fw.patch_sla < fastestPatch.hours) {
|
|
69
|
+
fastestPatch = { hours: fw.patch_sla, framework: fwName };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(entry).length > 0) {
|
|
73
|
+
entry.authority = fw.authority || null;
|
|
74
|
+
flat[fwName] = entry;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Object.keys(flat).length === 0) continue;
|
|
79
|
+
|
|
80
|
+
out[code] = {
|
|
81
|
+
jurisdiction_name: j.jurisdiction || code,
|
|
82
|
+
frameworks: flat,
|
|
83
|
+
fastest_breach_notification: fastestNotif,
|
|
84
|
+
fastest_patch_sla: fastestPatch,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Cross-jurisdiction summary slices for quick consumer lookups.
|
|
89
|
+
const allNotifs = [];
|
|
90
|
+
const allPatch = [];
|
|
91
|
+
for (const [code, j] of Object.entries(out)) {
|
|
92
|
+
for (const [fwName, fw] of Object.entries(j.frameworks)) {
|
|
93
|
+
if (fw.breach_notification?.hours != null) {
|
|
94
|
+
allNotifs.push({
|
|
95
|
+
jurisdiction: code,
|
|
96
|
+
framework: fwName,
|
|
97
|
+
hours: fw.breach_notification.hours,
|
|
98
|
+
trigger: fw.breach_notification.trigger,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (fw.patch_sla?.hours != null) {
|
|
102
|
+
allPatch.push({
|
|
103
|
+
jurisdiction: code,
|
|
104
|
+
framework: fwName,
|
|
105
|
+
hours: fw.patch_sla.hours,
|
|
106
|
+
note: fw.patch_sla.note,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
allNotifs.sort((a, b) => a.hours - b.hours);
|
|
112
|
+
allPatch.sort((a, b) => a.hours - b.hours);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
_meta: {
|
|
116
|
+
schema_version: "1.0.0",
|
|
117
|
+
note: "Normalized obligation matrix derived from data/global-frameworks.json. All times in hours. breach_notification.hours is null for jurisdictions that require notification only on a discretionary trigger.",
|
|
118
|
+
jurisdiction_count: Object.keys(out).length,
|
|
119
|
+
},
|
|
120
|
+
by_jurisdiction: out,
|
|
121
|
+
sorted_by_breach_notification_hours: allNotifs,
|
|
122
|
+
sorted_by_patch_sla_hours: allPatch,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { buildJurisdictionClocks };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/recipes.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/recipes.json` — curated skill sequences for the
|
|
6
|
+
* most common operator use cases. Each recipe is a vetted chain of skills
|
|
7
|
+
* to invoke, in order, with a brief rationale per step. Saves the
|
|
8
|
+
* researcher skill from re-deriving these on every request.
|
|
9
|
+
*
|
|
10
|
+
* Recipes are content (not derived), so this builder is mostly a
|
|
11
|
+
* declarative table. The build step validates that every referenced
|
|
12
|
+
* skill exists in the manifest, so a renamed/deleted skill surfaces as a
|
|
13
|
+
* build error.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const RECIPES = [
|
|
17
|
+
{
|
|
18
|
+
id: "ai-red-team-prep",
|
|
19
|
+
name: "AI Red Team Engagement Prep",
|
|
20
|
+
description: "Stand up the AI security baseline before red-teaming a model or AI feature. Covers attack surface, MCP plugin trust, prompt-injection class, agentic actions, RAG provenance.",
|
|
21
|
+
when_to_use: "Before scoping or executing a red-team engagement against a model, agentic system, or AI feature.",
|
|
22
|
+
typical_jurisdictions: ["US", "EU", "UK", "GLOBAL"],
|
|
23
|
+
steps: [
|
|
24
|
+
{ skill: "ai-attack-surface", why: "Comprehensive attack-surface inventory mapped to ATLAS v5.1.0 with gap flags." },
|
|
25
|
+
{ skill: "ai-c2-detection", why: "Detection coverage for AI-as-C2 (PROMPTFLUX / SesameOp / AI-API egress) before testing." },
|
|
26
|
+
{ skill: "mcp-agent-trust", why: "MCP server trust boundary for the engineering toolchain side of the surface." },
|
|
27
|
+
{ skill: "rag-pipeline-security", why: "RAG ingestion provenance + prompt-injection chain coverage." },
|
|
28
|
+
{ skill: "threat-modeling-methodology", why: "Frame the engagement scope using a current methodology (PASTA/LINDDUN-AI variant)." },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "pci-dss-4-audit-defense",
|
|
33
|
+
name: "PCI DSS 4.0 Audit Defense",
|
|
34
|
+
description: "Show real coverage of PCI 4.0 sections that auditors most commonly mis-read as 'compliant'. Focus on 6.3.3, 11.4, 12.10.",
|
|
35
|
+
when_to_use: "Mock audit, ROC drafting, or QSA pre-engagement review.",
|
|
36
|
+
typical_jurisdictions: ["US", "EU", "UK", "AU", "SG", "GLOBAL"],
|
|
37
|
+
steps: [
|
|
38
|
+
{ skill: "compliance-theater", why: "Detect the 7 documented theater patterns that pass PCI but leave exposure." },
|
|
39
|
+
{ skill: "framework-gap-analysis", why: "Pre-analyzed PCI control gaps and what to substitute as compensating controls." },
|
|
40
|
+
{ skill: "incident-response-playbook", why: "PCI 12.10 incident-response coverage that maps to ATT&CK + AI-class incidents." },
|
|
41
|
+
{ skill: "supply-chain-integrity", why: "PCI 6.4 covers SDLC; supply-chain integrity covers what 6.4 omits (MCP / coding assistants)." },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "federal-incident-response",
|
|
46
|
+
name: "Federal Incident Response (FISMA + CISA + FedRAMP)",
|
|
47
|
+
description: "Coordinate an incident affecting a federal system. Layered SLAs: CISA 8h, OMB M-22-09 access-control breach, FedRAMP continuous-monitoring obligations.",
|
|
48
|
+
when_to_use: "Active or suspected incident on a federal-system boundary, FedRAMP CSP, or DoD/CMMC scope.",
|
|
49
|
+
typical_jurisdictions: ["US"],
|
|
50
|
+
steps: [
|
|
51
|
+
{ skill: "incident-response-playbook", why: "PICERL + cross-jurisdiction notification clocks (CISA 8h initial, OMB, FedRAMP)." },
|
|
52
|
+
{ skill: "sector-federal-government", why: "Federal-specific framework obligations (NIST 800-53, FedRAMP, CMMC, OMB memos)." },
|
|
53
|
+
{ skill: "coordinated-vuln-disclosure", why: "If a vulnerability triggered the incident, CVD coordination via CISA's process." },
|
|
54
|
+
{ skill: "policy-exception-gen", why: "Time-bound exceptions for any control bypassed under emergency response." },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "dora-tlpt-scoping",
|
|
59
|
+
name: "DORA TLPT Scoping (EU Financial Services)",
|
|
60
|
+
description: "Scope a Threat-Led Penetration Test under DORA Art. 26-27 for an in-scope EU financial entity.",
|
|
61
|
+
when_to_use: "Annual or triennial DORA TLPT scoping cycle, or after a critical-third-party change.",
|
|
62
|
+
typical_jurisdictions: ["EU"],
|
|
63
|
+
steps: [
|
|
64
|
+
{ skill: "sector-financial", why: "DORA-specific scoping requirements (Art. 26-27) and critical-function inventory." },
|
|
65
|
+
{ skill: "attack-surface-pentest", why: "Pen-test methodology aligned to ATT&CK Enterprise + financial-services adversary profile." },
|
|
66
|
+
{ skill: "threat-modeling-methodology", why: "Threat-led methodology (ATT&CK + ATLAS variants), red-team rules of engagement." },
|
|
67
|
+
{ skill: "supply-chain-integrity", why: "ICT third-party scope (DORA Art. 28-30) — TLPT must cover critical ICT third parties." },
|
|
68
|
+
{ skill: "incident-response-playbook", why: "Findings → DORA Art. 19 major-incident reporting (4h / 24h / 30d) if a real incident is uncovered." },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "k12-edtech-privacy-review",
|
|
73
|
+
name: "K-12 EdTech Privacy & Safety Review",
|
|
74
|
+
description: "Review an EdTech product or pilot for a K-12 deployment. COPPA, FERPA, UK Children's Code, EU AI Act Annex III, AU eSafety, age-appropriate design.",
|
|
75
|
+
when_to_use: "Procurement review for an EdTech vendor, K-12 board approval cycle, or post-deployment audit.",
|
|
76
|
+
typical_jurisdictions: ["US", "UK", "EU", "AU"],
|
|
77
|
+
steps: [
|
|
78
|
+
{ skill: "age-gates-child-safety", why: "Age-appropriate design code, age estimation/assurance, dark-pattern audit, AI Act Annex III." },
|
|
79
|
+
{ skill: "dlp-gap-analysis", why: "Student record / PII flow audit; FERPA + state student-privacy laws (CA SOPIPA, NY Ed Law 2-D)." },
|
|
80
|
+
{ skill: "identity-assurance", why: "Auth posture for child accounts — NIST 800-63B IAL2/AAL2 mismatch for age-restricted UIs." },
|
|
81
|
+
{ skill: "compliance-theater", why: "K-12 vendor attestations are a known theater hotspot; surface the gaps." },
|
|
82
|
+
{ skill: "rag-pipeline-security", why: "If the EdTech includes any AI/LLM features (the common case), RAG + prompt-injection class." },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "ransomware-tabletop",
|
|
87
|
+
name: "Ransomware Tabletop Exercise",
|
|
88
|
+
description: "Run a tabletop covering modern ransomware: double-extortion, data exfiltration before encryption, OT impact, AI-assisted negotiation messages, multi-jurisdictional notification.",
|
|
89
|
+
when_to_use: "Annual tabletop, post-incident replay, or executive-team readiness review.",
|
|
90
|
+
typical_jurisdictions: ["US", "EU", "UK", "AU", "CA", "SG", "JP", "GLOBAL"],
|
|
91
|
+
steps: [
|
|
92
|
+
{ skill: "incident-response-playbook", why: "PICERL + cross-jurisdiction breach notification (GDPR 72h, NIS2 24h/72h, US state laws, NYDFS 72h)." },
|
|
93
|
+
{ skill: "sector-financial", why: "If financial-sector entity: ransom-payment disclosure (NYDFS 24h), DORA Art. 19, SAMA, MAS TRM." },
|
|
94
|
+
{ skill: "ot-ics-security", why: "If OT/critical infrastructure exposure: CIRCIA 24h, NERC CIP, IEC 62443, recovery without paying." },
|
|
95
|
+
{ skill: "email-security-anti-phishing", why: "Initial access vector typical of ransomware; AI-generated phishing baseline." },
|
|
96
|
+
{ skill: "defensive-countermeasure-mapping", why: "Map exercise findings to D3FEND countermeasures with verifiable telemetry." },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "new-cve-drop-triage",
|
|
101
|
+
name: "New CVE Drop Triage",
|
|
102
|
+
description: "Standardized first 30 minutes after a high-impact CVE drop. RWEP score, KEV check, evidence chain, dispatched to the right specialized skill.",
|
|
103
|
+
when_to_use: "Operator receives a CVE number from an advisory, news item, or vendor notice and needs to know what to do.",
|
|
104
|
+
typical_jurisdictions: ["GLOBAL"],
|
|
105
|
+
steps: [
|
|
106
|
+
{ skill: "researcher", why: "Entry-point dispatcher: anchors the CVE in the catalog, computes RWEP, routes to the right specialist." },
|
|
107
|
+
{ skill: "exploit-scoring", why: "RWEP score + factor breakdown, KEV + EPSS posture, weaponization timeline estimate." },
|
|
108
|
+
{ skill: "framework-gap-analysis", why: "Pre-analyzed control gaps that auditor-track frameworks have for this CVE class." },
|
|
109
|
+
{ skill: "incident-response-playbook", why: "If active exploitation suspected: notification clocks + PICERL kickoff." },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "open-source-dep-triage",
|
|
114
|
+
name: "Open-Source Dependency Triage",
|
|
115
|
+
description: "Triage a new advisory in a transitive npm/PyPI/Maven dependency: scope, SBOM, exploit-availability, action.",
|
|
116
|
+
when_to_use: "Dependabot or Renovate fires a critical/high finding, or a public advisory mentions a dependency you ship.",
|
|
117
|
+
typical_jurisdictions: ["GLOBAL"],
|
|
118
|
+
steps: [
|
|
119
|
+
{ skill: "supply-chain-integrity", why: "SBOM scoping, transitive-dep evaluation, build-system trust, signed-attestation check." },
|
|
120
|
+
{ skill: "exploit-scoring", why: "RWEP + EPSS + KEV decide whether this is a 'drop everything' or 'normal cycle' patch." },
|
|
121
|
+
{ skill: "coordinated-vuln-disclosure", why: "If the advisory is pre-coordination, hold the disclosure until the upstream window closes." },
|
|
122
|
+
{ skill: "fuzz-testing-strategy", why: "If you maintain a fork or critical patch: prioritized fuzz target the bug class." },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
function buildRecipes({ skills }) {
|
|
128
|
+
const skillNames = new Set(skills.map((s) => s.name));
|
|
129
|
+
const errors = [];
|
|
130
|
+
for (const r of RECIPES) {
|
|
131
|
+
for (const step of r.steps) {
|
|
132
|
+
if (!skillNames.has(step.skill)) {
|
|
133
|
+
errors.push(`recipe ${r.id}: references unknown skill "${step.skill}"`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (errors.length) {
|
|
138
|
+
throw new Error("recipes.js: " + errors.join("; "));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add token-budget hints once we have them — token-budget builder may run
|
|
142
|
+
// after this one, so we just emit the per-step list and let consumers
|
|
143
|
+
// join to token-budget.json. The skill_count is cheap to include here.
|
|
144
|
+
const out = {
|
|
145
|
+
_meta: {
|
|
146
|
+
schema_version: "1.0.0",
|
|
147
|
+
note: "Curated skill sequences for common operator use cases. Each step is a vetted skill plus the reason it belongs in the chain. Cross-reference token-budget.json to estimate context cost.",
|
|
148
|
+
recipe_count: RECIPES.length,
|
|
149
|
+
},
|
|
150
|
+
recipes: RECIPES.map((r) => ({
|
|
151
|
+
...r,
|
|
152
|
+
skill_count: r.steps.length,
|
|
153
|
+
skill_chain: r.steps.map((s) => s.skill),
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { buildRecipes };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/section-offsets.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/section-offsets.json` — for each skill, the byte
|
|
6
|
+
* + line offsets of every H2 section header in the body. AI consumers can
|
|
7
|
+
* slice a single section (e.g. "Compliance Theater Check") from disk
|
|
8
|
+
* without parsing the full skill file.
|
|
9
|
+
*
|
|
10
|
+
* Per-skill shape:
|
|
11
|
+
* {
|
|
12
|
+
* path: "skills/<name>/skill.md",
|
|
13
|
+
* total_bytes: n,
|
|
14
|
+
* total_lines: n,
|
|
15
|
+
* frontmatter: { byte_start, byte_end, line_start, line_end },
|
|
16
|
+
* sections: [
|
|
17
|
+
* {
|
|
18
|
+
* name: raw H2 text (e.g. "Threat Context (mid-2026)")
|
|
19
|
+
* normalized_name: collapsed for lookup ("threat-context")
|
|
20
|
+
* line: 1-based line number of the "## …" header
|
|
21
|
+
* byte_start: byte offset of the "## " character
|
|
22
|
+
* byte_end: byte offset where the next H2 begins (or EOF)
|
|
23
|
+
* bytes: byte_end - byte_start
|
|
24
|
+
* h3_count: number of "### " headers contained
|
|
25
|
+
* },
|
|
26
|
+
* ...
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* The normalized_name strips parenthetical qualifiers and common phrasings
|
|
31
|
+
* so consumers can request a canonical section name without caring about
|
|
32
|
+
* formatting drift.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require("fs");
|
|
36
|
+
const path = require("path");
|
|
37
|
+
|
|
38
|
+
// Recognized canonical section anchors. Multiple raw H2 phrasings map to one
|
|
39
|
+
// normalized name — see grep survey of skills/* for the variant phrasings.
|
|
40
|
+
const NORMALIZERS = [
|
|
41
|
+
[/threat\s*context/i, "threat-context"],
|
|
42
|
+
[/framework\s*lag\s*declaration/i, "framework-lag-declaration"],
|
|
43
|
+
[/ttp\s*mapping/i, "ttp-mapping"],
|
|
44
|
+
[/exploit\s*availability\s*matrix/i, "exploit-availability-matrix"],
|
|
45
|
+
[/compliance\s*theater\s*check/i, "compliance-theater-check"],
|
|
46
|
+
[/analysis\s*procedure/i, "analysis-procedure"],
|
|
47
|
+
[/defensive\s*countermeasure\s*mapping/i, "defensive-countermeasure-mapping"],
|
|
48
|
+
[/output\s*format/i, "output-format"],
|
|
49
|
+
[/hand-?off/i, "hand-off"],
|
|
50
|
+
[/detection\s*rules?/i, "detection-rules"],
|
|
51
|
+
[/exposure\s*assessment/i, "exposure-assessment"],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function normalize(headerText) {
|
|
55
|
+
const stripped = headerText.replace(/^##\s+/, "").trim();
|
|
56
|
+
for (const [re, canonical] of NORMALIZERS) {
|
|
57
|
+
if (re.test(stripped)) return canonical;
|
|
58
|
+
}
|
|
59
|
+
// Fall back: slug.
|
|
60
|
+
return stripped
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
63
|
+
.replace(/^-|-$/g, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildOne(absPath, relPath) {
|
|
67
|
+
const buf = fs.readFileSync(absPath);
|
|
68
|
+
const totalBytes = buf.length;
|
|
69
|
+
const text = buf.toString("utf8");
|
|
70
|
+
const lines = text.split(/\r?\n/);
|
|
71
|
+
const lineByteOffsets = [];
|
|
72
|
+
let cursor = 0;
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
lineByteOffsets.push(cursor);
|
|
75
|
+
// +1 for the newline. Counts as one byte for LF; CRLF would skew slightly
|
|
76
|
+
// but the file is written via the project's tooling which is LF-uniform.
|
|
77
|
+
cursor += Buffer.byteLength(line, "utf8") + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Frontmatter: lines between the first "---" and the second "---".
|
|
81
|
+
let fmLineStart = -1, fmLineEnd = -1;
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
if (lines[i].trim() === "---") {
|
|
84
|
+
if (fmLineStart === -1) fmLineStart = i;
|
|
85
|
+
else if (fmLineEnd === -1) {
|
|
86
|
+
fmLineEnd = i;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const frontmatter = fmLineStart >= 0 && fmLineEnd > fmLineStart
|
|
92
|
+
? {
|
|
93
|
+
line_start: fmLineStart + 1,
|
|
94
|
+
line_end: fmLineEnd + 1,
|
|
95
|
+
byte_start: lineByteOffsets[fmLineStart],
|
|
96
|
+
byte_end: lineByteOffsets[Math.min(fmLineEnd + 1, lineByteOffsets.length - 1)] || totalBytes,
|
|
97
|
+
}
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
// H2 headers — only those outside fenced code blocks. Skill bodies often
|
|
101
|
+
// contain "## Foo" lines inside ```...``` blocks as output templates; those
|
|
102
|
+
// are not real sections.
|
|
103
|
+
const h2 = [];
|
|
104
|
+
let inFence = false;
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
if (/^```/.test(lines[i])) {
|
|
107
|
+
inFence = !inFence;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!inFence && /^## /.test(lines[i])) {
|
|
111
|
+
h2.push({ line: i + 1, idx: i, raw: lines[i].trim() });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sections = [];
|
|
116
|
+
for (let j = 0; j < h2.length; j++) {
|
|
117
|
+
const cur = h2[j];
|
|
118
|
+
const next = h2[j + 1];
|
|
119
|
+
const startByte = lineByteOffsets[cur.idx];
|
|
120
|
+
const endByte = next ? lineByteOffsets[next.idx] : totalBytes;
|
|
121
|
+
// Count H3 within this section.
|
|
122
|
+
const endIdx = next ? next.idx : lines.length;
|
|
123
|
+
let h3Count = 0;
|
|
124
|
+
for (let k = cur.idx + 1; k < endIdx; k++) {
|
|
125
|
+
if (/^### /.test(lines[k])) h3Count++;
|
|
126
|
+
}
|
|
127
|
+
sections.push({
|
|
128
|
+
name: cur.raw.replace(/^##\s+/, ""),
|
|
129
|
+
normalized_name: normalize(cur.raw),
|
|
130
|
+
line: cur.line,
|
|
131
|
+
byte_start: startByte,
|
|
132
|
+
byte_end: endByte,
|
|
133
|
+
bytes: endByte - startByte,
|
|
134
|
+
h3_count: h3Count,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
path: relPath,
|
|
140
|
+
total_bytes: totalBytes,
|
|
141
|
+
total_lines: lines.length,
|
|
142
|
+
frontmatter,
|
|
143
|
+
sections,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildSectionOffsets({ root, skills }) {
|
|
148
|
+
const out = {};
|
|
149
|
+
for (const s of skills) {
|
|
150
|
+
out[s.name] = buildOne(path.join(root, s.path), s.path);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
_meta: {
|
|
154
|
+
schema_version: "1.0.0",
|
|
155
|
+
note: "Per-skill byte/line offsets of every H2 section. Use byte_start/byte_end to slice a specific section. normalized_name collapses phrasing variants (e.g. 'Threat Context (mid-2026)' → 'threat-context').",
|
|
156
|
+
canonical_section_names: NORMALIZERS.map(([, name]) => name),
|
|
157
|
+
},
|
|
158
|
+
skills: out,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { buildSectionOffsets };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* scripts/builders/stale-content.js
|
|
4
|
+
*
|
|
5
|
+
* Builds `data/_indexes/stale-content.json` — surfaces stale or
|
|
6
|
+
* drifted references the audit-cross-skill script catches at run time,
|
|
7
|
+
* persisted as a JSON artifact so CI / dashboards / downstream tools can
|
|
8
|
+
* read the same view without invoking the script.
|
|
9
|
+
*
|
|
10
|
+
* Checks performed here (subset of audit-cross-skill that's relevant to
|
|
11
|
+
* the index layer, deterministic across reruns):
|
|
12
|
+
*
|
|
13
|
+
* - Skill bodies referencing renamed-skill tokens (e.g. age-gates-minor-*)
|
|
14
|
+
* - README badge counts vs. live counts
|
|
15
|
+
* - "Researcher routes to N skills" claim vs. live count
|
|
16
|
+
* - Skills with last_threat_review older than 180 days from
|
|
17
|
+
* manifest.threat_review_date (gives a stale-content snapshot)
|
|
18
|
+
* - Catalog _meta.last_verified entries older than freshness_policy.stale_after_days
|
|
19
|
+
* - Forward_watch items mentioning dates that have already passed
|
|
20
|
+
*
|
|
21
|
+
* Each finding is { severity, category, artifact, detail }.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
|
|
27
|
+
const RENAMED_SKILL_TOKENS = [
|
|
28
|
+
// Historical names that should no longer appear in the corpus. Update on
|
|
29
|
+
// each rename — paired with audit-cross-skill.js stale-renamed-skill check.
|
|
30
|
+
"age-gates-minor-safeguarding",
|
|
31
|
+
"age-gates-minor",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function findInBody(body, needle) {
|
|
35
|
+
// Find occurrences with surrounding context. Returns up to first 3 line refs.
|
|
36
|
+
const lines = body.split(/\r?\n/);
|
|
37
|
+
const refs = [];
|
|
38
|
+
for (let i = 0; i < lines.length && refs.length < 3; i++) {
|
|
39
|
+
if (lines[i].includes(needle)) refs.push({ line: i + 1, content: lines[i].trim().slice(0, 120) });
|
|
40
|
+
}
|
|
41
|
+
return refs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildStaleContent({ root, manifest, skills, catalogFiles }) {
|
|
45
|
+
const findings = [];
|
|
46
|
+
const refDate = new Date((manifest.threat_review_date || "2026-05-01") + "T00:00:00Z");
|
|
47
|
+
|
|
48
|
+
// 1. Stale-renamed-skill tokens
|
|
49
|
+
for (const s of skills) {
|
|
50
|
+
const body = fs.readFileSync(path.join(root, s.path), "utf8");
|
|
51
|
+
for (const tok of RENAMED_SKILL_TOKENS) {
|
|
52
|
+
const refs = findInBody(body, tok);
|
|
53
|
+
if (refs.length > 0) {
|
|
54
|
+
findings.push({
|
|
55
|
+
severity: "high",
|
|
56
|
+
category: "stale_renamed_skill",
|
|
57
|
+
artifact: s.path,
|
|
58
|
+
detail: `references retired skill token "${tok}"`,
|
|
59
|
+
refs,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. README badge counts vs. live counts
|
|
66
|
+
const readmePath = path.join(root, "README.md");
|
|
67
|
+
if (fs.existsSync(readmePath)) {
|
|
68
|
+
const readme = fs.readFileSync(readmePath, "utf8");
|
|
69
|
+
const skillsBadge = readme.match(/skills-(\d+)-/);
|
|
70
|
+
const jurisdictionsBadge = readme.match(/jurisdictions?-(\d+)-/i);
|
|
71
|
+
const liveJurisdictions = (() => {
|
|
72
|
+
try {
|
|
73
|
+
const gf = JSON.parse(fs.readFileSync(path.join(root, "data/global-frameworks.json"), "utf8"));
|
|
74
|
+
return Object.keys(gf).filter((k) => !k.startsWith("_") && k !== "GLOBAL").length;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
if (skillsBadge && Number(skillsBadge[1]) !== skills.length) {
|
|
80
|
+
findings.push({
|
|
81
|
+
severity: "medium",
|
|
82
|
+
category: "badge_drift",
|
|
83
|
+
artifact: "README.md",
|
|
84
|
+
detail: `skills badge shows ${skillsBadge[1]}, manifest has ${skills.length}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (jurisdictionsBadge && liveJurisdictions && Number(jurisdictionsBadge[1]) !== liveJurisdictions) {
|
|
88
|
+
findings.push({
|
|
89
|
+
severity: "medium",
|
|
90
|
+
category: "badge_drift",
|
|
91
|
+
artifact: "README.md",
|
|
92
|
+
detail: `jurisdictions badge shows ${jurisdictionsBadge[1]}, live count is ${liveJurisdictions}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Researcher dispatch count claim
|
|
98
|
+
const researcherPath = path.join(root, "skills/researcher/skill.md");
|
|
99
|
+
if (fs.existsSync(researcherPath)) {
|
|
100
|
+
const r = fs.readFileSync(researcherPath, "utf8");
|
|
101
|
+
const claimMatch = r.match(/inventory of (\d+) specialized/);
|
|
102
|
+
if (claimMatch) {
|
|
103
|
+
const claimed = Number(claimMatch[1]);
|
|
104
|
+
const live = skills.length - 1; // researcher excluded from its own dispatch table
|
|
105
|
+
if (claimed !== live) {
|
|
106
|
+
findings.push({
|
|
107
|
+
severity: "medium",
|
|
108
|
+
category: "researcher_claim_drift",
|
|
109
|
+
artifact: "skills/researcher/skill.md",
|
|
110
|
+
detail: `claims ${claimed} specialized skills downstream; live count is ${live}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Skills with > 180 days since review (against reference date)
|
|
117
|
+
for (const s of skills) {
|
|
118
|
+
if (!s.last_threat_review) continue;
|
|
119
|
+
const ageDays = Math.floor(
|
|
120
|
+
(refDate - new Date(s.last_threat_review + "T00:00:00Z")) / 86400000
|
|
121
|
+
);
|
|
122
|
+
if (ageDays > 180) {
|
|
123
|
+
findings.push({
|
|
124
|
+
severity: "low",
|
|
125
|
+
category: "skill_review_stale",
|
|
126
|
+
artifact: s.path,
|
|
127
|
+
detail: `last_threat_review ${s.last_threat_review} is ${ageDays} days before manifest.threat_review_date`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. Catalog last_verified entries older than freshness_policy.stale_after_days
|
|
133
|
+
for (const rel of catalogFiles) {
|
|
134
|
+
const abs = path.join(root, rel);
|
|
135
|
+
try {
|
|
136
|
+
const j = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
137
|
+
const meta = j._meta || {};
|
|
138
|
+
const policy = meta.freshness_policy || null;
|
|
139
|
+
if (!policy?.stale_after_days) continue;
|
|
140
|
+
const last = meta.last_updated || meta.last_verified;
|
|
141
|
+
if (!last) continue;
|
|
142
|
+
const ageDays = Math.floor((refDate - new Date(last + "T00:00:00Z")) / 86400000);
|
|
143
|
+
if (ageDays > policy.stale_after_days) {
|
|
144
|
+
findings.push({
|
|
145
|
+
severity: "medium",
|
|
146
|
+
category: "catalog_stale",
|
|
147
|
+
artifact: rel,
|
|
148
|
+
detail: `last_updated ${last} is ${ageDays} days old; freshness_policy.stale_after_days is ${policy.stale_after_days}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// ignore parse errors — caught elsewhere
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const bySeverity = { high: 0, medium: 0, low: 0 };
|
|
157
|
+
for (const f of findings) bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
_meta: {
|
|
161
|
+
schema_version: "1.0.0",
|
|
162
|
+
reference_date: manifest.threat_review_date || null,
|
|
163
|
+
note: "Stale-content snapshot derived from audit-cross-skill checks. Re-runs of build-indexes against the same inputs produce byte-identical output (reference_date is manifest.threat_review_date, not 'now'). audit-cross-skill.js remains the canonical interactive audit.",
|
|
164
|
+
finding_count: findings.length,
|
|
165
|
+
by_severity: bySeverity,
|
|
166
|
+
},
|
|
167
|
+
findings,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { buildStaleContent };
|