@blamejs/exceptd-skills 0.19.32 → 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 (127) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +8 -8
  4. package/data/_indexes/activity-feed.json +2 -2
  5. package/data/_indexes/catalog-summaries.json +7 -7
  6. package/data/_indexes/chains.json +60118 -0
  7. package/data/attack-techniques.json +267 -7
  8. package/data/cve-catalog.json +9991 -3
  9. package/data/cwe-catalog.json +109 -2
  10. package/data/framework-control-gaps.json +578 -3
  11. package/data/zeroday-lessons.json +8330 -1
  12. package/lib/auto-discovery.js +56 -286
  13. package/lib/canonical-eq.js +7 -40
  14. package/lib/citation-resolve.js +22 -70
  15. package/lib/collectors/ai-api.js +20 -54
  16. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  17. package/lib/collectors/citation-hygiene.js +72 -210
  18. package/lib/collectors/containers.js +41 -130
  19. package/lib/collectors/cred-stores.js +31 -115
  20. package/lib/collectors/crypto-codebase.js +55 -138
  21. package/lib/collectors/crypto.js +24 -54
  22. package/lib/collectors/hardening.js +20 -78
  23. package/lib/collectors/kernel.js +16 -46
  24. package/lib/collectors/library-author.js +57 -206
  25. package/lib/collectors/mcp.js +24 -70
  26. package/lib/collectors/runtime.js +24 -86
  27. package/lib/collectors/sbom.js +34 -106
  28. package/lib/collectors/scan-excludes.js +31 -138
  29. package/lib/collectors/secrets.js +62 -178
  30. package/lib/cross-ref-api.js +39 -123
  31. package/lib/currency-severity.js +8 -27
  32. package/lib/cve-batch.js +13 -21
  33. package/lib/cve-cli.js +13 -20
  34. package/lib/cve-curation.js +72 -239
  35. package/lib/cve-regression-watcher.js +29 -152
  36. package/lib/cvss.js +13 -54
  37. package/lib/doctor-bucketing.js +3 -19
  38. package/lib/exit-codes.js +10 -42
  39. package/lib/flag-suggest.js +7 -25
  40. package/lib/framework-gap.js +35 -114
  41. package/lib/gap-detectors.js +37 -159
  42. package/lib/id-validation.js +9 -30
  43. package/lib/job-queue.js +13 -36
  44. package/lib/lint-skills.js +64 -232
  45. package/lib/playbook-runner.js +693 -2095
  46. package/lib/prefetch.js +100 -376
  47. package/lib/refresh-external.js +199 -627
  48. package/lib/refresh-network.js +75 -307
  49. package/lib/rfc-cli.js +23 -68
  50. package/lib/scoring.js +77 -145
  51. package/lib/sign.js +43 -229
  52. package/lib/source-advisories.js +43 -194
  53. package/lib/source-ghsa.js +37 -120
  54. package/lib/source-osv.js +94 -266
  55. package/lib/ttp-mapper.js +14 -24
  56. package/lib/upstream-check-cli.js +10 -28
  57. package/lib/upstream-check.js +19 -44
  58. package/lib/validate-catalog-meta.js +17 -61
  59. package/lib/validate-cve-catalog.js +43 -119
  60. package/lib/validate-indexes.js +25 -76
  61. package/lib/validate-package.js +16 -62
  62. package/lib/validate-playbooks.js +69 -275
  63. package/lib/validate-vendor.js +16 -49
  64. package/lib/verify.js +56 -286
  65. package/lib/version-pins.js +5 -34
  66. package/lib/worker-pool.js +11 -30
  67. package/lib/xml-tokenizer.js +47 -152
  68. package/manifest.json +53 -53
  69. package/orchestrator/dispatcher.js +17 -68
  70. package/orchestrator/event-bus.js +11 -74
  71. package/orchestrator/index.js +138 -412
  72. package/orchestrator/pipeline.js +28 -85
  73. package/orchestrator/scanner.js +34 -138
  74. package/orchestrator/scheduler.js +20 -84
  75. package/package.json +2 -2
  76. package/sbom.cdx.json +253 -253
  77. package/scripts/audit-catalog-gaps.js +9 -62
  78. package/scripts/audit-cross-skill.js +5 -31
  79. package/scripts/audit-perf.js +6 -16
  80. package/scripts/backfill-theater-test.js +7 -64
  81. package/scripts/bootstrap.js +12 -44
  82. package/scripts/build-indexes.js +40 -154
  83. package/scripts/builders/activity-feed.js +4 -14
  84. package/scripts/builders/catalog-summaries.js +3 -10
  85. package/scripts/builders/currency.js +7 -20
  86. package/scripts/builders/cwe-chains.js +7 -30
  87. package/scripts/builders/did-ladders.js +6 -13
  88. package/scripts/builders/frequency.js +5 -19
  89. package/scripts/builders/jurisdiction-clocks.js +6 -25
  90. package/scripts/builders/recipes.js +6 -14
  91. package/scripts/builders/section-offsets.js +13 -51
  92. package/scripts/builders/stale-content.js +7 -28
  93. package/scripts/builders/summary-cards.js +8 -29
  94. package/scripts/builders/theater-fingerprints.js +12 -27
  95. package/scripts/builders/token-budget.js +4 -31
  96. package/scripts/check-agents-md-collectors.js +11 -54
  97. package/scripts/check-catalog-gap-budget.js +15 -32
  98. package/scripts/check-changelog-extract.js +18 -48
  99. package/scripts/check-codebase-patterns-currency.js +6 -22
  100. package/scripts/check-codebase-patterns.js +50 -143
  101. package/scripts/check-epss-consistency.js +9 -64
  102. package/scripts/check-framework-gap-coverage.js +13 -31
  103. package/scripts/check-manifest-snapshot.js +13 -73
  104. package/scripts/check-sbom-currency.js +44 -142
  105. package/scripts/check-test-count.js +15 -52
  106. package/scripts/check-test-coverage.js +66 -197
  107. package/scripts/check-test-subjects.js +21 -62
  108. package/scripts/check-ttp-references.js +14 -38
  109. package/scripts/check-ttp-upstream.js +8 -40
  110. package/scripts/check-version-bump.js +9 -61
  111. package/scripts/check-version-tags.js +20 -121
  112. package/scripts/predeploy.js +38 -184
  113. package/scripts/refresh-manifest-snapshot.js +16 -38
  114. package/scripts/refresh-mitre-atlas.js +3 -8
  115. package/scripts/refresh-mitre-attack.js +1 -8
  116. package/scripts/refresh-mitre-d3fend.js +3 -9
  117. package/scripts/refresh-mitre-ics-attack.js +3 -8
  118. package/scripts/refresh-reverse-refs.js +27 -94
  119. package/scripts/refresh-rfc-index.js +2 -10
  120. package/scripts/refresh-sbom.js +31 -161
  121. package/scripts/refresh-upstream-catalogs.js +40 -137
  122. package/scripts/release.js +69 -232
  123. package/scripts/run-e2e-scenarios.js +24 -71
  124. package/scripts/sync-manifest-metadata.js +10 -34
  125. package/scripts/sync-package-description.js +8 -17
  126. package/scripts/validate-vendor-online.js +13 -44
  127. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,25 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/mcp.js
5
- *
6
- * Companion collector for the `mcp` playbook. Reads MCP client
7
- * config files (Cursor, Claude Code, Windsurf, VS Code Copilot,
8
- * Gemini CLI) and tool-response logs, flipping deterministic
9
- * indicators related to:
10
- * - mcp-version-without-integrity (pinned version without
11
- * sha256 / sri-integrity sibling)
12
- * - copilot-yolo-mode-flag (chat.tools.autoApprove true)
13
- * - mcp-response-ansi-escape (0x1B in tool response)
14
- * - mcp-response-unicode-tag-smuggling (U+E0000..U+E007F)
15
- *
16
- * Defers:
17
- * - unsigned-mcp-manifest (needs npm/pip package directory walk
18
- * + sigstore lookup; out of stdlib scope)
19
- * - vulnerable-windsurf-version (needs Windsurf install detection)
20
- * - mcp-server-running-as-root (needs ps + capabilities)
21
- * - mcp-server-invoked-from-ci-pipeline (needs process-tree
22
- * env-var inspection)
4
+ * Companion collector for the `mcp` playbook: reads MCP client configs and
5
+ * tool-response logs, flipping only the indicators decidable from those.
23
6
  *
24
7
  * Interface: see lib/collectors/README.md
25
8
  */
@@ -36,10 +19,8 @@ function readSafe(full, max = 1024 * 1024) {
36
19
  fd = fs.openSync(full, "r");
37
20
  const s = fs.fstatSync(fd);
38
21
  if (s.size > max) return null;
39
- // readFileSync(fd) loops read() to EOF — a single readSync may return
40
- // fewer than s.size bytes on network/FUSE/sync-backed fds, which would
41
- // leave the buffer tail NUL-filled and silently drop trailing content.
42
- // Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
22
+ // readFileSync(fd) loops read() to EOF, where one readSync can return short on
23
+ // a network/FUSE fd and NUL-fill the tail; via the fd it is also TOCTOU-free.
43
24
  return fs.readFileSync(fd, "utf8");
44
25
  } catch { return null; }
45
26
  finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
@@ -55,9 +36,8 @@ function fileExists(full) {
55
36
  try { return fs.statSync(full).isFile(); } catch { return false; }
56
37
  }
57
38
 
58
- // Per-vendor config locations. We try each and capture which were
59
- // present so the artifact field tells the operator what scope the
60
- // collector actually exercised.
39
+ // Per-vendor config locations; presence is recorded so the artifact shows the
40
+ // scope the collector actually exercised.
61
41
  function vendorConfigPaths(home) {
62
42
  return {
63
43
  cursor: [path.join(home, ".cursor", "mcp.json")],
@@ -67,7 +47,6 @@ function vendorConfigPaths(home) {
67
47
  ],
68
48
  windsurf: [path.join(home, ".codeium", "windsurf", "mcp_config.json")],
69
49
  "vscode-copilot": [
70
- // user-global locations
71
50
  path.join(home, ".config", "Code", "User", "settings.json"),
72
51
  path.join(home, "Library", "Application Support", "Code", "User", "settings.json"),
73
52
  path.join(home, "AppData", "Roaming", "Code", "User", "settings.json"),
@@ -85,9 +64,8 @@ function collectMcpServers(configsByVendor) {
85
64
  for (const f of files) {
86
65
  const j = readJson(f);
87
66
  if (!j) continue;
88
- // Cursor / Claude Code / Windsurf: { mcpServers: { <name>: {...} } }
89
- // VS Code Copilot: { chat: { mcp: { servers: { <name>: {...} } } } }
90
- // Gemini: { mcpServers: { <name>: {...} } }
67
+ // Cursor / Claude Code / Windsurf / Gemini nest servers under
68
+ // `mcpServers`; VS Code Copilot under `chat.mcp.servers`, flat or nested.
91
69
  const containers = [
92
70
  j.mcpServers,
93
71
  j["chat.mcp.servers"],
@@ -109,10 +87,8 @@ function collectMcpServers(configsByVendor) {
109
87
  return out;
110
88
  }
111
89
 
112
- // A pinned version is the @1.2.3 / @v1.2.3 / =1.2.3 trailer on a
113
- // package spec in args[] or command. We look for shapes that
114
- // downstream operators care about: `npx -y @vendor/pkg@1.2.3`,
115
- // `uvx pkg==1.2.3`, `pip install pkg==1.2.3`, etc.
90
+ // A pinned version is the `@1.2.3` / `==1.2.3` trailer on a package spec in
91
+ // args[] or command — `npx -y @vendor/pkg@1.2.3`, `uvx pkg==1.2.3`.
116
92
  function isPinnedNoIntegrity(server) {
117
93
  if (server.integrity) return false;
118
94
  const tokens = [server.command, ...server.args].filter(Boolean);
@@ -120,20 +96,14 @@ function isPinnedNoIntegrity(server) {
120
96
  if (typeof tok !== "string") continue;
121
97
  // npm package@version (excluding scope name@version)
122
98
  if (/@[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@\d+\.\d+\.\d+\b/.test(tok)) return true;
123
- // A `==X.Y.Z` pin (pip/uvx/poetry) with no integrity is the python analog
124
- // of the npm pin above. A single-token launch command like
125
- // `sh -c "pip install some-mcp==1.2.3"` carries the whole pip invocation in
126
- // one token, so testing the token for `pip install` and `continue`-ing here
127
- // skipped the `==version` check below for exactly the shape the comment
128
- // says operators care about. A bare `pip install requests` (no `==`) can't
129
- // match the pin regex, so checking every token is safe.
99
+ // The pip / uvx analog. Every token is tested rather than pre-filtering on
100
+ // `pip install`, since `sh -c "pip install some-mcp==1.2.3"` is one token.
130
101
  if (/==\d+\.\d+\.\d+/.test(tok)) return true;
131
102
  }
132
103
  return false;
133
104
  }
134
105
 
135
- // VS Code yolo-mode flag: chat.tools.autoApprove === true OR any
136
- // per-tool autoApprove === true.
106
+ // VS Code yolo mode: chat.tools.autoApprove, or any per-tool autoApprove.
137
107
  function hasYoloMode(j) {
138
108
  if (!j || typeof j !== "object") return false;
139
109
  if (j["chat.tools.autoApprove"] === true) return true;
@@ -153,9 +123,8 @@ function hasAnsiEscape(content) {
153
123
  return content.indexOf("\x1b") !== -1;
154
124
  }
155
125
 
156
- // Unicode tag smuggling: codepoints in U+E0000..U+E007F. These are
157
- // invisible in normal renders but carry instruction-coercion
158
- // payloads through MCP responses.
126
+ // Unicode tag smuggling: U+E0000..U+E007F are invisible in a normal render but
127
+ // carry instruction-coercion payloads through MCP responses.
159
128
  function hasUnicodeTagSmuggling(content) {
160
129
  for (let i = 0; i < content.length; i++) {
161
130
  const code = content.codePointAt(i);
@@ -167,11 +136,8 @@ function hasUnicodeTagSmuggling(content) {
167
136
  }
168
137
 
169
138
  function findMcpLogs(home) {
170
- // Candidate log directories per vendor. Caller scans each for
171
- // *.jsonl / *.log files. Directories like ~/.claude/logs/mcp/ are
172
- // already MCP-scoped by path; we accept every log file inside.
173
- // Generic log directories (~/.cursor/logs/) are filtered to
174
- // filenames containing "mcp".
139
+ // A path already MCP-scoped (~/.claude/logs/mcp/) accepts every log file
140
+ // inside; a generic one (~/.cursor/logs/) is filtered to names holding "mcp".
175
141
  const dirs = [
176
142
  { path: path.join(home, ".claude", "logs", "mcp"), filterByName: false },
177
143
  { path: path.join(home, ".cursor", "logs"), filterByName: true },
@@ -197,26 +163,22 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
197
163
  const root = path.resolve(cwd);
198
164
  const home = (env && env.HOME) || (env && env.USERPROFILE) || os.homedir();
199
165
 
200
- // Vendor MCP configs.
201
166
  const configsByVendor = vendorConfigPaths(home);
202
167
  const servers = collectMcpServers(configsByVendor);
203
168
  const pinnedNoIntegrity = servers.filter(isPinnedNoIntegrity);
204
169
 
205
- // VS Code yolo-mode across the candidate VS Code settings files.
206
170
  let yoloHit = false;
207
171
  let yoloFile = null;
208
172
  for (const f of configsByVendor["vscode-copilot"]) {
209
173
  const j = readJson(f);
210
174
  if (j && hasYoloMode(j)) { yoloHit = true; yoloFile = f; break; }
211
175
  }
212
- // Project-level .vscode/settings.json under cwd
213
176
  if (!yoloHit) {
214
177
  const projVsc = path.join(root, ".vscode", "settings.json");
215
178
  const j = readJson(projVsc);
216
179
  if (j && hasYoloMode(j)) { yoloHit = true; yoloFile = projVsc; }
217
180
  }
218
181
 
219
- // MCP response log scan for ANSI / unicode-tag smuggling.
220
182
  const logFiles = findMcpLogs(home);
221
183
  let ansiHit = false;
222
184
  let unicodeTagHit = false;
@@ -239,10 +201,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
239
201
  "mcp-version-without-integrity": pinnedNoIntegrity.length > 0 ? "hit" : "miss",
240
202
  "copilot-yolo-mode-flag": yoloHit ? "hit" : "miss",
241
203
  };
242
- // ANSI / unicode-tag indicators: only emit when we actually
243
- // scanned at least one log file. If no logs exist, leave the
244
- // indicators unflipped (the operator may not have enabled MCP
245
- // logging — the runner returns inconclusive).
204
+ // Emitted only when a log file was actually scanned: with none, the indicators
205
+ // stay unflipped and the runner returns inconclusive.
246
206
  if (logsScanned) {
247
207
  signal_overrides["mcp-response-ansi-escape"] = ansiHit ? "hit" : "miss";
248
208
  signal_overrides["mcp-response-unicode-tag-smuggling"] = unicodeTagHit ? "hit" : "miss";
@@ -298,17 +258,11 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
298
258
  },
299
259
  };
300
260
 
301
- // Attest any-ai-coding-assistant-installed (skip_phase gate) ONLY when a
302
- // client is confirmed present — an install directory the precondition itself
303
- // recognises (exists($HOME/.cursor)||.codeium/windsurf||.config/claude||
304
- // .config/Code||.gemini), OR a vendor config file we found. The runner can't
305
- // resolve the exists() DSL against the scanned home, so without this a host
306
- // that clearly has a client surfaces a spurious precondition_unverified.
307
- // Crucially we NEVER submit false: the gate is skip_phase, so a false on a
308
- // config-file-only absence would wrongly skip the detect phase even when an
309
- // installed client directory or a project-level signal (e.g. a .vscode
310
- // Copilot config) exists. When presence can't be confirmed, leave the gate
311
- // unsubmitted and let the host-side resolver decide.
261
+ // Attest the any-ai-coding-assistant-installed skip_phase gate ONLY when a
262
+ // client is confirmed present; unconfirmed leaves it for the host-side
263
+ // resolver. Never submit false: the gate is skip_phase, so a false on a
264
+ // config-file absence would skip detect on a host that has an installed client
265
+ // directory or a project-level Copilot config.
312
266
  const assistantInstallDirs = [
313
267
  ".cursor",
314
268
  path.join(".codeium", "windsurf"),
@@ -1,17 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/collectors/runtime.js
5
- *
6
- * Companion collector for the `runtime` playbook. Linux-only:
7
- * walks /etc/sudoers + /etc/sudoers.d/*, parses /etc/passwd for
8
- * duplicate UID 0 entries, scans well-known trusted-path
9
- * directories for world-writable files, and inspects /proc/<pid>
10
- * for orphan-privileged processes. Defers non-deterministic
11
- * indicators (non-baseline-suid, listening-socket-unknown-bind,
12
- * cron-or-timer-outside-policy) so the runner returns inconclusive.
13
- *
14
- * Interface: see lib/collectors/README.md
4
+ * Companion collector for the `runtime` playbook. Linux-only, deterministic
5
+ * indicators only — anything it cannot settle stays unflipped for the runner to
6
+ * call inconclusive. Interface: see lib/collectors/README.md
15
7
  */
16
8
 
17
9
  const fs = require("node:fs");
@@ -25,10 +17,8 @@ function readFileSafe(p, max = 512 * 1024) {
25
17
  fd = fs.openSync(p, "r");
26
18
  const s = fs.fstatSync(fd);
27
19
  if (s.size > max) return null;
28
- // readFileSync(fd) loops read() to EOF — a single readSync may return
29
- // fewer than s.size bytes on network/FUSE/sync-backed fds, which would
30
- // leave the buffer tail NUL-filled and silently drop trailing content.
31
- // Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
20
+ // readFileSync(fd) loops to EOF; a single readSync can short-read a network
21
+ // fd. Reading through the open fd also keeps the fstat-then-read TOCTOU-free.
32
22
  return fs.readFileSync(fd, "utf8");
33
23
  } catch { return null; }
34
24
  finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
@@ -36,10 +26,7 @@ function readFileSafe(p, max = 512 * 1024) {
36
26
 
37
27
  const NOPASSWD_WILDCARD_RE = /\bNOPASSWD:\s*(?:ALL|\/[^,\n]*\*)/;
38
28
 
39
- // Trusted-path roots scanned for world-writable files. The
40
- // playbook explicitly lists /etc, /usr/local/bin, /usr/local/sbin,
41
- // /opt. We add /usr/bin and /usr/sbin as common variants but stay
42
- // shallow (depth 2) so we don't walk arbitrarily deep filesystems.
29
+ // World-writable scan roots; the depth cap keeps the walk off deep filesystems.
43
30
  const TRUSTED_PATHS = ["/etc", "/usr/local/bin", "/usr/local/sbin", "/opt", "/usr/bin", "/usr/sbin"];
44
31
  const TRUSTED_PATH_MAX_DEPTH = 2;
45
32
 
@@ -56,11 +43,8 @@ function parseSudoersForWildcards(content) {
56
43
  const line = raw.replace(/#.*$/, "").trim();
57
44
  if (!line) continue;
58
45
  if (!NOPASSWD_WILDCARD_RE.test(line)) continue;
59
- // Sudoers entries lead with a comma-separated user/group list:
60
- // `<user1>,<user2>,%group host = (runas) NOPASSWD: cmd*`
61
- // Skip ONLY when the user list is exactly `root` (no other
62
- // principals). A line like `root,deploy ALL=(ALL) NOPASSWD: ALL`
63
- // still grants wildcard sudo to `deploy` and counts as a hit.
46
+ // Skip only when the principal list is exactly `root` — `root,deploy ALL=(ALL)
47
+ // NOPASSWD: ALL` still grants wildcard sudo to deploy and counts as a hit.
64
48
  const userListMatch = line.match(/^([^\s]+)\s/);
65
49
  if (userListMatch) {
66
50
  const principals = userListMatch[1].split(",").map(s => s.trim()).filter(Boolean);
@@ -113,12 +97,8 @@ function isWorldWritable(p) {
113
97
  } catch { return false; }
114
98
  }
115
99
 
116
- // Classify a world-writable hit against the two deterministic
117
- // false_positive_checks_required entries for world-writable-in-trusted-path:
118
- // [0] sticky-bit (1777-style) dirs/files intentionally permit per-user write
119
- // [1] 0-byte stamp / unix-socket / FIFO documented for the application
120
- // Returns { stickyBit, special } so the caller can both keep only genuine
121
- // hits and attest exactly the checks it ran.
100
+ // The two deterministic FP checks on world-writable-in-trusted-path: [0] sticky-bit
101
+ // per-user-write, [1] 0-byte stamp / socket / FIFO. The caller drops carriers.
122
102
  function classifyWorldWritable(p) {
123
103
  try {
124
104
  const s = fs.lstatSync(p);
@@ -150,33 +130,21 @@ function readProcPid(pid, procRoot) {
150
130
  }
151
131
 
152
132
  function scanOrphanPrivileged(procRoot) {
153
- // Returns { hits, exeReadable }. exeReadable is false when at
154
- // least one candidate PID (UID 0, PPID 1) had an unreadable
155
- // /proc/<pid>/exe symlink AND PID 1's exe was also unreadable —
156
- // in that case the indicator MUST stay unflipped (the caller
157
- // converts that to inconclusive). hidepid / ptrace-restrict /
158
- // non-root scope all collapse exe readability to zero, and
159
- // emitting "miss" there would mask real privileged orphans.
133
+ // Returns { hits, exeReadable }. exeReadable false means no exe-link visibility
134
+ // and the caller must leave the indicator unflipped — a "miss" masks real orphans.
160
135
  let entries;
161
136
  try { entries = fs.readdirSync(procRoot); } catch { return { hits: [], exeReadable: false }; }
162
137
  const hits = [];
163
138
 
164
- // PID 1's exe is the canonical anchor. If we can't read it, we
165
- // can't tell whether the parent is canonical init or a hijacked
166
- // PID 1. That's an inconclusive condition.
139
+ // PID 1's exe is the anchor: unreadable, canonical init and a hijacked PID 1 look alike.
167
140
  const initProc = readProcPid(1, procRoot);
168
141
  const initExeReadable = !!(initProc && initProc.exe);
169
142
  if (!initExeReadable) return { hits: [], exeReadable: false };
170
143
  if (!PARENT_INIT_BINARIES.has(initProc.exe)) {
171
- // PID 1's exe is readable but isn't a canonical init. Treat
172
- // as inconclusive — the playbook predicate explicitly says
173
- // "parent != systemd/init" is the orphan condition, but
174
- // distinguishing a legitimate non-systemd init (e.g. SysV,
175
- // BusyBox init) from a hijacked PID 1 needs operator review.
144
+ // Not a canonical init: SysV / BusyBox vs a hijacked PID 1 needs operator review.
176
145
  return { hits: [], exeReadable: false };
177
146
  }
178
147
 
179
- // PID 1's exe is canonical init. Now scan candidate PIDs.
180
148
  let anyExeReadable = false;
181
149
  let anyExeUnreadable = false;
182
150
  for (const name of entries) {
@@ -194,12 +162,7 @@ function scanOrphanPrivileged(procRoot) {
194
162
  anyExeUnreadable = true;
195
163
  }
196
164
  }
197
- // If we couldn't read ANY candidate's exe (typical non-root /
198
- // hidepid scope), the indicator stays unflipped. If we read at
199
- // least one and saw zero risky-path orphans, the miss is
200
- // honest. If we saw mixed (some readable, some not), still
201
- // report what we found — but only emit a verdict when at least
202
- // one exe was readable so we have evidence to back it.
165
+ // A verdict needs at least one readable exe behind it; with none, it stays unflipped.
203
166
  const exeReadable = anyExeReadable || (!anyExeUnreadable && entries.length > 0);
204
167
  return { hits, exeReadable };
205
168
  }
@@ -240,14 +203,12 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
240
203
  };
241
204
  }
242
205
 
243
- // Sudo rules: /etc/sudoers + /etc/sudoers.d/*
244
206
  const sudoersBase = readFileSafe(P.sudoers);
245
207
  let sudoersContent = sudoersBase || "";
246
208
  let sudoersReadable = sudoersBase != null;
247
209
  try {
248
210
  const dEntries = fs.readdirSync(P.sudoersD);
249
211
  for (const e of dEntries) {
250
- // skip editor backup files (~, .bak, .swp)
251
212
  if (/[~]$/.test(e) || /\.(bak|swp|orig)$/.test(e)) continue;
252
213
  const c = readFileSafe(path.join(P.sudoersD, e));
253
214
  if (c != null) {
@@ -258,16 +219,10 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
258
219
  } catch { /* .d not present */ }
259
220
  const sudoersHits = parseSudoersForWildcards(sudoersContent);
260
221
 
261
- // /etc/passwd UID-zero count
262
222
  const passwdContent = readFileSafe(P.passwd);
263
223
  const uid0 = passwdContent ? parsePasswdUidZero(passwdContent) : null;
264
224
 
265
- // World-writable files under trusted paths. Split into genuine hits
266
- // (regular non-empty files without the sticky bit) and benign carriers
267
- // the two false_positive_checks_required entries demote (sticky-bit
268
- // per-user-write dirs; 0-byte stamps / sockets / FIFOs). Only the
269
- // genuine hits flip the indicator; the split records which FP checks
270
- // the collector deterministically ran.
225
+ // Genuine hits are regular non-empty files without the sticky bit; carriers are demoted.
271
226
  const worldWritableFiles = [];
272
227
  let sawStickyBitCarrier = false;
273
228
  let sawSpecialCarrier = false;
@@ -281,8 +236,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
281
236
  }
282
237
  }
283
238
 
284
- // Orphan-privileged process scan. Only meaningful on a real
285
- // /proc; on a synthetic tempdir we still walk it the same way.
286
239
  const orphanScan = scanOrphanPrivileged(P.procRoot);
287
240
 
288
241
  const signal_overrides = {};
@@ -292,29 +245,21 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
292
245
  if (uid0 !== null) {
293
246
  signal_overrides["duplicate-uid-zero"] = uid0.length > 1 ? "hit" : "miss";
294
247
  }
295
- // world-writable-in-trusted-path: only emit a verdict if at least
296
- // one trusted path was readable. If every TP was unreadable
297
- // (chroot / restricted container), leave the indicator unflipped.
248
+ // A verdict needs one readable trusted path; an all-unreadable scope stays unflipped.
298
249
  const anyTpReadable = P.trustedPaths.some(tp => {
299
250
  try { fs.readdirSync(tp); return true; } catch { return false; }
300
251
  });
301
252
  if (anyTpReadable) {
302
253
  const wwHit = worldWritableFiles.length > 0;
303
254
  signal_overrides["world-writable-in-trusted-path"] = wwHit ? "hit" : "miss";
304
- // Attest the false_positive_checks_required entries the collector
305
- // ran against every flagged file: [0] sticky-bit carriers and [1]
306
- // 0-byte/socket/FIFO carriers were both stat-inspected and excluded,
307
- // so a surviving hit satisfies both. Without this attestation the
308
- // runner downgrades a real world-writable hit to inconclusive.
255
+ // Both FP checks ran against every flagged file, so a surviving hit satisfies
256
+ // them; without this attestation the runner downgrades the hit to inconclusive.
309
257
  if (wwHit) {
310
258
  signal_overrides["world-writable-in-trusted-path__fp_checks"] = { "0": true, "1": true };
311
259
  }
312
260
  }
313
- // orphan-privileged-process: only emit when /proc was walkable
314
- // AND the scan had enough exe-link visibility to reach a verdict.
315
- // hidepid / ptrace-restrict / non-root scope leaves
316
- // exeReadable=false; the indicator stays unflipped so the runner
317
- // returns inconclusive (codex P1 #80).
261
+ // A verdict needs a walkable /proc and exe-link visibility; otherwise exeReadable
262
+ // is false and the indicator stays unflipped.
318
263
  let procWalkable = false;
319
264
  try { fs.readdirSync(P.procRoot); procWalkable = true; } catch { /* not present */ }
320
265
  if (procWalkable && orphanScan.exeReadable) {
@@ -341,16 +286,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
341
286
  return {
342
287
  precondition_checks: {
343
288
  "linux-platform": true,
344
- // The collector performs a read-only inventory directly via fs (reads
345
- // /etc/sudoers, /etc/passwd, /proc/<pid>/status) and execs no commands,
346
- // so the `exec-allowed` precondition ('permitted to execute read-only
347
- // inventory commands') is satisfied by construction once the collection
348
- // reaches at least one inventory source. The runner cannot mechanically
349
- // resolve `agent_has_command_exec == true`, so without this attestation
350
- // the on_fail=halt preflight blocked the canonical `collect runtime | run
351
- // runtime` pipe even with valid evidence — the sibling fs-only cred-stores
352
- // collector attests its own halt precondition the same way. Gated on
353
- // readability so a fully-masked /proc+/etc scope reports it false.
289
+ // The collector execs nothing, so `exec-allowed` holds by construction; the
290
+ // runner cannot resolve `agent_has_command_exec` mechanically and its
291
+ // on_fail=halt preflight would block `collect runtime | run runtime`.
354
292
  "exec-allowed": Boolean(sudoersReadable || passwdContent != null || procWalkable || anyTpReadable),
355
293
  },
356
294
  artifacts,