@blamejs/exceptd-skills 0.12.11 → 0.12.15
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/CHANGELOG.md +243 -0
- package/bin/exceptd.js +299 -48
- package/data/_indexes/_meta.json +49 -48
- package/data/_indexes/activity-feed.json +13 -5
- package/data/_indexes/catalog-summaries.json +51 -29
- package/data/_indexes/chains.json +3238 -3210
- package/data/_indexes/frequency.json +3 -0
- package/data/_indexes/jurisdiction-map.json +5 -3
- package/data/_indexes/section-offsets.json +712 -685
- package/data/_indexes/theater-fingerprints.json +1 -1
- package/data/_indexes/token-budget.json +355 -340
- package/data/atlas-ttps.json +144 -129
- package/data/attack-techniques.json +339 -0
- package/data/cve-catalog.json +515 -475
- package/data/cwe-catalog.json +1081 -759
- package/data/exploit-availability.json +63 -15
- package/data/framework-control-gaps.json +867 -843
- package/data/rfc-references.json +276 -276
- package/keys/EXPECTED_FINGERPRINT +1 -0
- package/lib/auto-discovery.js +21 -4
- package/lib/cross-ref-api.js +39 -6
- package/lib/cve-curation.js +505 -47
- package/lib/lint-skills.js +217 -15
- package/lib/playbook-runner.js +1224 -183
- package/lib/prefetch.js +121 -8
- package/lib/refresh-external.js +261 -95
- package/lib/refresh-network.js +208 -18
- package/lib/schemas/manifest.schema.json +16 -0
- package/lib/scoring.js +83 -7
- package/lib/sign.js +112 -3
- package/lib/source-ghsa.js +219 -37
- package/lib/source-osv.js +381 -122
- package/lib/validate-catalog-meta.js +64 -9
- package/lib/validate-cve-catalog.js +213 -7
- package/lib/validate-indexes.js +88 -37
- package/lib/validate-playbooks.js +469 -0
- package/lib/verify.js +313 -16
- package/manifest-snapshot.json +1 -1
- package/manifest-snapshot.sha256 +1 -0
- package/manifest.json +73 -73
- package/orchestrator/dispatcher.js +21 -1
- package/orchestrator/event-bus.js +52 -8
- package/orchestrator/index.js +279 -20
- package/orchestrator/pipeline.js +63 -2
- package/orchestrator/scanner.js +32 -10
- package/orchestrator/scheduler.js +196 -20
- package/package.json +3 -1
- package/sbom.cdx.json +9 -9
- package/scripts/check-manifest-snapshot.js +32 -0
- package/scripts/check-sbom-currency.js +65 -3
- package/scripts/check-test-coverage.js +142 -19
- package/scripts/predeploy.js +110 -40
- package/scripts/refresh-manifest-snapshot.js +55 -4
- package/scripts/validate-vendor-online.js +169 -0
- package/scripts/verify-shipped-tarball.js +106 -3
- package/skills/ai-attack-surface/skill.md +18 -10
- package/skills/ai-c2-detection/skill.md +7 -2
- package/skills/ai-risk-management/skill.md +5 -4
- package/skills/api-security/skill.md +3 -3
- package/skills/attack-surface-pentest/skill.md +5 -5
- package/skills/cloud-security/skill.md +1 -1
- package/skills/compliance-theater/skill.md +8 -8
- package/skills/container-runtime-security/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +5 -1
- package/skills/email-security-anti-phishing/skill.md +1 -1
- package/skills/exploit-scoring/skill.md +18 -18
- package/skills/framework-gap-analysis/skill.md +6 -6
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +2 -2
- package/skills/incident-response-playbook/skill.md +4 -4
- package/skills/kernel-lpe-triage/skill.md +21 -2
- package/skills/mcp-agent-trust/skill.md +17 -10
- package/skills/mlops-security/skill.md +2 -1
- package/skills/ot-ics-security/skill.md +1 -1
- package/skills/policy-exception-gen/skill.md +3 -3
- package/skills/pqc-first/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +7 -3
- package/skills/researcher/skill.md +20 -3
- package/skills/sector-energy/skill.md +1 -1
- package/skills/sector-federal-government/skill.md +1 -1
- package/skills/sector-financial/skill.md +3 -3
- package/skills/sector-healthcare/skill.md +2 -2
- package/skills/security-maturity-tiers/skill.md +7 -7
- package/skills/skill-update-loop/skill.md +19 -3
- package/skills/supply-chain-integrity/skill.md +1 -1
- package/skills/threat-model-currency/skill.md +11 -11
- package/skills/threat-modeling-methodology/skill.md +3 -3
- package/skills/webapp-security/skill.md +1 -1
- package/skills/zeroday-gap-learn/skill.md +51 -7
- package/vendor/blamejs/_PROVENANCE.json +4 -1
- package/vendor/blamejs/worker-pool.js +38 -0
|
@@ -3,16 +3,41 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Scheduled task coordinator for skill currency maintenance.
|
|
5
5
|
*
|
|
6
|
-
* Schedules: weekly currency check, monthly CVE validation, annual
|
|
7
|
-
* Emits events via event-bus.js when currency thresholds
|
|
6
|
+
* Schedules: weekly currency check, monthly CVE validation reminder, annual
|
|
7
|
+
* full audit reminder. Emits events via event-bus.js when currency thresholds
|
|
8
|
+
* are breached.
|
|
8
9
|
*
|
|
9
10
|
* This is a simple interval-based scheduler. For production use, swap for a
|
|
10
11
|
* proper cron daemon or cloud scheduler without changing the task definitions.
|
|
12
|
+
*
|
|
13
|
+
* Bootstrap-fire policy. Long-cadence tasks (monthly, annual) are also
|
|
14
|
+
* evaluated on `start()` so a freshly-restarted watcher does not silently
|
|
15
|
+
* skip a due interval. Whether a task fires on bootstrap is gated by a
|
|
16
|
+
* persisted "last fired" timestamp store at
|
|
17
|
+
* `~/.exceptd/scheduler-last-fired.json` — the task fires only when the
|
|
18
|
+
* elapsed time since the persisted timestamp exceeds the interval. The
|
|
19
|
+
* weekly currency check always fires on bootstrap (legacy behavior).
|
|
20
|
+
*
|
|
21
|
+
* Implementation note — INT32 overflow guard. `setInterval` / `setTimeout`
|
|
22
|
+
* delay values are coerced to a signed 32-bit integer; any value above
|
|
23
|
+
* 2^31 - 1 ms (~24.8 days) is silently clamped to 1 ms by Node, which
|
|
24
|
+
* causes the handler to fire ~1000×/sec. The MONTHLY_CVE_VALIDATION and
|
|
25
|
+
* ANNUAL_AUDIT intervals both exceed that limit. `scheduleEvery` wraps the
|
|
26
|
+
* underlying timer with a short tick interval (capped at SAFE_MAX_MS) and
|
|
27
|
+
* compares wall-clock elapsed time against the requested interval, so any
|
|
28
|
+
* delay — including multi-year intervals — fires exactly when due.
|
|
11
29
|
*/
|
|
12
30
|
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
|
|
13
35
|
const { bus, EVENT_TYPES } = require('./event-bus');
|
|
14
36
|
const { currencyCheck } = require('./pipeline');
|
|
15
37
|
|
|
38
|
+
const SAFE_MAX_MS = 2_147_483_647; // INT32 max — Node's setTimeout/setInterval ceiling.
|
|
39
|
+
const TICK_MS = Math.min(SAFE_MAX_MS, 24 * 60 * 60 * 1000); // 24h tick by default.
|
|
40
|
+
|
|
16
41
|
const INTERVALS = {
|
|
17
42
|
WEEKLY_CURRENCY: 7 * 24 * 60 * 60 * 1000,
|
|
18
43
|
MONTHLY_CVE_VALIDATION: 30 * 24 * 60 * 60 * 1000,
|
|
@@ -24,22 +49,146 @@ const CURRENCY_THRESHOLDS = {
|
|
|
24
49
|
warning: 70
|
|
25
50
|
};
|
|
26
51
|
|
|
27
|
-
|
|
52
|
+
const LAST_FIRED_KEYS = {
|
|
53
|
+
WEEKLY_CURRENCY: 'weekly_currency_check',
|
|
54
|
+
MONTHLY_CVE_VALIDATION: 'monthly_cve_validation',
|
|
55
|
+
ANNUAL_AUDIT: 'annual_full_audit'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let unschedulers = [];
|
|
28
59
|
let running = false;
|
|
29
60
|
|
|
61
|
+
// --- persistent last-fired store ---
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the path of the last-fired persistence file. Defaults to
|
|
65
|
+
* `~/.exceptd/scheduler-last-fired.json`. Honors EXCEPTD_HOME for the test
|
|
66
|
+
* suite so unit tests stay isolated from the maintainer's real home dir.
|
|
67
|
+
*/
|
|
68
|
+
function _lastFiredStorePath() {
|
|
69
|
+
const root = process.env.EXCEPTD_HOME || path.join(os.homedir(), '.exceptd');
|
|
70
|
+
return path.join(root, 'scheduler-last-fired.json');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _loadLastFired() {
|
|
74
|
+
const p = _lastFiredStorePath();
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _saveLastFired(store) {
|
|
83
|
+
const p = _lastFiredStorePath();
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
86
|
+
fs.writeFileSync(p, JSON.stringify(store, null, 2));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// Persistence is best-effort. A failed write only means the next start
|
|
89
|
+
// can't tell whether the task fired; it does not break the running
|
|
90
|
+
// scheduler.
|
|
91
|
+
console.error('[scheduler] could not persist last-fired:', err.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _markFired(key, when) {
|
|
96
|
+
const store = _loadLastFired();
|
|
97
|
+
store[key] = when || new Date().toISOString();
|
|
98
|
+
_saveLastFired(store);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _shouldBootstrapFire(key, intervalMs) {
|
|
102
|
+
const store = _loadLastFired();
|
|
103
|
+
const stamp = store[key];
|
|
104
|
+
if (!stamp) return true;
|
|
105
|
+
const last = Date.parse(stamp);
|
|
106
|
+
if (!Number.isFinite(last)) return true;
|
|
107
|
+
return (Date.now() - last) >= intervalMs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- scheduling primitive ---
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Schedule `handler` to fire every `intervalMs`, safely for intervals that
|
|
114
|
+
* exceed Node's INT32 setTimeout ceiling. Returns an unschedule function.
|
|
115
|
+
*
|
|
116
|
+
* @param {number} intervalMs Desired interval in milliseconds (any positive value).
|
|
117
|
+
* @param {Function} handler Function to invoke on each interval.
|
|
118
|
+
* @returns {Function} Call to stop further firings.
|
|
119
|
+
*/
|
|
120
|
+
function scheduleEvery(intervalMs, handler) {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
let lastFired = startedAt;
|
|
123
|
+
const tick = () => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (now - lastFired >= intervalMs) {
|
|
126
|
+
lastFired = now;
|
|
127
|
+
try { handler(); } catch (e) { console.error('[scheduler]', e); }
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const id = setInterval(tick, Math.min(intervalMs, TICK_MS));
|
|
131
|
+
// Deliberately NOT calling id.unref(): the `watch` orchestrator verb
|
|
132
|
+
// is long-running and relies on the scheduler timers to keep the event
|
|
133
|
+
// loop alive (the event bus has no I/O of its own). Callers that don't
|
|
134
|
+
// want the timer to hold the loop open should call the returned
|
|
135
|
+
// unschedule function in their teardown path.
|
|
136
|
+
return () => clearInterval(id);
|
|
137
|
+
}
|
|
138
|
+
|
|
30
139
|
// --- public API ---
|
|
31
140
|
|
|
32
141
|
/**
|
|
33
|
-
* Start the scheduler. Runs
|
|
142
|
+
* Start the scheduler. Runs the weekly task immediately, then schedules all
|
|
143
|
+
* three on their intervals. Monthly and annual tasks are also "bootstrap
|
|
144
|
+
* fired" on start when the persisted last-fired timestamp is older than the
|
|
145
|
+
* interval (or absent), so a restarted watcher does not silently skip a due
|
|
146
|
+
* window. Bootstrap-fire happens inside the same try/catch the periodic
|
|
147
|
+
* wrapper uses so a single thrown task cannot crash the watcher.
|
|
34
148
|
*/
|
|
35
149
|
function start() {
|
|
36
150
|
if (running) return;
|
|
37
151
|
running = true;
|
|
38
152
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
153
|
+
const safeRun = (label, fn) => {
|
|
154
|
+
try { fn(); }
|
|
155
|
+
catch (e) { console.error('[scheduler] ' + label + ' failed:', e); }
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Weekly always fires on bootstrap (legacy behavior).
|
|
159
|
+
safeRun('weekly currency bootstrap', () => {
|
|
160
|
+
runWeeklyCurrencyCheck();
|
|
161
|
+
_markFired(LAST_FIRED_KEYS.WEEKLY_CURRENCY);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Monthly + annual bootstrap-fire only when the persisted timestamp is
|
|
165
|
+
// older than the interval. This closes the "freshly-restarted watcher
|
|
166
|
+
// never fires the long-cadence task" gap.
|
|
167
|
+
if (_shouldBootstrapFire(LAST_FIRED_KEYS.MONTHLY_CVE_VALIDATION, INTERVALS.MONTHLY_CVE_VALIDATION)) {
|
|
168
|
+
safeRun('monthly CVE bootstrap', () => {
|
|
169
|
+
runMonthlyCveValidation();
|
|
170
|
+
_markFired(LAST_FIRED_KEYS.MONTHLY_CVE_VALIDATION);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (_shouldBootstrapFire(LAST_FIRED_KEYS.ANNUAL_AUDIT, INTERVALS.ANNUAL_AUDIT)) {
|
|
174
|
+
safeRun('annual audit bootstrap', () => {
|
|
175
|
+
runAnnualAudit();
|
|
176
|
+
_markFired(LAST_FIRED_KEYS.ANNUAL_AUDIT);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
unschedulers.push(scheduleEvery(INTERVALS.WEEKLY_CURRENCY, () => {
|
|
181
|
+
runWeeklyCurrencyCheck();
|
|
182
|
+
_markFired(LAST_FIRED_KEYS.WEEKLY_CURRENCY);
|
|
183
|
+
}));
|
|
184
|
+
unschedulers.push(scheduleEvery(INTERVALS.MONTHLY_CVE_VALIDATION, () => {
|
|
185
|
+
runMonthlyCveValidation();
|
|
186
|
+
_markFired(LAST_FIRED_KEYS.MONTHLY_CVE_VALIDATION);
|
|
187
|
+
}));
|
|
188
|
+
unschedulers.push(scheduleEvery(INTERVALS.ANNUAL_AUDIT, () => {
|
|
189
|
+
runAnnualAudit();
|
|
190
|
+
_markFired(LAST_FIRED_KEYS.ANNUAL_AUDIT);
|
|
191
|
+
}));
|
|
43
192
|
|
|
44
193
|
console.log('[scheduler] Started. Weekly currency check, monthly CVE validation, annual audit scheduled.');
|
|
45
194
|
}
|
|
@@ -48,8 +197,10 @@ function start() {
|
|
|
48
197
|
* Stop the scheduler and clear all timers.
|
|
49
198
|
*/
|
|
50
199
|
function stop() {
|
|
51
|
-
for (const
|
|
52
|
-
|
|
200
|
+
for (const off of unschedulers) {
|
|
201
|
+
try { off(); } catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
unschedulers = [];
|
|
53
204
|
running = false;
|
|
54
205
|
console.log('[scheduler] Stopped.');
|
|
55
206
|
}
|
|
@@ -70,14 +221,25 @@ function runWeeklyCurrencyCheck() {
|
|
|
70
221
|
|
|
71
222
|
const { currency_report, action_required, critical_count } = currencyCheck();
|
|
72
223
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
224
|
+
// Emit ONE aggregated SKILL_CURRENCY_LOW_AGGREGATE event per run instead
|
|
225
|
+
// of N per-skill events. Per-run aggregate prevents downstream consumers
|
|
226
|
+
// (`watch`, dashboards, alerting webhooks) from receiving an event storm
|
|
227
|
+
// when N skills are simultaneously stale — common after a long pause
|
|
228
|
+
// between runs or on first bootstrap. The aggregate payload carries
|
|
229
|
+
// critical_count + the full array of stale skills so consumers can still
|
|
230
|
+
// drill in. The legacy per-skill SKILL_CURRENCY_LOW signature is preserved
|
|
231
|
+
// for callers (and tests) that consume bus.skillCurrencyLow() directly.
|
|
232
|
+
const critical = currency_report.filter(s => s.currency_score < CURRENCY_THRESHOLDS.critical);
|
|
233
|
+
if (critical.length > 0) {
|
|
234
|
+
bus.emit(EVENT_TYPES.SKILL_CURRENCY_LOW_AGGREGATE, {
|
|
235
|
+
critical_count: critical.length,
|
|
236
|
+
skills: critical.map(s => ({
|
|
237
|
+
skill_name: s.skill,
|
|
238
|
+
currency_score: s.currency_score,
|
|
239
|
+
days_since_review: s.days_since_review
|
|
240
|
+
})),
|
|
241
|
+
timestamp
|
|
242
|
+
});
|
|
81
243
|
}
|
|
82
244
|
|
|
83
245
|
const result = {
|
|
@@ -86,7 +248,7 @@ function runWeeklyCurrencyCheck() {
|
|
|
86
248
|
skills_checked: currency_report.length,
|
|
87
249
|
action_required,
|
|
88
250
|
critical_count,
|
|
89
|
-
critical_skills:
|
|
251
|
+
critical_skills: critical.map(s => s.skill),
|
|
90
252
|
warning_skills: currency_report.filter(s =>
|
|
91
253
|
s.currency_score >= CURRENCY_THRESHOLDS.critical && s.currency_score < CURRENCY_THRESHOLDS.warning
|
|
92
254
|
).map(s => s.skill)
|
|
@@ -134,4 +296,18 @@ function runAnnualAudit() {
|
|
|
134
296
|
};
|
|
135
297
|
}
|
|
136
298
|
|
|
137
|
-
module.exports = {
|
|
299
|
+
module.exports = {
|
|
300
|
+
start,
|
|
301
|
+
stop,
|
|
302
|
+
runCurrencyNow,
|
|
303
|
+
scheduleEvery,
|
|
304
|
+
SAFE_MAX_MS,
|
|
305
|
+
TICK_MS,
|
|
306
|
+
INTERVALS,
|
|
307
|
+
LAST_FIRED_KEYS,
|
|
308
|
+
// Internal hooks exposed for tests; not part of the operator surface.
|
|
309
|
+
_lastFiredStorePath,
|
|
310
|
+
_shouldBootstrapFire,
|
|
311
|
+
_markFired,
|
|
312
|
+
_loadLastFired,
|
|
313
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/exceptd-skills",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.15",
|
|
4
4
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-security",
|
|
@@ -54,8 +54,10 @@
|
|
|
54
54
|
"data/",
|
|
55
55
|
"skills/",
|
|
56
56
|
"keys/public.pem",
|
|
57
|
+
"keys/EXPECTED_FINGERPRINT",
|
|
57
58
|
"manifest.json",
|
|
58
59
|
"manifest-snapshot.json",
|
|
60
|
+
"manifest-snapshot.sha256",
|
|
59
61
|
"sbom.cdx.json",
|
|
60
62
|
"AGENTS.md",
|
|
61
63
|
"ARCHITECTURE.md",
|
package/sbom.cdx.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:0fb3661f-4f3b-4d31-98b3-d5fd759aa8c1",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-
|
|
7
|
+
"timestamp": "2026-05-14T16:47:04.535Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"name": "hand-written",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.15",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.12.
|
|
19
|
+
"version": "0.12.15",
|
|
20
20
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
|
|
21
21
|
"licenses": [
|
|
22
22
|
{
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.15",
|
|
29
29
|
"externalReferences": [
|
|
30
30
|
{
|
|
31
31
|
"type": "distribution",
|
|
32
|
-
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.
|
|
32
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.15"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"type": "vcs",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"properties": [
|
|
41
41
|
{
|
|
42
42
|
"name": "cyclonedx:dataflow:input",
|
|
43
|
-
"value": "data/atlas-ttps.json,data/cve-catalog.json,data/cwe-catalog.json,data/d3fend-catalog.json,data/dlp-controls.json,data/exploit-availability.json,data/framework-control-gaps.json,data/global-frameworks.json,data/rfc-references.json,data/zeroday-lessons.json"
|
|
43
|
+
"value": "data/atlas-ttps.json,data/attack-techniques.json,data/cve-catalog.json,data/cwe-catalog.json,data/d3fend-catalog.json,data/dlp-controls.json,data/exploit-availability.json,data/framework-control-gaps.json,data/global-frameworks.json,data/rfc-references.json,data/zeroday-lessons.json"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"name": "exceptd:catalog:count",
|
|
47
|
-
"value": "
|
|
47
|
+
"value": "11"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"name": "exceptd:skill:count",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"hashes": [
|
|
130
130
|
{
|
|
131
131
|
"alg": "SHA-256",
|
|
132
|
-
"content": "
|
|
132
|
+
"content": "fa9814c2d18db221a2dc552cf2a5047467d87a2590cbc9d64b8a0a340e545b55"
|
|
133
133
|
}
|
|
134
134
|
],
|
|
135
135
|
"externalReferences": [
|
|
@@ -32,10 +32,12 @@
|
|
|
32
32
|
|
|
33
33
|
const fs = require("fs");
|
|
34
34
|
const path = require("path");
|
|
35
|
+
const crypto = require("crypto");
|
|
35
36
|
|
|
36
37
|
const ROOT = path.join(__dirname, "..");
|
|
37
38
|
const MANIFEST_PATH = path.join(ROOT, "manifest.json");
|
|
38
39
|
const SNAPSHOT_PATH = path.join(ROOT, "manifest-snapshot.json");
|
|
40
|
+
const SNAPSHOT_SHA_PATH = path.join(ROOT, "manifest-snapshot.sha256");
|
|
39
41
|
|
|
40
42
|
function captureSurface(manifest) {
|
|
41
43
|
// Public surface = the set of facts downstream consumers may have
|
|
@@ -188,6 +190,36 @@ if (require.main === module) {
|
|
|
188
190
|
process.exit(2);
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
// Audit G F23 — when manifest-snapshot.sha256 is present, validate that
|
|
194
|
+
// the on-disk snapshot still hashes to the recorded value. Catches a
|
|
195
|
+
// hand-edit of manifest-snapshot.json that bypassed refresh-manifest-
|
|
196
|
+
// snapshot.js (so the F5 commit-only guard never had a chance to fire).
|
|
197
|
+
// The file is OPTIONAL: when absent, the gate warns-and-continues so
|
|
198
|
+
// pre-v0.12.14 trees still work.
|
|
199
|
+
if (fs.existsSync(SNAPSHOT_SHA_PATH)) {
|
|
200
|
+
const expectedLine = fs.readFileSync(SNAPSHOT_SHA_PATH, "utf8").trim();
|
|
201
|
+
const expectedSha = expectedLine.split(/\s+/)[0];
|
|
202
|
+
const liveSha = crypto
|
|
203
|
+
.createHash("sha256")
|
|
204
|
+
.update(fs.readFileSync(SNAPSHOT_PATH))
|
|
205
|
+
.digest("hex");
|
|
206
|
+
if (expectedSha !== liveSha) {
|
|
207
|
+
console.error(
|
|
208
|
+
"[check-manifest-snapshot] manifest-snapshot.json integrity check FAILED " +
|
|
209
|
+
`(expected ${expectedSha.slice(0, 12)}…, live ${liveSha.slice(0, 12)}…). ` +
|
|
210
|
+
"Someone edited manifest-snapshot.json without running refresh-manifest-snapshot.js. " +
|
|
211
|
+
"Re-run `node scripts/refresh-manifest-snapshot.js --commit-only` to regenerate."
|
|
212
|
+
);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
console.warn(
|
|
217
|
+
"[check-manifest-snapshot] WARN: manifest-snapshot.sha256 missing — " +
|
|
218
|
+
"integrity check skipped. Run `node scripts/refresh-manifest-snapshot.js --commit-only` " +
|
|
219
|
+
"to generate it."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
191
223
|
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
|
|
192
224
|
const current = captureSurface(manifest);
|
|
193
225
|
const result = diff(baseline, current);
|
|
@@ -62,11 +62,67 @@ function checkSbomCurrency(root) {
|
|
|
62
62
|
if (sbom.bomFormat !== "CycloneDX" || sbom.specVersion !== "1.6") {
|
|
63
63
|
errors.push("SBOM is not CycloneDX 1.6");
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
// Audit G F2 — component-level cross-check. A renamed or version-bumped
|
|
67
|
+
// skill that never made it into the SBOM refresh will pass the count
|
|
68
|
+
// check (the cardinality is unchanged) but the per-component name +
|
|
69
|
+
// version comparison surfaces it. Two component classes are recognised:
|
|
70
|
+
//
|
|
71
|
+
// 1. Skill components — bom-ref begins with "skill:" OR the component
|
|
72
|
+
// name matches a manifest.skills[].name. Each one must exist in
|
|
73
|
+
// manifest.skills with the same version.
|
|
74
|
+
// 2. Vendor components — bom-ref begins with "vendor:". Validated
|
|
75
|
+
// against vendor/blamejs/_PROVENANCE.json when present.
|
|
76
|
+
//
|
|
77
|
+
// Components that don't fit either pattern are surfaced as warnings
|
|
78
|
+
// (not errors) so the gate isn't brittle against future component types.
|
|
79
|
+
const components = Array.isArray(sbom.components) ? sbom.components : [];
|
|
80
|
+
const skillByName = new Map(
|
|
81
|
+
(manifest.skills || []).map((s) => [s.name, s])
|
|
82
|
+
);
|
|
83
|
+
const provPath = path.join(root, "vendor", "blamejs", "_PROVENANCE.json");
|
|
84
|
+
let vendorProv = null;
|
|
85
|
+
if (fs.existsSync(provPath)) {
|
|
86
|
+
try { vendorProv = JSON.parse(fs.readFileSync(provPath, "utf8")); } catch { /* leave null */ }
|
|
87
|
+
}
|
|
88
|
+
for (const comp of components) {
|
|
89
|
+
const bomRef = typeof comp["bom-ref"] === "string" ? comp["bom-ref"] : "";
|
|
90
|
+
const name = comp.name;
|
|
91
|
+
const version = comp.version;
|
|
92
|
+
if (bomRef.startsWith("skill:") || skillByName.has(name)) {
|
|
93
|
+
const skillName = bomRef.startsWith("skill:")
|
|
94
|
+
? bomRef.slice("skill:".length)
|
|
95
|
+
: name;
|
|
96
|
+
const live = skillByName.get(skillName);
|
|
97
|
+
if (!live) {
|
|
98
|
+
errors.push(
|
|
99
|
+
`SBOM component "${name}" (bom-ref ${bomRef}) is not in manifest.skills — skill renamed or removed without SBOM refresh`
|
|
100
|
+
);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (live.version && version && String(live.version) !== String(version)) {
|
|
104
|
+
errors.push(
|
|
105
|
+
`SBOM component "${name}" version ${version} != manifest.skills version ${live.version} — bump without SBOM refresh`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} else if (bomRef.startsWith("vendor:")) {
|
|
109
|
+
if (vendorProv && vendorProv.pinned_commit) {
|
|
110
|
+
const expected = vendorProv.pinned_commit.slice(0, 12);
|
|
111
|
+
if (version && String(version) !== expected) {
|
|
112
|
+
errors.push(
|
|
113
|
+
`SBOM vendor component "${name}" version ${version} != _PROVENANCE.json pinned_commit ${expected}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
65
120
|
return {
|
|
66
121
|
ok: errors.length === 0,
|
|
67
122
|
errors,
|
|
68
123
|
skills: sbomSkills,
|
|
69
124
|
catalogs: sbomCatalogs,
|
|
125
|
+
components_validated: components.length,
|
|
70
126
|
};
|
|
71
127
|
}
|
|
72
128
|
|
|
@@ -76,10 +132,16 @@ function main() {
|
|
|
76
132
|
if (!result.ok) {
|
|
77
133
|
for (const e of result.errors) process.stderr.write(e + "\n");
|
|
78
134
|
process.stderr.write("Run `npm run refresh-sbom` to regenerate sbom.cdx.json.\n");
|
|
79
|
-
|
|
135
|
+
// v0.11.13 pattern: set exitCode + return so buffered stdout/stderr
|
|
136
|
+
// writes drain before the event loop exits. process.exit() can
|
|
137
|
+
// truncate piped output (CI log capture, JSON consumers).
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
80
140
|
}
|
|
81
|
-
process.stdout.write(
|
|
82
|
-
|
|
141
|
+
process.stdout.write(
|
|
142
|
+
`SBOM current — ${result.skills} skills, ${result.catalogs} catalogs, ` +
|
|
143
|
+
`${result.components_validated} components validated.\n`
|
|
144
|
+
);
|
|
83
145
|
}
|
|
84
146
|
|
|
85
147
|
module.exports = { checkSbomCurrency, resolveRoot };
|