@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +2 -2
  4. package/lib/auto-discovery.js +56 -286
  5. package/lib/canonical-eq.js +7 -40
  6. package/lib/citation-resolve.js +22 -70
  7. package/lib/collectors/ai-api.js +20 -54
  8. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  9. package/lib/collectors/citation-hygiene.js +72 -210
  10. package/lib/collectors/containers.js +41 -130
  11. package/lib/collectors/cred-stores.js +31 -115
  12. package/lib/collectors/crypto-codebase.js +55 -138
  13. package/lib/collectors/crypto.js +24 -54
  14. package/lib/collectors/hardening.js +20 -78
  15. package/lib/collectors/kernel.js +16 -46
  16. package/lib/collectors/library-author.js +57 -206
  17. package/lib/collectors/mcp.js +24 -70
  18. package/lib/collectors/runtime.js +24 -86
  19. package/lib/collectors/sbom.js +34 -106
  20. package/lib/collectors/scan-excludes.js +31 -138
  21. package/lib/collectors/secrets.js +62 -178
  22. package/lib/cross-ref-api.js +39 -123
  23. package/lib/currency-severity.js +8 -27
  24. package/lib/cve-batch.js +13 -21
  25. package/lib/cve-cli.js +13 -20
  26. package/lib/cve-curation.js +72 -239
  27. package/lib/cve-regression-watcher.js +29 -152
  28. package/lib/cvss.js +13 -54
  29. package/lib/doctor-bucketing.js +3 -19
  30. package/lib/exit-codes.js +10 -42
  31. package/lib/flag-suggest.js +7 -25
  32. package/lib/framework-gap.js +35 -114
  33. package/lib/gap-detectors.js +37 -159
  34. package/lib/id-validation.js +9 -30
  35. package/lib/job-queue.js +13 -36
  36. package/lib/lint-skills.js +64 -232
  37. package/lib/playbook-runner.js +693 -2095
  38. package/lib/prefetch.js +100 -376
  39. package/lib/refresh-external.js +199 -627
  40. package/lib/refresh-network.js +75 -307
  41. package/lib/rfc-cli.js +23 -68
  42. package/lib/scoring.js +77 -145
  43. package/lib/sign.js +43 -229
  44. package/lib/source-advisories.js +43 -194
  45. package/lib/source-ghsa.js +37 -120
  46. package/lib/source-osv.js +94 -266
  47. package/lib/ttp-mapper.js +14 -24
  48. package/lib/upstream-check-cli.js +10 -28
  49. package/lib/upstream-check.js +19 -44
  50. package/lib/validate-catalog-meta.js +17 -61
  51. package/lib/validate-cve-catalog.js +43 -119
  52. package/lib/validate-indexes.js +25 -76
  53. package/lib/validate-package.js +16 -62
  54. package/lib/validate-playbooks.js +69 -275
  55. package/lib/validate-vendor.js +16 -49
  56. package/lib/verify.js +56 -286
  57. package/lib/version-pins.js +5 -34
  58. package/lib/worker-pool.js +11 -30
  59. package/lib/xml-tokenizer.js +47 -152
  60. package/manifest.json +53 -53
  61. package/orchestrator/dispatcher.js +17 -68
  62. package/orchestrator/event-bus.js +11 -74
  63. package/orchestrator/index.js +138 -412
  64. package/orchestrator/pipeline.js +28 -85
  65. package/orchestrator/scanner.js +34 -138
  66. package/orchestrator/scheduler.js +20 -84
  67. package/package.json +1 -1
  68. package/sbom.cdx.json +241 -241
  69. package/scripts/audit-catalog-gaps.js +9 -62
  70. package/scripts/audit-cross-skill.js +5 -31
  71. package/scripts/audit-perf.js +6 -16
  72. package/scripts/backfill-theater-test.js +7 -64
  73. package/scripts/bootstrap.js +12 -44
  74. package/scripts/build-indexes.js +40 -154
  75. package/scripts/builders/activity-feed.js +4 -14
  76. package/scripts/builders/catalog-summaries.js +3 -10
  77. package/scripts/builders/currency.js +7 -20
  78. package/scripts/builders/cwe-chains.js +7 -30
  79. package/scripts/builders/did-ladders.js +6 -13
  80. package/scripts/builders/frequency.js +5 -19
  81. package/scripts/builders/jurisdiction-clocks.js +6 -25
  82. package/scripts/builders/recipes.js +6 -14
  83. package/scripts/builders/section-offsets.js +13 -51
  84. package/scripts/builders/stale-content.js +7 -28
  85. package/scripts/builders/summary-cards.js +8 -29
  86. package/scripts/builders/theater-fingerprints.js +12 -27
  87. package/scripts/builders/token-budget.js +4 -31
  88. package/scripts/check-agents-md-collectors.js +11 -54
  89. package/scripts/check-catalog-gap-budget.js +15 -32
  90. package/scripts/check-changelog-extract.js +18 -48
  91. package/scripts/check-codebase-patterns-currency.js +6 -22
  92. package/scripts/check-codebase-patterns.js +50 -143
  93. package/scripts/check-epss-consistency.js +9 -64
  94. package/scripts/check-framework-gap-coverage.js +13 -31
  95. package/scripts/check-manifest-snapshot.js +13 -73
  96. package/scripts/check-sbom-currency.js +44 -142
  97. package/scripts/check-test-count.js +15 -52
  98. package/scripts/check-test-coverage.js +66 -197
  99. package/scripts/check-test-subjects.js +21 -62
  100. package/scripts/check-ttp-references.js +14 -38
  101. package/scripts/check-ttp-upstream.js +8 -40
  102. package/scripts/check-version-bump.js +9 -61
  103. package/scripts/check-version-tags.js +20 -121
  104. package/scripts/predeploy.js +38 -184
  105. package/scripts/refresh-manifest-snapshot.js +16 -38
  106. package/scripts/refresh-mitre-atlas.js +3 -8
  107. package/scripts/refresh-mitre-attack.js +1 -8
  108. package/scripts/refresh-mitre-d3fend.js +3 -9
  109. package/scripts/refresh-mitre-ics-attack.js +3 -8
  110. package/scripts/refresh-reverse-refs.js +27 -94
  111. package/scripts/refresh-rfc-index.js +2 -10
  112. package/scripts/refresh-sbom.js +31 -161
  113. package/scripts/refresh-upstream-catalogs.js +40 -137
  114. package/scripts/release.js +69 -232
  115. package/scripts/run-e2e-scenarios.js +24 -71
  116. package/scripts/sync-manifest-metadata.js +10 -34
  117. package/scripts/sync-package-description.js +8 -17
  118. package/scripts/validate-vendor-online.js +13 -44
  119. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,22 +1,11 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/crypto.js
5
- *
6
- * Companion collector for the `crypto` playbook. Linux-only. Reads
7
- * the host's TLS library state (openssl version + KEM / signature
8
- * algorithm catalogues) and sshd_config (effective directives after
9
- * Include expansion) to flip post-quantum-readiness indicators.
10
- *
11
- * Skipped indicators (require operator judgement or live behavioural
12
- * data, left unflipped so the runner returns inconclusive rather
13
- * than a forced miss):
14
- *
15
- * tls-no-hybrid-group needs a real TLS handshake against
16
- * a target server
17
- * rsa-2048-cert-long-life cert content + chain walk; sensitivity-
18
- * horizon comparison is operator review
19
- * no-crypto-inventory governance / process indicator
4
+ * Companion collector for the `crypto` playbook, Linux only. Reads the host's
5
+ * TLS library state (openssl version, KEM and signature catalogues) and the
6
+ * effective sshd_config after Include expansion, to flip post-quantum readiness
7
+ * indicators. Indicators needing a live handshake, a certificate-chain review
8
+ * or governance evidence are left unflipped.
20
9
  *
21
10
  * Interface: see lib/collectors/README.md
22
11
  */
@@ -33,18 +22,16 @@ function readFileSafe(p, max = 256 * 1024) {
33
22
  fd = fs.openSync(p, "r");
34
23
  const st = fs.fstatSync(fd);
35
24
  if (st.size > max) return null;
36
- // readFileSync(fd) loops read() to EOF — a single readSync may return
37
- // fewer than st.size bytes on network/FUSE/sync-backed fds, which would
38
- // leave the buffer tail NUL-filled and silently drop trailing content.
39
- // Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
25
+ // readFileSync(fd), never a single readSync: on network/FUSE fds a short read
26
+ // leaves the tail NUL-filled. The open fd also keeps fstat-then-read TOCTOU-free.
40
27
  return fs.readFileSync(fd, "utf8");
41
28
  } catch { return null; }
42
29
  finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
43
30
  }
44
31
 
45
- // Same sshd_config Include-expansion logic as the hardening
46
- // collector: first-match-wins, inline drop-in files in lexical
47
- // order at the textual position of the `Include` directive.
32
+ // Drop-in files are inlined in lexical order at the textual position of the
33
+ // `Include` directive, so a later first-match-wins parse sees what sshd sees.
34
+ // The hardening collector expands sshd_config the same way.
48
35
  function expandSshdConfig(baseContent, configDPath) {
49
36
  if (!baseContent) return "";
50
37
  const out = [];
@@ -74,8 +61,7 @@ function expandSshdConfig(baseContent, configDPath) {
74
61
  return out.join("\n");
75
62
  }
76
63
 
77
- // First-match-wins parse of KexAlgorithms / MACs / Ciphers /
78
- // PermitRootLogin from the effective sshd_config content.
64
+ // First match wins, as in sshd itself; an unset directive stays null.
79
65
  function parseSshdEffective(content) {
80
66
  if (content == null) return { kex: null, macs: null, ciphers: null };
81
67
  const out = { kex: null, macs: null, ciphers: null };
@@ -92,9 +78,8 @@ function parseSshdEffective(content) {
92
78
  return out;
93
79
  }
94
80
 
95
- // Compare OpenSSL banner string against the 3.5.0 native-ML-KEM cutoff.
96
- // Returns "hit" (< 3.5.0), "miss" (>= 3.5.0), or undefined (banner
97
- // could not be parsed — collector returns inconclusive).
81
+ // 3.5.0 is the native-ML-KEM cutoff: "hit" below it, "miss" at or above, and
82
+ // undefined when the banner does not parse, which leaves it inconclusive.
98
83
  function compareOpensslVersion(verStr) {
99
84
  if (!verStr) return undefined;
100
85
  const m = verStr.match(/OpenSSL\s+(\d+)\.(\d+)\.(\d+)/);
@@ -106,21 +91,16 @@ function compareOpensslVersion(verStr) {
106
91
  return "miss";
107
92
  }
108
93
 
109
- // PQC kex: hit when KexAlgorithms is absent (no PQC by default) OR
110
- // present-without sntrup761x25519 / mlkem768x25519 / mlkem1024.
94
+ // Hits when KexAlgorithms is absent (no PQC kex by default) or lacks a hybrid.
111
95
  function parsePqcKex(content, hasSshdContent) {
112
96
  if (!hasSshdContent) return undefined;
113
97
  if (content == null) return "hit";
114
98
  return /sntrup761x25519|mlkem768x25519|mlkem1024/.test(content) ? "miss" : "hit";
115
99
  }
116
100
 
117
- // Weak mac or cipher: hit when MACs contains hmac-md5 / hmac-sha1
118
- // (without -etm suffix) OR Ciphers contains arcfour / 3des-cbc /
119
- // des-cbc / blowfish-cbc / aes-cbc. The playbook treats every CBC
120
- // mode as weak under modern SSH cipher policy (BEAST / padding
121
- // oracle / chosen-plaintext considerations) — aes128-cbc /
122
- // aes192-cbc / aes256-cbc still appear in legacy sshd configs and
123
- // must be flagged. Both fields absent → undefined (inconclusive).
101
+ // The playbook treats EVERY CBC mode as weak, aes256-cbc included. hmac-md5 and
102
+ // hmac-sha1 are weak only without the -etm suffix. Both fields absent leaves the
103
+ // indicator undefined, hence inconclusive.
124
104
  function parseWeakMacOrCipher(macs, ciphers) {
125
105
  if (macs == null && ciphers == null) return undefined;
126
106
  const macsWeak = macs && /(?:^|,)(?:hmac-md5(?!-etm)|hmac-sha1(?!-etm))(?:,|$)/.test(macs);
@@ -128,13 +108,10 @@ function parseWeakMacOrCipher(macs, ciphers) {
128
108
  return (macsWeak || cipherWeak) ? "hit" : "miss";
129
109
  }
130
110
 
131
- // Either read a path-override fixture (synthetic-tempdir tests) or
132
- // invoke the named binary via execFile-shape spawning. Never
133
- // shell-interpolated. ENOENT / EACCES → null; caller surfaces that
134
- // via collector_errors / unflipped indicators. Some binaries write
135
- // their banner to stderr (e.g. `ssh -V`); others to stdout
136
- // (`openssl version`). Prefer stdout, fall back to stderr when
137
- // stdout is empty so the banner is not lost.
111
+ // A path override reads a fixture; otherwise the binary is spawned argv-style,
112
+ // never through a shell. A missing binary returns null, which the caller
113
+ // surfaces as an unflipped indicator. `ssh -V` writes its banner to stderr and
114
+ // `openssl version` to stdout, so stdout wins and stderr is the fallback.
138
115
  function readOrSpawn(pathOverride, cmd, args, errors) {
139
116
  if (pathOverride != null) return readFileSafe(pathOverride);
140
117
  const r = spawnSync(cmd, args, {
@@ -179,25 +156,20 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
179
156
  };
180
157
  }
181
158
 
182
- // TLS library version + algorithm catalogues.
183
159
  const opensslVer = readOrSpawn(paths.opensslVersionOutput, "openssl", ["version", "-a"], errors);
184
160
  const opensslKem = readOrSpawn(paths.opensslKemOutput, "openssl", ["list", "-kem-algorithms"], errors);
185
161
  const opensslSig = readOrSpawn(paths.opensslSignatureOutput, "openssl", ["list", "-signature-algorithms"], errors);
186
162
  const sshVer = readOrSpawn(paths.sshVersionOutput, "ssh", ["-V"], errors);
187
163
 
188
- // sshd_config: read base + expand Include directives. The base
189
- // can come from a path override (tests stage a synthetic file
190
- // tree); otherwise read /etc/ssh/sshd_config directly.
164
+ // A path override lets a test stage a synthetic tree in place of /etc/ssh.
191
165
  const sshdConfigPath = paths.sshdConfig || "/etc/ssh/sshd_config";
192
166
  const sshdConfigDPath = paths.sshdConfigD || "/etc/ssh/sshd_config.d";
193
167
  const sshdBase = readFileSafe(sshdConfigPath);
194
168
  const sshdEffective = sshdBase ? expandSshdConfig(sshdBase, sshdConfigDPath) : null;
195
169
  const sshdParsed = parseSshdEffective(sshdEffective);
196
170
 
197
- // Flip indicators only when the underlying source was readable.
198
- // Unreadable openssl / sshd_config → indicator stays out of
199
- // signal_overrides so the runner returns inconclusive rather than
200
- // asserting a clean posture without evidence.
171
+ // An indicator is flipped only when its source was readable; otherwise it stays
172
+ // out of this object and the runner returns inconclusive.
201
173
  const signal_overrides = {};
202
174
 
203
175
  const verSig = compareOpensslVersion(opensslVer);
@@ -220,8 +192,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
220
192
  : undefined;
221
193
  if (weakSig !== undefined) signal_overrides["weak-mac-or-cipher"] = weakSig;
222
194
 
223
- // certificate-store: list count of *.pem / *.crt under the
224
- // standard trust roots. Path overridable for tests.
225
195
  let certStoreSummary;
226
196
  const certStoreRoot = paths.certStore || "/etc/ssl/certs";
227
197
  try {
@@ -1,13 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/hardening.js
5
- *
6
- * Companion collector for the `hardening` playbook. Linux-only:
7
- * reads `/proc/sys/kernel/*`, `/proc/cmdline`,
8
- * `/sys/kernel/security/lockdown`, and `/etc/ssh/sshd_config` to
9
- * flip deterministic indicators. On non-Linux platforms the
10
- * precondition fails and the collector emits an empty submission.
4
+ * Companion collector for the `hardening` playbook. Linux-only: off Linux the
5
+ * precondition fails and the submission comes back empty.
11
6
  *
12
7
  * Interface: see lib/collectors/README.md
13
8
  */
@@ -30,21 +25,15 @@ function readFileSafe(p, max = 256 * 1024) {
30
25
  fd = fs.openSync(p, "r");
31
26
  const st = fs.fstatSync(fd);
32
27
  if (st.size > max) return null;
33
- // readFileSync(fd) loops read() to EOF — a single readSync may return
34
- // fewer than st.size bytes on network/FUSE/sync-backed fds, which would
35
- // leave the buffer tail NUL-filled and silently drop trailing content.
36
- // Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
28
+ // readFileSync(fd) loops to EOF, where one readSync can return short on a
29
+ // network/FUSE fd and NUL-fill the tail; via the fd it is also TOCTOU-free.
37
30
  return fs.readFileSync(fd, "utf8");
38
31
  } catch { return null; }
39
32
  finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
40
33
  }
41
34
 
42
- // Expand the base sshd_config into the effective directive stream by
43
- // inlining `Include <glob>` directives at their textual position.
44
- // Mirrors OpenSSH's parse order: first-match-wins, and the first match
45
- // can come from a drop-in file when `Include` appears earlier in the
46
- // base config than the directive it sets. Drop-in files within an
47
- // Include are processed in lexical order (matching OpenSSH glob).
35
+ // Inlines `Include <glob>` at its textual position: OpenSSH is first-match-wins,
36
+ // so a drop-in included early beats a later line in the base config.
48
37
  function expandSshdConfig(baseContent, configDPath) {
49
38
  if (!baseContent) return "";
50
39
  const out = [];
@@ -53,13 +42,9 @@ function expandSshdConfig(baseContent, configDPath) {
53
42
  const m = stripped.match(/^Include\s+(\S+)/i);
54
43
  if (!m) { out.push(raw); continue; }
55
44
  const glob = m[1];
56
- // Resolve the include glob — only handle the common
57
- // `<dir>/*.conf` form; other shapes fall back to no-op.
45
+ // Only the common `<dir>/*.conf` form resolves; other shapes are no-ops.
58
46
  let dir = null;
59
47
  if (glob.endsWith("/sshd_config.d/*.conf")) {
60
- // The canonical sshd-config drop-in directory. Honour the
61
- // path override (tests / chroot / sshd_config.d outside the
62
- // default location).
63
48
  dir = configDPath;
64
49
  } else {
65
50
  const dirMatch = glob.match(/^(.*)\/\*\.conf$/);
@@ -80,9 +65,7 @@ function expandSshdConfig(baseContent, configDPath) {
80
65
  }
81
66
 
82
67
  function parseSshdEffective(content) {
83
- // Best-effort: scan uncommented `PermitRootLogin <value>` and
84
- // `PasswordAuthentication <value>` lines. sshd_config is parsed
85
- // first-match-wins for most directives.
68
+ // First occurrence wins, as sshd_config parses these directives.
86
69
  if (!content) return { permitRootLogin: null, passwordAuth: null };
87
70
  const out = { permitRootLogin: null, passwordAuth: null };
88
71
  for (const raw of content.split(/\r?\n/)) {
@@ -100,10 +83,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
100
83
  const errors = [];
101
84
  const startTime = Date.now();
102
85
  const root = path.resolve(cwd);
103
- // Path-override hooks for tests: caller can pass args.paths to
104
- // redirect /proc / /sys / /etc reads to a synthetic tempdir
105
- // mirroring the real layout. Without overrides the collector
106
- // reads the live host paths.
86
+ // args.paths redirects the /proc, /sys and /etc reads at a synthetic tree.
107
87
  const paths = args.paths || {};
108
88
  const P = {
109
89
  kptrRestrict: paths.kptrRestrict || "/proc/sys/kernel/kptr_restrict",
@@ -117,9 +97,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
117
97
  sshdConfigD: paths.sshdConfigD || "/etc/ssh/sshd_config.d",
118
98
  kallsyms: paths.kallsyms || "/proc/kallsyms",
119
99
  };
120
- // Force-linux switch lets tests exercise the Linux code path even
121
- // when running on win32 / darwin. Without it, the platform gate
122
- // remains the source of truth for whether the collector runs.
100
+ // args.forceLinux exercises the Linux path from win32 / darwin.
123
101
  const isLinux = args.forceLinux === true || process.platform === "linux";
124
102
 
125
103
  if (!isLinux) {
@@ -160,8 +138,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
160
138
  };
161
139
  }
162
140
 
163
- // Sysctl reads. `null` means the sysctl path didn't exist (older
164
- // kernel / non-standard build).
141
+ // `null` means the sysctl path is absent (older kernel, non-standard build).
165
142
  const kptrRestrict = readSysctl(P.kptrRestrict);
166
143
  const unprivUserns = readSysctl(P.unprivUserns);
167
144
  const unprivBpf = readSysctl(P.unprivBpf);
@@ -170,21 +147,12 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
170
147
  const cmdline = readFileSafe(P.cmdline) || "";
171
148
  const lockdown = readSysctl(P.lockdown) || "";
172
149
 
173
- // sshd_config: expand Include directives in-place so the
174
- // first-match-wins parse honours OpenSSH's effective directive
175
- // order. On Debian/Ubuntu the default sshd_config begins with
176
- // `Include /etc/ssh/sshd_config.d/*.conf` — drop-in values take
177
- // precedence over the base file's later lines.
178
150
  const sshdBase = readFileSafe(P.sshdConfig);
179
151
  const sshdContent = sshdBase ? expandSshdConfig(sshdBase, P.sshdConfigD) : null;
180
152
  const sshdParsed = parseSshdEffective(sshdContent);
181
153
 
182
- // Indicator predicates. Each sysctl-derived indicator emits a
183
- // verdict ONLY when the underlying sysctl was readable; unreadable
184
- // sysctls (e.g. masked /proc in a constrained container, kernel
185
- // built without that knob) leave the indicator unflipped so the
186
- // runner returns inconclusive rather than forging a "hardened"
187
- // miss without evidence.
154
+ // A sysctl-derived indicator flips only when the sysctl was readable, so an
155
+ // unreadable knob reports inconclusive rather than a "hardened" miss.
188
156
  function fromSysctl(value, hitWhen) {
189
157
  if (value == null) return undefined; // unreadable → inconclusive
190
158
  return value === hitWhen ? "hit" : "miss";
@@ -200,9 +168,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
200
168
  const yamaSig = fromSysctl(yamaPtrace, "0");
201
169
  if (yamaSig !== undefined) signal_overrides["yama-ptrace-permissive"] = yamaSig;
202
170
 
203
- // /proc/cmdline derives kaslr / mitigations / lockdown=. If we
204
- // couldn't read it at all, those three indicators stay unflipped
205
- // (inconclusive) rather than asserting an absent string.
171
+ // An unreadable cmdline leaves both unflipped, not asserted against nothing.
206
172
  if (cmdline) {
207
173
  const kaslrDisabled = /\bnokaslr\b/.test(cmdline) || /\bkaslr=off\b/.test(cmdline);
208
174
  const mitigationsOff = /\bmitigations=off\b/.test(cmdline);
@@ -210,10 +176,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
210
176
  signal_overrides["mitigations-off"] = mitigationsOff ? "hit" : "miss";
211
177
  }
212
178
 
213
- // kernel-lockdown-none: the file shows `[none]` OR is absent and
214
- // /proc/cmdline carries no lockdown= parameter. When both the
215
- // lockdown file AND /proc/cmdline are unreadable, leave the
216
- // indicator unflipped.
179
+ // Lockdown is none when the file shows `[none]`, or it is absent and no
180
+ // lockdown= rides the cmdline. Both unreadable leaves the indicator unflipped.
217
181
  if (lockdown || cmdline) {
218
182
  const lockdownShowsNone = /\[none\]/.test(lockdown);
219
183
  const lockdownCmdline = /\blockdown=(?:integrity|confidentiality)\b/.test(cmdline);
@@ -223,8 +187,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
223
187
  signal_overrides["kernel-lockdown-none"] = lockdownNoneHit ? "hit" : "miss";
224
188
  }
225
189
 
226
- // sshd-permitrootlogin-yes: emit a verdict only when sshd_config
227
- // was readable. Missing config (no SSH server) → unflipped.
190
+ // A missing sshd_config (no SSH server) leaves this unflipped.
228
191
  let sshdRootHit = false;
229
192
  if (sshdContent !== null) {
230
193
  sshdRootHit =
@@ -235,30 +198,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
235
198
 
236
199
  const kptrHit = kptrSig === "hit";
237
200
 
238
- // Per-indicator __fp_checks attestation. The collector attests
239
- // ONLY the checks it actually performed; operator-judgement /
240
- // network-required FP checks remain unsatisfied so the runner
241
- // honestly downgrades to inconclusive.
242
- //
243
- // kptr-restrict-disabled:
244
- // [0] kdump / perf debug-session runbook — operator judgement
245
- // [1] /proc/kallsyms zero-leakage cross-check — collector CAN
246
- // attest (read first line as unprivileged user).
247
- // yama-ptrace-permissive:
248
- // [0] MAC enforcement (AppArmor / SELinux) — operator
249
- // [1] container observability — operator
250
- // [2] single-tenant dev VM — operator
251
- // kaslr-disabled-at-boot:
252
- // [0] kdump runbook — operator
253
- // [1] dmesg KASLR offsets — collector cannot read dmesg
254
- // without root on most distros (dmesg-restrict=1).
255
- // mitigations-off:
256
- // [0] HPC/benchmark exemption — operator
257
- // [1] single-tenant — operator
258
- //
259
- // For kptr-restrict-disabled the kallsyms cross-check is the only
260
- // FP-check the collector can attest; we attest index 1 when we
261
- // observed the kallsyms first line carries non-zero hex.
201
+ // __fp_checks attests only the checks actually performed; the rest stay
202
+ // unsatisfied so the runner downgrades to inconclusive. Index 1 of
203
+ // kptr-restrict-disabled is the /proc/kallsyms leakage cross-check.
262
204
  if (kptrHit) {
263
205
  let kallsymsLeaks = false;
264
206
  try {
@@ -1,18 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/kernel.js
5
- *
6
- * Companion collector for the `kernel` playbook. Establishes
7
- * preconditions (linux-platform / uname-available) and captures the
8
- * kernel release string for the kver-in-affected-range indicator.
9
- *
10
- * Scope: Linux only. On macOS / Windows the playbook's linux-platform
11
- * precondition halts at preflight; the collector reports that
12
- * truthfully so the operator sees the visibility gap without the
13
- * runner having to re-derive it.
14
- *
15
- * Interface: see lib/collectors/README.md
4
+ * Companion collector for the `kernel` playbook: the linux-platform and
5
+ * uname-available preconditions, plus the kernel release, cmdline and sysctl
6
+ * snapshot its indicators read. Off Linux it reports the visibility gap rather
7
+ * than staying silent. Interface: lib/collectors/README.md
16
8
  */
17
9
 
18
10
  const { execFileSync } = require("node:child_process");
@@ -32,12 +24,8 @@ function runUname(arg) {
32
24
  function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
33
25
  const errors = [];
34
26
 
35
- // Precondition 1: linux-platform. Use process.platform first
36
- // (Node-derived, always available); cross-check against uname -s
37
- // when available.
38
27
  const linuxPlatform = process.platform === "linux";
39
28
 
40
- // Precondition 2: uname-available. Pure capability check.
41
29
  const unameR = runUname("-r");
42
30
  const unameAvailable = unameR.ok;
43
31
  if (!unameAvailable && linuxPlatform) {
@@ -47,10 +35,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
47
35
  });
48
36
  }
49
37
 
50
- // Artifact: kernel-release. The exact string returned by `uname -r`,
51
- // e.g. "5.15.0-69-generic". When uname is unavailable, the artifact
52
- // is captured=false with the reason; the runner treats the
53
- // dependent indicators as inconclusive.
38
+ // captured=false carries the reason, so the runner reads dependent indicators
39
+ // as inconclusive rather than as a miss.
54
40
  const artifacts = {};
55
41
  if (unameR.ok) {
56
42
  artifacts["kernel-release"] = { value: unameR.value, captured: true };
@@ -64,9 +50,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
64
50
  };
65
51
  }
66
52
 
67
- // Optional artifact: cmdline. Not always required but useful for
68
- // KASLR / unpriv-userns / unpriv-bpf indicator evaluation. Read
69
- // from /proc directly so we don't fork another process.
53
+ // kernel-cmdline feeds the KASLR / unpriv-userns / unpriv-bpf indicators.
70
54
  if (linuxPlatform) {
71
55
  try {
72
56
  const fs = require("node:fs");
@@ -79,8 +63,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
79
63
  reason: `/proc/cmdline read failed: ${e.message}`,
80
64
  });
81
65
  }
82
- // sysctl snapshot for kernel.unprivileged_userns_clone +
83
- // kernel.unprivileged_bpf_disabled when readable.
84
66
  try {
85
67
  const fs = require("node:fs");
86
68
  const sysctls = {};
@@ -93,8 +75,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
93
75
  try {
94
76
  sysctls[path.basename(p)] = fs.readFileSync(p, "utf8").trim();
95
77
  } catch {
96
- // Best-effort; a missing file usually means the sysctl
97
- // doesn't exist on this kernel.
78
+ // A missing file means the sysctl does not exist on this kernel.
98
79
  }
99
80
  }
100
81
  if (Object.keys(sysctls).length) {
@@ -109,10 +90,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
109
90
  }
110
91
  }
111
92
 
112
- // Running-kernel build config (/boot/config-$(uname -r)) backs the
113
- // CONFIG_* false_positive_checks_required entries: a sysctl is moot if the
114
- // feature is compiled out of the kernel. Best-effort — unreadable on many
115
- // hardened hosts.
93
+ // The running kernel's build config backs the CONFIG_* false-positive checks:
94
+ // a sysctl is moot when the feature is compiled out. Often unreadable.
116
95
  let kernelConfig = null;
117
96
  if (linuxPlatform && unameR.ok) {
118
97
  try {
@@ -121,10 +100,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
121
100
  } catch { /* config not readable — CONFIG_* checks stay unattested */ }
122
101
  }
123
102
 
124
- // Signal overrides: we can't decide kver-in-affected-range without
125
- // the CVE-affected-version catalog (the runner does that
126
- // correlation). But we CAN flip the deterministic indicators that
127
- // read directly off the sysctl snapshot.
103
+ // kver-in-affected-range needs the CVE affected-version catalog, so the runner
104
+ // correlates that; only straight sysctl reads are decided here.
128
105
  const signal_overrides = {};
129
106
  const sysctl = artifacts["sysctl-snapshot"];
130
107
  if (sysctl && sysctl.captured) {
@@ -136,29 +113,22 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
136
113
  const v = parseInt(parsed.randomize_va_space, 10);
137
114
  signal_overrides["kaslr-disabled"] = (v < 2) ? "hit" : "miss";
138
115
  }
139
- // unpriv-userns-enabled: clone == 1 means enabled (risky).
116
+ // unprivileged_userns_clone == 1 means unprivileged userns is enabled.
140
117
  if (parsed.unprivileged_userns_clone != null) {
141
118
  const v = parseInt(parsed.unprivileged_userns_clone, 10);
142
119
  const hit = v === 1;
143
120
  signal_overrides["unpriv-userns-enabled"] = hit ? "hit" : "miss";
144
- // FP[1]: CONFIG_USER_NS=y — the sysctl is live only when userns is
145
- // compiled in. Attested when /boot/config confirms it. FP[0]
146
- // (rootless-runtime exception + LSM enforcement) is operator
147
- // judgement and stays unattested.
121
+ // Attests FP[1] only: the sysctl is live only when userns is compiled in.
148
122
  if (hit && kernelConfig && /^CONFIG_USER_NS=y$/m.test(kernelConfig)) {
149
123
  signal_overrides["unpriv-userns-enabled__fp_checks"] = { "1": true };
150
124
  }
151
125
  }
152
- // unpriv-bpf-allowed: bpf_disabled == 0 means unprivileged BPF
153
- // is allowed (risky).
126
+ // unprivileged_bpf_disabled == 0 means unprivileged BPF is allowed.
154
127
  if (parsed.unprivileged_bpf_disabled != null) {
155
128
  const v = parseInt(parsed.unprivileged_bpf_disabled, 10);
156
129
  const hit = v === 0;
157
130
  signal_overrides["unpriv-bpf-allowed"] = hit ? "hit" : "miss";
158
- // FP[0]: CONFIG_BPF_SYSCALL=y AND CONFIG_BPF_JIT=y — the sysctl is
159
- // moot if BPF is compiled out. Attested when /boot/config confirms
160
- // both. FP[1] (enforcing LSM bpf() restriction) is operator
161
- // judgement and stays unattested.
131
+ // Attests FP[0] only: the sysctl is moot if BPF is compiled out.
162
132
  if (hit && kernelConfig &&
163
133
  /^CONFIG_BPF_SYSCALL=y$/m.test(kernelConfig) &&
164
134
  /^CONFIG_BPF_JIT=y$/m.test(kernelConfig)) {