@blamejs/exceptd-skills 0.19.33 → 0.19.34
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 +10 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
package/orchestrator/pipeline.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Multi-agent pipeline coordinator
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This module coordinates agent handoffs using a structured JSON protocol.
|
|
8
|
-
* Each stage produces a handoff package that the next stage consumes.
|
|
9
|
-
* Agents themselves are defined in agents/ and executed by AI assistants — not by this code.
|
|
10
|
-
* This module tracks state, validates handoffs, and routes between stages.
|
|
4
|
+
* Multi-agent pipeline coordinator: threat-researcher → source-validator →
|
|
5
|
+
* skill-updater → report-generator. Tracks state, validates handoffs and routes
|
|
6
|
+
* between stages; the agents live in agents/ and are run by AI assistants.
|
|
11
7
|
*/
|
|
12
8
|
|
|
13
9
|
const fs = require('fs');
|
|
@@ -20,8 +16,6 @@ const REPORTS_DIR = path.join(__dirname, '..', 'reports');
|
|
|
20
16
|
|
|
21
17
|
const PIPELINE_STAGES = ['threat-researcher', 'source-validator', 'skill-updater', 'report-generator'];
|
|
22
18
|
|
|
23
|
-
// --- public API ---
|
|
24
|
-
|
|
25
19
|
/**
|
|
26
20
|
* Initialize a new pipeline run for a given trigger.
|
|
27
21
|
*
|
|
@@ -52,8 +46,7 @@ function initPipeline(triggerType, triggerPayload) {
|
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
/**
|
|
55
|
-
* Build the handoff package for a
|
|
56
|
-
* This is what an AI assistant reads to understand what to do and what to pass forward.
|
|
49
|
+
* Build the handoff package for a pipeline stage — what the next agent reads.
|
|
57
50
|
*
|
|
58
51
|
* @param {object} run - Pipeline run object from initPipeline()
|
|
59
52
|
* @param {number} stageIndex - 0-based stage index
|
|
@@ -61,10 +54,8 @@ function initPipeline(triggerType, triggerPayload) {
|
|
|
61
54
|
* @returns {object} Handoff package for the next stage
|
|
62
55
|
*/
|
|
63
56
|
function buildHandoff(run, stageIndex, stageOutput) {
|
|
64
|
-
// Bounds-check
|
|
65
|
-
//
|
|
66
|
-
// validateHandoff. With it, callers see a structured error that names the
|
|
67
|
-
// exact invalid input.
|
|
57
|
+
// Bounds-check up front, else an out-of-range index surfaces as an opaque
|
|
58
|
+
// "cannot read properties of undefined" from inside validateHandoff.
|
|
68
59
|
if (!run || !Array.isArray(run.stages)) {
|
|
69
60
|
throw new TypeError('buildHandoff: run.stages must be an array');
|
|
70
61
|
}
|
|
@@ -102,18 +93,8 @@ function buildHandoff(run, stageIndex, stageOutput) {
|
|
|
102
93
|
return handoff;
|
|
103
94
|
}
|
|
104
95
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* Used by the scheduler for weekly currency checks.
|
|
108
|
-
*
|
|
109
|
-
* @returns {{ currency_report: object[], action_required: boolean }}
|
|
110
|
-
*/
|
|
111
|
-
// Manifest read cache. currencyCheck() runs on every weekly tick AND on every
|
|
112
|
-
// `exceptd currency` invocation; in `watch` mode the scheduler triggers it
|
|
113
|
-
// repeatedly. Re-reading + JSON.parse'ing the (~80 KB) manifest each time is
|
|
114
|
-
// pure waste when the file hasn't changed within the cache window. 60s TTL is
|
|
115
|
-
// short enough that a manual edit during a long-running watcher shows up by
|
|
116
|
-
// the next periodic tick.
|
|
96
|
+
// currencyCheck() runs on every weekly tick and every `exceptd currency` call, so
|
|
97
|
+
// the TTL is short enough that a manual manifest edit shows by the next tick.
|
|
117
98
|
const MANIFEST_CACHE_TTL_MS = 60_000;
|
|
118
99
|
let _manifestCache = { value: null, mtimeMs: 0, readAt: 0 };
|
|
119
100
|
|
|
@@ -121,8 +102,7 @@ function _loadManifestCached() {
|
|
|
121
102
|
const manifestPath = path.join(__dirname, '..', 'manifest.json');
|
|
122
103
|
const now = Date.now();
|
|
123
104
|
if (_manifestCache.value && (now - _manifestCache.readAt) < MANIFEST_CACHE_TTL_MS) {
|
|
124
|
-
// Within TTL
|
|
125
|
-
// ~tens of microseconds; trade it for the JSON.parse() cost saved.
|
|
105
|
+
// Within TTL: one stat() to confirm, in place of a re-parse.
|
|
126
106
|
try {
|
|
127
107
|
const st = fs.statSync(manifestPath);
|
|
128
108
|
if (st.mtimeMs === _manifestCache.mtimeMs) {
|
|
@@ -141,25 +121,17 @@ function _loadManifestCached() {
|
|
|
141
121
|
}
|
|
142
122
|
|
|
143
123
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* real manifest read. A malformed `last_threat_review` (e.g. "2026-13-99",
|
|
148
|
-
* which is structurally ISO-shaped but not a real calendar date) must map to
|
|
149
|
-
* maximally STALE — score 0, action_required true — not to the safe-LOOKING
|
|
150
|
-
* 100 a NaN delta would otherwise yield. The misformat is also surfaced via
|
|
151
|
-
* `unparseable_review_date` so a bad date is OBSERVABLE rather than silently
|
|
152
|
-
* defaulting (matching the project rule that a no-match/fall-through path must
|
|
153
|
-
* surface, not silently pass).
|
|
124
|
+
* Currency row for a single skill against a reference `now`. A malformed
|
|
125
|
+
* `last_threat_review` maps to maximally STALE — score 0, action_required true,
|
|
126
|
+
* `unparseable_review_date` set — not the safe-LOOKING 100 a NaN delta yields.
|
|
154
127
|
*
|
|
155
128
|
* @param {object} skill
|
|
156
129
|
* @param {Date} now
|
|
157
130
|
*/
|
|
158
131
|
function _skillCurrencyRow(skill, now) {
|
|
159
132
|
const rawDate = skill.last_threat_review || '2020-01-01';
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// the runner's local offset.
|
|
133
|
+
// A bare YYYY-MM-DD gets an explicit T00:00:00Z, so the day delta does not
|
|
134
|
+
// shift with the runner's local offset.
|
|
163
135
|
const t = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(rawDate) ? rawDate + 'T00:00:00Z' : rawDate);
|
|
164
136
|
const unparseable = !Number.isFinite(t);
|
|
165
137
|
const reviewDate = unparseable ? new Date('2020-01-01T00:00:00Z') : new Date(t);
|
|
@@ -199,10 +171,8 @@ function currencyCheck() {
|
|
|
199
171
|
}
|
|
200
172
|
|
|
201
173
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* @param {string} stageName - Agent name
|
|
205
|
-
* @returns {string|null} Agent instruction content
|
|
174
|
+
* Reads agents/<stageName>.md.
|
|
175
|
+
* @returns {string|null} Agent instruction content, null when unreadable.
|
|
206
176
|
*/
|
|
207
177
|
function getAgentDefinition(stageName) {
|
|
208
178
|
const agentPath = path.join(AGENTS_DIR, `${stageName}.md`);
|
|
@@ -213,16 +183,10 @@ function getAgentDefinition(stageName) {
|
|
|
213
183
|
}
|
|
214
184
|
}
|
|
215
185
|
|
|
216
|
-
// --- private helpers ---
|
|
217
|
-
|
|
218
186
|
function validateHandoff(stageName, output) {
|
|
219
|
-
// Guard the `in` deref below:
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// style as buildHandoff's stageIndex RangeError, so the caller sees which
|
|
223
|
-
// input was wrong. A handoff payload is a keyed object — an array is not a
|
|
224
|
-
// valid payload, so Array.isArray is rejected too (today it would yield the
|
|
225
|
-
// less-precise "missing fields" error).
|
|
187
|
+
// Guard the `in` deref below: null, a non-object or an array would otherwise
|
|
188
|
+
// throw an opaque "Cannot use 'in' operator". A payload is keyed, so an array
|
|
189
|
+
// is rejected here rather than reported as missing fields.
|
|
226
190
|
if (output === null || typeof output !== 'object' || Array.isArray(output)) {
|
|
227
191
|
throw new TypeError(
|
|
228
192
|
`buildHandoff: stageOutput for ${stageName} must be a non-null object`
|
|
@@ -264,26 +228,11 @@ function getStageInstructions(stageName, previousOutput) {
|
|
|
264
228
|
}
|
|
265
229
|
|
|
266
230
|
function _currencyScore(daysSinceReview, _forwardWatchCount) {
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// signal of ACTIVE maintenance, not staleness, so the count no
|
|
273
|
-
// longer affects the score. The arg is retained for ABI compat.
|
|
274
|
-
// The penalty schedule must be able to cross the tiers the gate checks
|
|
275
|
-
// against (currencyCheck: action_required at < 70, critical_count at < 50;
|
|
276
|
-
// _currencyLabel: 'stale' < 70, 'critical_stale' < 50). A schedule whose
|
|
277
|
-
// worst penalty was -30 floored the score at 70, so the warn/critical tiers —
|
|
278
|
-
// and the workflow issue they gate — could never fire. The deeper penalties
|
|
279
|
-
// only bite past 180/270/365 days, so a normally-maintained skill stays
|
|
280
|
-
// 'acceptable' while a genuinely abandoned one reaches the gate.
|
|
281
|
-
//
|
|
282
|
-
// Belt-and-suspenders: a NaN delta (the day-count derived from an
|
|
283
|
-
// unparseable last_threat_review) must map to maximally STALE (0), never the
|
|
284
|
-
// safe-LOOKING 100 the additive schedule below would leave it at. This is an
|
|
285
|
-
// exported surface other callers/tests use, so guard here even though
|
|
286
|
-
// _skillCurrencyRow already substitutes a stale fallback date upstream.
|
|
231
|
+
// Age of last_threat_review alone. A forward_watch entry signals ACTIVE
|
|
232
|
+
// maintenance, so the count must not penalise the score; the argument stays for
|
|
233
|
+
// callers. The schedule has to cross the tiers the gate checks — 'stale' below
|
|
234
|
+
// 70, 'critical_stale' below 50 — so a worst penalty of -30 would floor the
|
|
235
|
+
// score at 70 and neither tier could fire. A non-finite delta maps to STALE.
|
|
287
236
|
if (!Number.isFinite(daysSinceReview)) return 0;
|
|
288
237
|
let score = 100;
|
|
289
238
|
if (daysSinceReview > 365) score -= 100; // a year+ unreviewed → 0 (critical_stale)
|
|
@@ -302,12 +251,8 @@ function _currencyLabel(score) {
|
|
|
302
251
|
return 'critical_stale';
|
|
303
252
|
}
|
|
304
253
|
|
|
305
|
-
// Test-only hook
|
|
306
|
-
//
|
|
307
|
-
// the inline { mtimeMs: 0, readAt: 0 } reset values don't get detected
|
|
308
|
-
// by scripts/check-test-coverage.js's extractLibExports regex as
|
|
309
|
-
// pseudo-exports (`mtimeMs` and `readAt` are private cache fields, not
|
|
310
|
-
// module exports).
|
|
254
|
+
// Test-only hook. Defined here rather than inline in the export literal so its
|
|
255
|
+
// reset values are not read as exports by scripts/check-test-coverage.js.
|
|
311
256
|
function _resetManifestCache() {
|
|
312
257
|
_manifestCache = { value: null, mtimeMs: 0, readAt: 0 };
|
|
313
258
|
}
|
|
@@ -319,10 +264,8 @@ module.exports = {
|
|
|
319
264
|
getAgentDefinition,
|
|
320
265
|
MANIFEST_CACHE_TTL_MS,
|
|
321
266
|
_resetManifestCache,
|
|
322
|
-
// Exported for the gate-reachability contract test
|
|
323
|
-
// to reach the warn (< 70) and critical (< 50) tiers the workflow issues on.
|
|
267
|
+
// Exported for the gate-reachability contract test (warn < 70, critical < 50).
|
|
324
268
|
_currencyScore,
|
|
325
|
-
// Exported so the malformed-date
|
|
326
|
-
// real manifest read.
|
|
269
|
+
// Exported so the malformed-date guard is testable without a manifest read.
|
|
327
270
|
_skillCurrencyRow,
|
|
328
271
|
};
|
package/orchestrator/scanner.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Environment scanner
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Designed to be run by a human operator or an AI assistant — not a background daemon.
|
|
8
|
-
* All discovery is read-only. No writes, no network calls beyond local probes.
|
|
4
|
+
* Environment scanner: discovers host security-posture signals as structured
|
|
5
|
+
* findings that dispatcher.js routes to skills.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
const fs = require('fs');
|
|
@@ -15,22 +12,14 @@ const { execFileSync, spawnSync } = require('child_process');
|
|
|
15
12
|
|
|
16
13
|
const DATA_DIR = process.env.EXCEPTD_DATA_DIR || path.join(__dirname, '..', 'data');
|
|
17
14
|
|
|
18
|
-
// CLI flags
|
|
19
|
-
//
|
|
20
|
-
// orchestrator's flag allowlists accept all three). The flag MUST be honored
|
|
21
|
-
// equivalently to EXCEPTD_AIR_GAP=1 on the egress path — historically the env
|
|
22
|
-
// var suppressed the outbound TLS probe but the CLI flag did not, so an
|
|
23
|
-
// operator passing `--air-gap` still fired a TLS s_client connect.
|
|
15
|
+
// CLI flags requesting air-gap (no-egress) operation. All three must be honored
|
|
16
|
+
// equivalently to EXCEPTD_AIR_GAP=1 on the egress path.
|
|
24
17
|
const AIR_GAP_FLAGS = ['--air-gap', '--offline', '--no-network'];
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* though the scanner is reached in-process from the orchestrator entry — the
|
|
31
|
-
* flag and the env var are now equivalent on the egress path.
|
|
32
|
-
*
|
|
33
|
-
* @param {{ airGap?: boolean }} [opts]
|
|
20
|
+
* True for EXCEPTD_AIR_GAP=1, `opts.airGap`, or an air-gap flag on argv. Reading
|
|
21
|
+
* argv is what makes the CLI flag effective, since the scanner is reached
|
|
22
|
+
* in-process from the orchestrator entry.
|
|
34
23
|
*/
|
|
35
24
|
function isAirGap(opts) {
|
|
36
25
|
if (opts && opts.airGap) return true;
|
|
@@ -39,28 +28,12 @@ function isAirGap(opts) {
|
|
|
39
28
|
return argv.some(a => typeof a === 'string' && AIR_GAP_FLAGS.includes(a));
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
// --- public API ---
|
|
43
|
-
|
|
44
31
|
/**
|
|
45
|
-
* Run all scanners and return consolidated findings.
|
|
46
|
-
*
|
|
47
|
-
* DEPRECATED in v0.10.0. This is the pre-seven-phase legacy scanner that
|
|
48
|
-
* shells out from Node — kept as a safety-net path for non-AI operators.
|
|
49
|
-
* New code should call the playbook runner instead:
|
|
50
|
-
*
|
|
51
|
-
* const runner = require('../lib/playbook-runner');
|
|
52
|
-
* const result = await runner.run('kernel', 'all-catalogued-kernel-cves', agentSubmission);
|
|
53
|
-
*
|
|
54
|
-
* The runner emits seven-phase findings (govern through close) with full
|
|
55
|
-
* GRC closure: CSAF-2.0 evidence bundles, jurisdiction-aware notification
|
|
56
|
-
* deadlines, auditor-ready exception language, regression schedules.
|
|
57
|
-
* `exceptd scan` will be removed in v1.0.
|
|
32
|
+
* Run all scanners and return consolidated findings. `opts.airGap` suppresses
|
|
33
|
+
* the outbound TLS probe.
|
|
58
34
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* air-gap CLI flags (--air-gap / --offline / --no-network) are also honored
|
|
62
|
-
* directly off process.argv, so the flag is equivalent to the env var.
|
|
63
|
-
* @returns {{ timestamp: string, host: object, findings: object[], summary: object }}
|
|
35
|
+
* Deprecated: new code calls lib/playbook-runner, which emits seven-phase
|
|
36
|
+
* findings with full GRC closure; this is a safety net for non-AI operators.
|
|
64
37
|
*/
|
|
65
38
|
async function scan(opts) {
|
|
66
39
|
const timestamp = new Date().toISOString();
|
|
@@ -83,16 +56,9 @@ async function scan(opts) {
|
|
|
83
56
|
findings.push(...frameworkScan());
|
|
84
57
|
|
|
85
58
|
const summary = summarize(findings);
|
|
86
|
-
// _deprecation is a stderr-only banner — it MUST NOT appear in the JSON
|
|
87
|
-
// shape that downstream consumers ingest. Internal narrative belongs on
|
|
88
|
-
// stderr (the banner above), never in the structured result body.
|
|
89
59
|
return { timestamp, host, findings, summary };
|
|
90
60
|
}
|
|
91
61
|
|
|
92
|
-
/**
|
|
93
|
-
* Run a targeted scan for a specific domain.
|
|
94
|
-
* @param {'kernel'|'mcp'|'crypto'|'ai_api'|'framework'} domain
|
|
95
|
-
*/
|
|
96
62
|
async function scanDomain(domain, opts) {
|
|
97
63
|
const scanners = { kernel: kernelScan, mcp: mcpScan, crypto: cryptoScan, ai_api: aiApiScan, framework: frameworkScan };
|
|
98
64
|
const fn = scanners[domain];
|
|
@@ -100,8 +66,6 @@ async function scanDomain(domain, opts) {
|
|
|
100
66
|
return fn(opts);
|
|
101
67
|
}
|
|
102
68
|
|
|
103
|
-
// --- domain scanners ---
|
|
104
|
-
|
|
105
69
|
function kernelScan() {
|
|
106
70
|
const findings = [];
|
|
107
71
|
if (os.platform() !== 'linux') return findings;
|
|
@@ -117,8 +81,8 @@ function kernelScan() {
|
|
|
117
81
|
value: kernel,
|
|
118
82
|
cve_id: cveId,
|
|
119
83
|
rwep_score: cve.rwep_score,
|
|
120
|
-
//
|
|
121
|
-
//
|
|
84
|
+
// Carried so the CSAF report emits a real cvss_v3 block rather than a
|
|
85
|
+
// placeholder base_score:0.
|
|
122
86
|
cvss_score: cve.cvss_score,
|
|
123
87
|
cvss_vector: cve.cvss_vector,
|
|
124
88
|
cisa_kev: cve.cisa_kev,
|
|
@@ -161,13 +125,8 @@ function mcpScan() {
|
|
|
161
125
|
|
|
162
126
|
for (const serverName of serverList) {
|
|
163
127
|
const server = mcpServers[serverName];
|
|
164
|
-
// A
|
|
165
|
-
//
|
|
166
|
-
// Dereferencing its fields would throw inside this per-server loop,
|
|
167
|
-
// which the outer catch would mis-report as a whole-file config
|
|
168
|
-
// parse error — dropping every sibling server's finding. Emit a
|
|
169
|
-
// per-server malformed marker and continue so siblings still scan,
|
|
170
|
-
// mirroring how a genuine parse failure is surfaced per-file.
|
|
128
|
+
// A non-object entry would throw into the outer catch, mis-reported as
|
|
129
|
+
// a whole-file parse error that drops every sibling server's finding.
|
|
171
130
|
if (server === null || typeof server !== 'object' || Array.isArray(server)) {
|
|
172
131
|
findings.push({
|
|
173
132
|
domain: 'mcp',
|
|
@@ -210,9 +169,8 @@ function mcpScan() {
|
|
|
210
169
|
tool,
|
|
211
170
|
config_path: p,
|
|
212
171
|
severity: 'low',
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// routing table changes — domain fallback alone left it brittle.
|
|
172
|
+
// Routed directly, so the finding reaches mcp-agent-trust whatever the
|
|
173
|
+
// domain routing table does.
|
|
216
174
|
skill_hint: 'mcp-agent-trust',
|
|
217
175
|
action_required: 'MCP config file exists but could not be parsed'
|
|
218
176
|
});
|
|
@@ -233,10 +191,7 @@ function cryptoScan(opts) {
|
|
|
233
191
|
const [major, minor] = version.split('.').map(Number);
|
|
234
192
|
const isPqcReady = major > 3 || (major === 3 && minor >= 5);
|
|
235
193
|
|
|
236
|
-
//
|
|
237
|
-
// Reports per-algo via boolean flags so downstream callers can
|
|
238
|
-
// decide which gaps matter for their threat model (ML-KEM for
|
|
239
|
-
// HNDL exposure, LMS/XMSS for firmware signing, etc.).
|
|
194
|
+
// Per-algo flags: the caller decides which gaps matter for its threat model.
|
|
240
195
|
const pqc = probePqcAlgorithms();
|
|
241
196
|
const pqcDetail = [
|
|
242
197
|
`ML-KEM=${pqc.ml_kem ? 'avail' : 'missing'}`, // FIPS 203
|
|
@@ -264,12 +219,8 @@ function cryptoScan(opts) {
|
|
|
264
219
|
});
|
|
265
220
|
}
|
|
266
221
|
|
|
267
|
-
// Air-gap
|
|
268
|
-
//
|
|
269
|
-
// suppressed rather than failing silently. Honored equivalently for the
|
|
270
|
-
// EXCEPTD_AIR_GAP=1 env var AND the --air-gap / --offline / --no-network CLI
|
|
271
|
-
// flags (isAirGap inspects opts, env, and process.argv) — the flag is NOT a
|
|
272
|
-
// no-op on this egress path.
|
|
222
|
+
// Air-gap short-circuits the outbound TLS probe; the skipped annotation makes
|
|
223
|
+
// the suppression visible rather than a silent absence.
|
|
273
224
|
if (isAirGap(opts)) {
|
|
274
225
|
findings.push({
|
|
275
226
|
domain: 'crypto',
|
|
@@ -376,8 +327,6 @@ function frameworkScan() {
|
|
|
376
327
|
return findings;
|
|
377
328
|
}
|
|
378
329
|
|
|
379
|
-
// --- helpers ---
|
|
380
|
-
|
|
381
330
|
function hostInfo() {
|
|
382
331
|
return {
|
|
383
332
|
platform: os.platform(),
|
|
@@ -415,68 +364,24 @@ function safeExecFile(cmd, args) {
|
|
|
415
364
|
}
|
|
416
365
|
|
|
417
366
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* + a `provider_hint` indicating which surface confirmed each one.
|
|
421
|
-
*
|
|
422
|
-
* --- NIST PQC finalized (FIPS 203/204/205, 2024) ---
|
|
423
|
-
* ml_kem ML-KEM (Kyber) — FIPS 203, key encapsulation
|
|
424
|
-
* ml_dsa ML-DSA (Dilithium) — FIPS 204, signatures
|
|
425
|
-
* slh_dsa SLH-DSA (SPHINCS+) — FIPS 205, stateless hash sigs
|
|
426
|
-
*
|
|
427
|
-
* --- NIST PQC draft / alternate (2025+) ---
|
|
428
|
-
* fn_dsa FN-DSA (Falcon) — FIPS 206 draft, compact lattice sigs
|
|
429
|
-
* hqc HQC — alternate KEM selected March 2025
|
|
430
|
-
*
|
|
431
|
-
* --- NIST PQC Round-4 alternates (still relevant in niche / archival) ---
|
|
432
|
-
* frodo FrodoKEM — conservative lattice KEM
|
|
433
|
-
* ntru NTRU / NTRU-Prime / sNTRU — original lattice KEM family
|
|
434
|
-
* mceliece Classic McEliece — code-based, long-term archival use
|
|
435
|
-
* bike BIKE — code-based KEM, OQS-Provider exposed
|
|
436
|
-
*
|
|
437
|
-
* --- NIST additional signature on-ramp (2023+ Round 2) ---
|
|
438
|
-
* hawk HAWK — NTRU-based lattice signatures
|
|
439
|
-
* mayo MAYO — multivariate
|
|
440
|
-
* sqisign SQIsign — isogeny-based, small signatures
|
|
441
|
-
* cross CROSS — code-based
|
|
442
|
-
* uov UOV / SNOVA — Unbalanced Oil & Vinegar multivariate
|
|
443
|
-
* sdith SDitH — code-based (Syndrome Decoding in the Head)
|
|
444
|
-
* mirath MIRATH — code-based (rank metric)
|
|
445
|
-
* faest FAEST — symmetric-key/AES-based signatures
|
|
446
|
-
* perk PERK — code-based (Permuted Kernel Problem)
|
|
447
|
-
*
|
|
448
|
-
* --- Stateful hash signatures (RFC 8391 / 8554) ---
|
|
449
|
-
* lms LMS — Leighton-Micali Signatures (firmware)
|
|
450
|
-
* xmss XMSS — eXtended Merkle Signature Scheme
|
|
451
|
-
* hss HSS — Hierarchical Signature System
|
|
452
|
-
*
|
|
453
|
-
* --- IETF hybrid / composite sigs (emerging RFC drafts) ---
|
|
454
|
-
* composite_sig Composite Signatures (e.g. RSA+ML-DSA, ECDSA+ML-DSA)
|
|
455
|
-
* composite_kem Composite KEMs (e.g. X25519+ML-KEM)
|
|
367
|
+
* Probes the runtime for PQC algorithm availability. Returns a boolean flag per
|
|
368
|
+
* algorithm plus a `provider_hint` naming the surface that confirmed each one.
|
|
456
369
|
*/
|
|
457
370
|
function probePqcAlgorithms() {
|
|
458
371
|
const result = {
|
|
459
|
-
// NIST finalized
|
|
460
372
|
ml_kem: false, ml_dsa: false, slh_dsa: false,
|
|
461
|
-
// NIST draft / alternate
|
|
462
373
|
fn_dsa: false, hqc: false,
|
|
463
|
-
// NIST Round-4 / niche
|
|
464
374
|
frodo: false, ntru: false, mceliece: false, bike: false,
|
|
465
|
-
// NIST signature on-ramp (Round 2)
|
|
466
375
|
hawk: false, mayo: false, sqisign: false, cross: false,
|
|
467
376
|
uov: false, sdith: false, mirath: false, faest: false, perk: false,
|
|
468
|
-
// Stateful hash sigs
|
|
469
377
|
lms: false, xmss: false, hss: false,
|
|
470
|
-
// IETF composite / hybrid
|
|
471
378
|
composite_sig: false, composite_kem: false,
|
|
472
379
|
provider_hint: {},
|
|
473
380
|
};
|
|
474
381
|
const crypto = require('crypto');
|
|
475
382
|
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// against algorithm-list output specifically (no English-prose
|
|
479
|
-
// false positives expected; the lists contain only algo names).
|
|
383
|
+
// Spellings Node / OpenSSL / OQS-Provider / Bouncy Castle emit. Matched only
|
|
384
|
+
// against algorithm-list output, never prose, which would false-positive.
|
|
480
385
|
const PATTERNS = {
|
|
481
386
|
// NIST finalized
|
|
482
387
|
ml_kem: /\b(ml-?kem|kyber)\b/i,
|
|
@@ -490,7 +395,7 @@ function probePqcAlgorithms() {
|
|
|
490
395
|
ntru: /\b(s?ntru(-?prime)?(-?\d+)?|hrss\d*|hps\d+)\b/i,
|
|
491
396
|
mceliece: /\b(classic-?)?mceliece(-?\d+)?\b/i,
|
|
492
397
|
bike: /\bbike(-?l?\d+)?\b/i,
|
|
493
|
-
// NIST signature on-ramp (Round 2
|
|
398
|
+
// NIST signature on-ramp (Round 2)
|
|
494
399
|
hawk: /\bhawk(-?\d+)?\b/i,
|
|
495
400
|
mayo: /\bmayo(-?\d+)?\b/i,
|
|
496
401
|
sqisign: /\bsqi-?sign(?:hd)?\b/i,
|
|
@@ -516,9 +421,8 @@ function probePqcAlgorithms() {
|
|
|
516
421
|
}
|
|
517
422
|
}
|
|
518
423
|
|
|
519
|
-
// Channel 1 — Node's crypto APIs
|
|
520
|
-
//
|
|
521
|
-
// presence is itself an ML-KEM availability signal.
|
|
424
|
+
// Channel 1 — Node's crypto APIs, no shellouts. crypto.kemEncapsulate is an
|
|
425
|
+
// experimental ML-KEM gateway, so its presence is itself an ML-KEM signal.
|
|
522
426
|
try {
|
|
523
427
|
if (typeof crypto.kemEncapsulate === 'function') {
|
|
524
428
|
record('ml_kem', 'node:crypto.kemEncapsulate');
|
|
@@ -535,9 +439,7 @@ function probePqcAlgorithms() {
|
|
|
535
439
|
}
|
|
536
440
|
} catch (_) { /* probe failure → fall through to openssl */ }
|
|
537
441
|
|
|
538
|
-
// Channel 2 — `openssl list
|
|
539
|
-
// algorithms split across two list commands; OQS-Provider exposes
|
|
540
|
-
// the on-ramp / niche algos when installed.
|
|
442
|
+
// Channel 2 — `openssl list`: KEMs and signatures split across two commands.
|
|
541
443
|
const kemList = safeExecFile('openssl', ['list', '-kem-algorithms']);
|
|
542
444
|
if (kemList) {
|
|
543
445
|
const kemAlgos = ['ml_kem', 'hqc', 'frodo', 'ntru', 'mceliece', 'bike', 'composite_kem'];
|
|
@@ -562,8 +464,7 @@ function probePqcAlgorithms() {
|
|
|
562
464
|
}
|
|
563
465
|
|
|
564
466
|
function probeTls(target) {
|
|
565
|
-
// Target is "host:port";
|
|
566
|
-
// is read once per scan rather than baked in here.
|
|
467
|
+
// Target is "host:port"; the call site owns the env-var default.
|
|
567
468
|
const t = typeof target === 'string' && target.length > 0 ? target : 'registry.npmjs.org:443';
|
|
568
469
|
const result = spawnSync('openssl', ['s_client', '-connect', t, '-brief'], {
|
|
569
470
|
input: '',
|
|
@@ -576,14 +477,11 @@ function probeTls(target) {
|
|
|
576
477
|
return match ? match[1] : null;
|
|
577
478
|
}
|
|
578
479
|
|
|
579
|
-
// Key names that signal a credential value, matched
|
|
580
|
-
//
|
|
581
|
-
// `headers` sub-objects (e.g. env.OPENAI_API_KEY, headers.Authorization), so a
|
|
582
|
-
// top-level-only sweep leaks them into emitted findings.
|
|
480
|
+
// Key names that signal a credential value, matched at any nesting depth: MCP
|
|
481
|
+
// configs put real secrets inside `env` and `headers` sub-objects.
|
|
583
482
|
const SECRET_KEY_RE = /token|key|secret|password|credential|auth|bearer|api[-_]?key|access[-_]?key/i;
|
|
584
483
|
|
|
585
|
-
// Value shapes that look like live credentials
|
|
586
|
-
// (e.g. args: ['--token', 'sk-...'] or a positional bearer string).
|
|
484
|
+
// Value shapes that look like live credentials under an innocuous key name.
|
|
587
485
|
const SECRET_VALUE_RES = [
|
|
588
486
|
/\bsk-[A-Za-z0-9_-]{8,}/, // OpenAI-style keys
|
|
589
487
|
/\bAKIA[0-9A-Z]{12,}/, // AWS access key id
|
|
@@ -599,10 +497,8 @@ function looksLikeSecretValue(value) {
|
|
|
599
497
|
return SECRET_VALUE_RES.some((re) => re.test(value));
|
|
600
498
|
}
|
|
601
499
|
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
// keys at any depth and any standalone string value that matches a known
|
|
605
|
-
// credential shape. Cycles are guarded with a seen-set.
|
|
500
|
+
// Redacts credential-shaped data at any depth so nothing emitted or persisted
|
|
501
|
+
// carries an operator's live secrets. Cycle-guarded.
|
|
606
502
|
function redactDeep(value, seen) {
|
|
607
503
|
if (value === null || typeof value !== 'object') {
|
|
608
504
|
return looksLikeSecretValue(value) ? '[REDACTED]' : value;
|