@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,328 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Framework gap analysis and lag scoring library.
|
|
5
|
+
* Operates on data/framework-control-gaps.json and data/global-frameworks.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const THEATER_PATTERNS = {
|
|
9
|
+
patch_management: {
|
|
10
|
+
name: 'Patch Management Theater',
|
|
11
|
+
description: 'Organization meets framework patch SLA but remains exposed to active exploitation.',
|
|
12
|
+
detection_test: (control, cveData) => {
|
|
13
|
+
if (!control.real_requirement) return false;
|
|
14
|
+
const hasExploitedCVE = control.evidence_cves?.some(id => {
|
|
15
|
+
const cve = cveData[id];
|
|
16
|
+
return cve && (cve.cisa_kev || cve.active_exploitation === 'confirmed');
|
|
17
|
+
});
|
|
18
|
+
return hasExploitedCVE && control.status === 'open';
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
access_control_ai: {
|
|
22
|
+
name: 'AI Access Control Theater',
|
|
23
|
+
description: 'Service account authorization is compliant; prompt injection bypasses it entirely.',
|
|
24
|
+
detection_test: (control) => {
|
|
25
|
+
return control.control_id === 'AC-2' && control.status === 'open' &&
|
|
26
|
+
control.misses?.some(m => m.toLowerCase().includes('prompt injection'));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
vendor_management_ai: {
|
|
30
|
+
name: 'Vendor Management Theater',
|
|
31
|
+
description: 'Vendor management controls pass audit but do not reach AI tool plugins (MCP servers).',
|
|
32
|
+
detection_test: (control) => {
|
|
33
|
+
return (control.control_id === 'SA-12' || control.control_id === 'CC9') &&
|
|
34
|
+
control.status === 'open' &&
|
|
35
|
+
control.misses?.some(m => m.toLowerCase().includes('mcp'));
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
malware_protection_ai: {
|
|
39
|
+
name: 'Malware Protection Theater',
|
|
40
|
+
description: 'Signature-based malware protection is compliant; AI-generated novel code evades all signatures.',
|
|
41
|
+
detection_test: (control) => {
|
|
42
|
+
return control.control_id === 'SI-3' && control.status === 'open' &&
|
|
43
|
+
control.misses?.some(m => m.toLowerCase().includes('promptflux') || m.toLowerCase().includes('ai-generated'));
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
supply_chain_ai: {
|
|
47
|
+
name: 'Supply Chain Theater',
|
|
48
|
+
description: 'Software supply chain controls pass audit; developer-installed AI plugins are outside scope.',
|
|
49
|
+
detection_test: (control) => {
|
|
50
|
+
return control.status === 'open' &&
|
|
51
|
+
control.misses?.some(m => m.toLowerCase().includes('mcp server') && m.toLowerCase().includes('supply chain'));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
encryption_pqc: {
|
|
55
|
+
name: 'Encryption Theater',
|
|
56
|
+
description: 'Encryption controls are compliant with classical algorithms; HNDL exposure is unaddressed.',
|
|
57
|
+
detection_test: (control) => {
|
|
58
|
+
return control.status === 'open' &&
|
|
59
|
+
control.misses?.some(m => m.toLowerCase().includes('quantum') || m.toLowerCase().includes('pqc'));
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
detection_ai: {
|
|
63
|
+
name: 'Detection Theater',
|
|
64
|
+
description: 'Security monitoring is compliant; AI C2 (SesameOp) and AI-querying malware are not detected.',
|
|
65
|
+
detection_test: (control) => {
|
|
66
|
+
return control.status === 'open' &&
|
|
67
|
+
control.misses?.some(m => m.toLowerCase().includes('ai api') || m.toLowerCase().includes('c2'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const LAG_WEIGHTS = {
|
|
73
|
+
patch_sla_days_vs_optimal: 0.30,
|
|
74
|
+
universal_gaps_count: 0.25,
|
|
75
|
+
ai_framework_coverage: 0.20,
|
|
76
|
+
pqc_requirement: 0.10,
|
|
77
|
+
notification_sla_hours: 0.15
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const OPTIMAL = {
|
|
81
|
+
patch_sla_days: 2,
|
|
82
|
+
notification_sla_hours: 4,
|
|
83
|
+
universal_gaps: 0
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Score how much a given framework lags behind current threat reality.
|
|
88
|
+
* Returns 0 (no lag) to 100 (complete theater).
|
|
89
|
+
*
|
|
90
|
+
* @param {string} frameworkId - Key in framework-control-gaps.json
|
|
91
|
+
* @param {object} controlGaps - Parsed framework-control-gaps.json
|
|
92
|
+
* @param {object} globalFrameworks - Parsed global-frameworks.json
|
|
93
|
+
* @returns {{ score: number, breakdown: object, label: string }}
|
|
94
|
+
*/
|
|
95
|
+
function lagScore(frameworkId, controlGaps, globalFrameworks) {
|
|
96
|
+
const gaps = Object.values(controlGaps).filter(g =>
|
|
97
|
+
g.framework?.includes(frameworkId) && g.status === 'open'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const universalGaps = Object.values(controlGaps).filter(g =>
|
|
101
|
+
g.framework === 'ALL' && g.status === 'open'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const frameworkData = _findFrameworkData(frameworkId, globalFrameworks);
|
|
105
|
+
|
|
106
|
+
const patchSlaScore = _scorePatchSla(frameworkData?.patch_sla);
|
|
107
|
+
const notifSlaScore = _scoreNotifSla(frameworkData?.notification_sla);
|
|
108
|
+
const aiCoverageScore = _scoreAiCoverage(frameworkData?.ai_coverage);
|
|
109
|
+
const pqcScore = frameworkData?.pqc_coverage === 'None' ? 100 : 30;
|
|
110
|
+
const universalGapScore = Math.min(100, universalGaps.length * 20);
|
|
111
|
+
|
|
112
|
+
const weighted =
|
|
113
|
+
patchSlaScore * LAG_WEIGHTS.patch_sla_days_vs_optimal +
|
|
114
|
+
universalGapScore * LAG_WEIGHTS.universal_gaps_count +
|
|
115
|
+
aiCoverageScore * LAG_WEIGHTS.ai_framework_coverage +
|
|
116
|
+
pqcScore * LAG_WEIGHTS.pqc_requirement +
|
|
117
|
+
notifSlaScore * LAG_WEIGHTS.notification_sla_hours;
|
|
118
|
+
|
|
119
|
+
const score = Math.round(Math.min(100, weighted));
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
score,
|
|
123
|
+
label: _lagLabel(score),
|
|
124
|
+
breakdown: {
|
|
125
|
+
patch_sla: { raw_days: frameworkData?.patch_sla ?? null, score: patchSlaScore },
|
|
126
|
+
notification_sla: { raw_hours: frameworkData?.notification_sla ?? null, score: notifSlaScore },
|
|
127
|
+
ai_coverage: { coverage: frameworkData?.ai_coverage ?? 'unknown', score: aiCoverageScore },
|
|
128
|
+
pqc_coverage: { coverage: frameworkData?.pqc_coverage ?? 'unknown', score: pqcScore },
|
|
129
|
+
universal_gaps: { count: universalGaps.length, score: universalGapScore },
|
|
130
|
+
framework_specific_gaps: gaps.length
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate a gap report for one or more frameworks vs. a threat scenario.
|
|
137
|
+
*
|
|
138
|
+
* @param {string[]} frameworkIds - Framework identifiers
|
|
139
|
+
* @param {string} threatScenario - Description of the threat or CVE ID
|
|
140
|
+
* @param {object} controlGaps - Parsed framework-control-gaps.json
|
|
141
|
+
* @param {object} cveCatalog - Parsed cve-catalog.json (optional)
|
|
142
|
+
* @returns {{ frameworks: object, universal_gaps: object[], theater_risks: object[] }}
|
|
143
|
+
*/
|
|
144
|
+
function gapReport(frameworkIds, threatScenario, controlGaps, cveCatalog = {}) {
|
|
145
|
+
const scenario = threatScenario.toLowerCase();
|
|
146
|
+
|
|
147
|
+
const relevantGaps = Object.entries(controlGaps).filter(([, gap]) => {
|
|
148
|
+
const misses = gap.misses?.join(' ').toLowerCase() ?? '';
|
|
149
|
+
const real = gap.real_requirement?.toLowerCase() ?? '';
|
|
150
|
+
return (
|
|
151
|
+
misses.includes(scenario) ||
|
|
152
|
+
real.includes(scenario) ||
|
|
153
|
+
gap.evidence_cves?.some(id => id.toLowerCase() === scenario)
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const universalGaps = Object.values(controlGaps).filter(g =>
|
|
158
|
+
g.framework === 'ALL' && g.status === 'open'
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const frameworkResults = {};
|
|
162
|
+
for (const id of frameworkIds) {
|
|
163
|
+
const frameworkGaps = relevantGaps.filter(([, g]) => g.framework?.includes(id));
|
|
164
|
+
frameworkResults[id] = {
|
|
165
|
+
gap_count: frameworkGaps.length,
|
|
166
|
+
gaps: frameworkGaps.map(([key, g]) => ({
|
|
167
|
+
id: key,
|
|
168
|
+
control: g.control_name,
|
|
169
|
+
real_requirement: g.real_requirement,
|
|
170
|
+
status: g.status
|
|
171
|
+
})),
|
|
172
|
+
theater_exposure: frameworkGaps.some(([, g]) => g.status === 'open')
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const theaterRisks = relevantGaps
|
|
177
|
+
.filter(([, g]) => g.status === 'open' && g.theater_pattern)
|
|
178
|
+
.map(([key, g]) => ({
|
|
179
|
+
control: key,
|
|
180
|
+
pattern: g.theater_pattern,
|
|
181
|
+
framework: g.framework
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
threat_scenario: threatScenario,
|
|
186
|
+
frameworks: frameworkResults,
|
|
187
|
+
universal_gaps: universalGaps.map(g => ({
|
|
188
|
+
id: g.control_id,
|
|
189
|
+
name: g.control_name,
|
|
190
|
+
real_requirement: g.real_requirement
|
|
191
|
+
})),
|
|
192
|
+
theater_risks: theaterRisks,
|
|
193
|
+
summary: {
|
|
194
|
+
total_gaps: relevantGaps.length,
|
|
195
|
+
universal_gaps: universalGaps.length,
|
|
196
|
+
theater_risk_controls: theaterRisks.length
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Run all seven theater pattern checks against an organization's control inventory.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} controlGaps - Parsed framework-control-gaps.json
|
|
205
|
+
* @param {object} cveCatalog - Parsed cve-catalog.json
|
|
206
|
+
* @returns {{ findings: object[], theater_score: number, compliant_but_exposed: boolean }}
|
|
207
|
+
*/
|
|
208
|
+
function theaterCheck(controlGaps, cveCatalog = {}) {
|
|
209
|
+
const findings = [];
|
|
210
|
+
|
|
211
|
+
for (const [patternId, pattern] of Object.entries(THEATER_PATTERNS)) {
|
|
212
|
+
const matchingControls = Object.values(controlGaps).filter(control =>
|
|
213
|
+
pattern.detection_test(control, cveCatalog)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (matchingControls.length > 0) {
|
|
217
|
+
findings.push({
|
|
218
|
+
pattern_id: patternId,
|
|
219
|
+
pattern_name: pattern.name,
|
|
220
|
+
description: pattern.description,
|
|
221
|
+
affected_controls: matchingControls.map(c => c.control_id || c.control_name),
|
|
222
|
+
severity: _theaterSeverity(patternId)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const theaterScore = Math.min(100, findings.length * 15);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
findings,
|
|
231
|
+
theater_score: theaterScore,
|
|
232
|
+
theater_label: _theaterLabel(theaterScore),
|
|
233
|
+
compliant_but_exposed: findings.length > 0,
|
|
234
|
+
recommendation: findings.length > 0
|
|
235
|
+
? 'Compliance audit would pass. Real-world exposure exists. Address highest-severity theater patterns first.'
|
|
236
|
+
: 'No theater patterns detected in scanned controls.'
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Compare multiple frameworks by lag score for a dashboard view.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} controlGaps - Parsed framework-control-gaps.json
|
|
244
|
+
* @param {object} globalFrameworks - Parsed global-frameworks.json
|
|
245
|
+
* @returns {Array} Sorted by lag score descending
|
|
246
|
+
*/
|
|
247
|
+
function compareFrameworks(controlGaps, globalFrameworks) {
|
|
248
|
+
const results = [];
|
|
249
|
+
const frameworkIds = _extractFrameworkIds(globalFrameworks);
|
|
250
|
+
|
|
251
|
+
for (const id of frameworkIds) {
|
|
252
|
+
const lag = lagScore(id, controlGaps, globalFrameworks);
|
|
253
|
+
results.push({ framework: id, ...lag });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return results.sort((a, b) => b.score - a.score);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- private helpers ---
|
|
260
|
+
|
|
261
|
+
function _findFrameworkData(frameworkId, globalFrameworks) {
|
|
262
|
+
for (const jurisdiction of Object.values(globalFrameworks)) {
|
|
263
|
+
if (!jurisdiction.frameworks) continue;
|
|
264
|
+
for (const [key, fw] of Object.entries(jurisdiction.frameworks)) {
|
|
265
|
+
if (key === frameworkId || fw.full_name?.includes(frameworkId)) return fw;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function _scorePatchSla(sla_hours) {
|
|
272
|
+
if (sla_hours === null || sla_hours === undefined) return 60;
|
|
273
|
+
const days = sla_hours / 24;
|
|
274
|
+
if (days <= 2) return 10;
|
|
275
|
+
if (days <= 7) return 30;
|
|
276
|
+
if (days <= 14) return 55;
|
|
277
|
+
if (days <= 30) return 80;
|
|
278
|
+
return 95;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function _scoreNotifSla(sla_hours) {
|
|
282
|
+
if (sla_hours === null || sla_hours === undefined) return 50;
|
|
283
|
+
if (sla_hours <= 4) return 10;
|
|
284
|
+
if (sla_hours <= 24) return 30;
|
|
285
|
+
if (sla_hours <= 72) return 60;
|
|
286
|
+
return 80;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function _scoreAiCoverage(coverage) {
|
|
290
|
+
if (!coverage || coverage === 'None') return 100;
|
|
291
|
+
if (coverage.toLowerCase().includes('no ai-specific')) return 80;
|
|
292
|
+
if (coverage.toLowerCase().includes('partial')) return 50;
|
|
293
|
+
return 20;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _lagLabel(score) {
|
|
297
|
+
if (score >= 80) return 'critical_lag';
|
|
298
|
+
if (score >= 60) return 'significant_lag';
|
|
299
|
+
if (score >= 40) return 'moderate_lag';
|
|
300
|
+
if (score >= 20) return 'minor_lag';
|
|
301
|
+
return 'current';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function _theaterSeverity(patternId) {
|
|
305
|
+
const high = ['access_control_ai', 'malware_protection_ai', 'vendor_management_ai'];
|
|
306
|
+
const critical = ['patch_management'];
|
|
307
|
+
if (critical.includes(patternId)) return 'critical';
|
|
308
|
+
if (high.includes(patternId)) return 'high';
|
|
309
|
+
return 'medium';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function _theaterLabel(score) {
|
|
313
|
+
if (score >= 75) return 'systemic_theater';
|
|
314
|
+
if (score >= 45) return 'significant_theater';
|
|
315
|
+
if (score >= 15) return 'partial_theater';
|
|
316
|
+
return 'minimal_theater';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function _extractFrameworkIds(globalFrameworks) {
|
|
320
|
+
const ids = [];
|
|
321
|
+
for (const jurisdiction of Object.values(globalFrameworks)) {
|
|
322
|
+
if (!jurisdiction.frameworks) continue;
|
|
323
|
+
ids.push(...Object.keys(jurisdiction.frameworks));
|
|
324
|
+
}
|
|
325
|
+
return ids;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = { lagScore, gapReport, theaterCheck, compareFrameworks };
|
package/lib/job-queue.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/job-queue.js
|
|
4
|
+
*
|
|
5
|
+
* Async job queue specialized for exceptd's upstream-fetch workloads. The
|
|
6
|
+
* generic retry semantics (HTTP + Node net classifier, exponential
|
|
7
|
+
* backoff with crypto jitter, AbortSignal-aware sleep) live in the
|
|
8
|
+
* vendored blamejs retry primitive — this module wraps it with the
|
|
9
|
+
* per-source concurrency caps + token-bucket rate limits + priority
|
|
10
|
+
* ordering specific to the KEV / EPSS / NVD / IETF / GitHub workloads.
|
|
11
|
+
*
|
|
12
|
+
* - per-source concurrency caps (max in-flight)
|
|
13
|
+
* - token-bucket rate limiting (max requests per window)
|
|
14
|
+
* - priority ordering (higher number = sooner)
|
|
15
|
+
* - retry classification + backoff delegated to vendor/blamejs/retry.js
|
|
16
|
+
* - per-source stats (queued / in_flight / completed / failed / retried)
|
|
17
|
+
*
|
|
18
|
+
* No npm deps. Node 24 stdlib + vendored retry.js.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const retry = require("../vendor/blamejs/retry");
|
|
22
|
+
|
|
23
|
+
class TokenBucket {
|
|
24
|
+
constructor({ tokens, windowMs }) {
|
|
25
|
+
this.capacity = tokens;
|
|
26
|
+
this.tokens = tokens;
|
|
27
|
+
this.windowMs = windowMs;
|
|
28
|
+
this.refillIntervalMs = windowMs / tokens;
|
|
29
|
+
this.lastRefill = Date.now();
|
|
30
|
+
}
|
|
31
|
+
refill() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const elapsed = now - this.lastRefill;
|
|
34
|
+
if (elapsed <= 0) return;
|
|
35
|
+
const add = elapsed / this.refillIntervalMs;
|
|
36
|
+
if (add >= 1) {
|
|
37
|
+
this.tokens = Math.min(this.capacity, this.tokens + Math.floor(add));
|
|
38
|
+
this.lastRefill = now;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
tryTake() {
|
|
42
|
+
this.refill();
|
|
43
|
+
if (this.tokens >= 1) {
|
|
44
|
+
this.tokens -= 1;
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
return Math.max(1, Math.ceil(this.refillIntervalMs - (Date.now() - this.lastRefill)));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class JobQueue {
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
const defaults = { concurrency: 4 };
|
|
54
|
+
this.sources = options.sources || { default: defaults };
|
|
55
|
+
// Retry options forwarded to vendored withRetry. Defaults are tighter
|
|
56
|
+
// than withRetry's own DEFAULT_RETRY so a network blip on KEV/EPSS
|
|
57
|
+
// doesn't stall the whole refresh by 80s.
|
|
58
|
+
this.retry = {
|
|
59
|
+
maxAttempts: 3,
|
|
60
|
+
baseDelayMs: 200,
|
|
61
|
+
maxDelayMs: 5000,
|
|
62
|
+
jitterFactor: 0.5,
|
|
63
|
+
...options.retry,
|
|
64
|
+
};
|
|
65
|
+
// Caller may override the retry classifier. Default is the vendored
|
|
66
|
+
// blamejs classifier (HTTP 408/425/429/5xx + Node net codes), which
|
|
67
|
+
// matches everything the upstream sources emit.
|
|
68
|
+
this.isRetryable = options.isRetryable || retry.isRetryable;
|
|
69
|
+
this._perSource = {};
|
|
70
|
+
for (const [name, cfg] of Object.entries(this.sources)) {
|
|
71
|
+
this._perSource[name] = {
|
|
72
|
+
cfg: { concurrency: defaults.concurrency, ...cfg },
|
|
73
|
+
pending: [],
|
|
74
|
+
inFlight: 0,
|
|
75
|
+
bucket: cfg.rate ? new TokenBucket(cfg.rate) : null,
|
|
76
|
+
stats: { queued: 0, in_flight: 0, completed: 0, failed: 0, retried: 0 },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
this._drainResolvers = [];
|
|
80
|
+
this._closed = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Enqueue a job.
|
|
85
|
+
* @param {object} job
|
|
86
|
+
* - source: source key from the sources map (default: "default")
|
|
87
|
+
* - priority: integer; higher runs sooner within a source
|
|
88
|
+
* - run: async () => result
|
|
89
|
+
* - signal: optional AbortSignal forwarded to retry.withRetry sleep
|
|
90
|
+
* - retry: per-job retry override (merged onto queue defaults)
|
|
91
|
+
* - meta: freeform metadata attached to stats / errors
|
|
92
|
+
*/
|
|
93
|
+
add(job) {
|
|
94
|
+
if (this._closed) return Promise.reject(new Error("JobQueue is closed"));
|
|
95
|
+
const source = job.source || "default";
|
|
96
|
+
const bucket = this._perSource[source] || this._perSource.default;
|
|
97
|
+
if (!bucket) {
|
|
98
|
+
return Promise.reject(new Error(`JobQueue: unknown source "${source}" and no default configured`));
|
|
99
|
+
}
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const entry = {
|
|
102
|
+
source,
|
|
103
|
+
priority: typeof job.priority === "number" ? job.priority : 0,
|
|
104
|
+
run: job.run,
|
|
105
|
+
retry: { ...this.retry, ...(job.retry || {}) },
|
|
106
|
+
signal: job.signal,
|
|
107
|
+
meta: job.meta || {},
|
|
108
|
+
resolve,
|
|
109
|
+
reject,
|
|
110
|
+
};
|
|
111
|
+
bucket.pending.push(entry);
|
|
112
|
+
bucket.pending.sort((a, b) => b.priority - a.priority);
|
|
113
|
+
bucket.stats.queued++;
|
|
114
|
+
this._tick(source);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_tick(source) {
|
|
119
|
+
const bucket = this._perSource[source];
|
|
120
|
+
if (!bucket) return;
|
|
121
|
+
while (bucket.inFlight < bucket.cfg.concurrency && bucket.pending.length > 0) {
|
|
122
|
+
if (bucket.bucket) {
|
|
123
|
+
const waitMs = bucket.bucket.tryTake();
|
|
124
|
+
if (waitMs > 0) {
|
|
125
|
+
setTimeout(() => this._tick(source), waitMs);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const entry = bucket.pending.shift();
|
|
130
|
+
bucket.stats.queued--;
|
|
131
|
+
bucket.inFlight++;
|
|
132
|
+
bucket.stats.in_flight = bucket.inFlight;
|
|
133
|
+
this._run(entry).finally(() => {
|
|
134
|
+
bucket.inFlight--;
|
|
135
|
+
bucket.stats.in_flight = bucket.inFlight;
|
|
136
|
+
this._tick(source);
|
|
137
|
+
if (this._allIdle()) this._notifyDrain();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async _run(entry) {
|
|
143
|
+
const bucket = this._perSource[entry.source];
|
|
144
|
+
const onRetry = () => { bucket.stats.retried++; };
|
|
145
|
+
try {
|
|
146
|
+
const result = await retry.withRetry(
|
|
147
|
+
() => entry.run(),
|
|
148
|
+
{
|
|
149
|
+
maxAttempts: entry.retry.maxAttempts,
|
|
150
|
+
baseDelayMs: entry.retry.baseDelayMs,
|
|
151
|
+
maxDelayMs: entry.retry.maxDelayMs,
|
|
152
|
+
jitterFactor: entry.retry.jitterFactor,
|
|
153
|
+
isRetryable: this.isRetryable,
|
|
154
|
+
onRetry,
|
|
155
|
+
signal: entry.signal,
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
bucket.stats.completed++;
|
|
159
|
+
entry.resolve(result);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
bucket.stats.failed++;
|
|
162
|
+
try { err.queue_meta = { source: entry.source, ...entry.meta }; } catch { /* readonly */ }
|
|
163
|
+
entry.reject(err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_allIdle() {
|
|
168
|
+
for (const b of Object.values(this._perSource)) {
|
|
169
|
+
if (b.inFlight > 0 || b.pending.length > 0) return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_notifyDrain() {
|
|
175
|
+
for (const r of this._drainResolvers) r();
|
|
176
|
+
this._drainResolvers = [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
drain() {
|
|
180
|
+
if (this._allIdle()) return Promise.resolve();
|
|
181
|
+
return new Promise((resolve) => this._drainResolvers.push(resolve));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
stats() {
|
|
185
|
+
const out = {};
|
|
186
|
+
for (const [name, b] of Object.entries(this._perSource)) out[name] = { ...b.stats };
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
close() {
|
|
191
|
+
this._closed = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { JobQueue, TokenBucket, isRetryable: retry.isRetryable };
|