@clear-capabilities/agentic-security-scanner 0.163.1 → 0.165.0

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.
@@ -16,15 +16,17 @@
16
16
  // 4. The same cleanup runs on a normal exit, so a backgrounded grandchild
17
17
  // cannot outlive a run that "succeeded".
18
18
  //
19
- // KNOWN GAP (not hidden): a descendant that double-forks and calls setsid
20
- // between two sweeps, with its parent exiting before the next sweep, is
21
- // reparented away from the tree and is not found. The sweep interval bounds the
22
- // window; it does not close it. Only a PID-namespace or cgroup boundary closes
23
- // it, and neither is available on the macOS backend.
19
+ // KNOWN GAP on the userspace backend (not hidden): a descendant that
20
+ // double-forks and calls setsid between two sweeps, with its parent exiting
21
+ // before the next sweep, is reparented away from the tree and is not found. The
22
+ // sweep interval bounds the window; it does not close it. Only a PID-namespace
23
+ // or cgroup boundary closes it, and macOS has neither. The namespace backend
24
+ // has the boundary (see `runConfinedSupervised`).
24
25
  import { spawn, spawnSync } from 'node:child_process';
25
26
  import { detectBackend } from './capabilities.js';
26
27
  import { runDisabled } from './backend-disabled.js';
27
28
  import { buildUserspaceInvocation } from './backend-userspace.js';
29
+ import { buildNamespaceInvocation } from './backend-namespace.js';
28
30
  import { buildResult, errorResult } from './result.js';
29
31
 
30
32
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -85,7 +87,10 @@ export function superviseSpawn(bin, args, {
85
87
  let timer = null;
86
88
 
87
89
  const sweep = () => { if (pgid) for (const p of snapshotTree(pgid)) tracked.add(p); };
88
- const sweeper = setInterval(sweep, sweepMs);
90
+ // sweepMs <= 0 turns the periodic sweep off. Each sweep is a SYNCHRONOUS `ps`, which blocks the controller's event loop for as long as
91
+ // it runs; on the namespace backend the kernel ends every member with the supervisor (--kill-child), so the periodic sweep adds nothing
92
+ // there and only costs responsiveness. The cleanup below still takes its own snapshots.
93
+ const sweeper = sweepMs > 0 ? setInterval(sweep, sweepMs) : null;
89
94
 
90
95
  const survivors = () => {
91
96
  sweep();
@@ -124,7 +129,8 @@ export function superviseSpawn(bin, args, {
124
129
  function finish() {
125
130
  if (finishing) return finishing;
126
131
  finishing = (async () => {
127
- clearInterval(sweeper); clearTimeout(timer);
132
+ if (sweeper) clearInterval(sweeper);
133
+ clearTimeout(timer);
128
134
  const termination = await terminate();
129
135
  try { child.stdout?.destroy(); child.stderr?.destroy(); } catch { /* ignore */ }
130
136
  resolve({ spawnError, exitCode, exitSignal, stdout, stderr, timedOut, cancelled, outputCapped, termination });
@@ -163,31 +169,48 @@ export function superviseSpawn(bin, args, {
163
169
  *
164
170
  * Backend coverage, stated plainly:
165
171
  * userspace supervised (verified by execution on macOS)
166
- * namespace REFUSED: tree termination has not been implemented and executed
167
- * on the kernel-namespace backend, so the run does not happen
172
+ * namespace supervised ONLY where the PID namespace is created with
173
+ * `--kill-child`: killing the unshare process then makes the
174
+ * kernel kill every member of the namespace, however it detached
175
+ * (setsid, double fork). Where the host's unshare lacks the flag
176
+ * the run is refused, since the sweep alone would be the whole
177
+ * guarantee. That it holds on a host is shown by the active probe
178
+ * (`linux-probes.js`), not by this comment.
168
179
  * disabled refused, as always
169
180
  */
170
181
  export async function runConfinedSupervised(argv, opts = {}) {
171
182
  const backend = detectBackend({ force: opts.force });
172
183
  if (backend === 'disabled') return runDisabled(argv, opts);
173
- if (backend !== 'userspace') {
184
+ if (backend !== 'userspace' && backend !== 'namespace') {
174
185
  return errorResult(backend, `process-tree termination is not implemented or verified on the ${backend} backend; refusing to execute`);
175
186
  }
176
- const inv = buildUserspaceInvocation(argv, opts);
187
+ const inv = backend === 'userspace' ? buildUserspaceInvocation(argv, opts) : buildNamespaceInvocation(argv, opts, opts.deps);
177
188
  if (inv.error) return inv.error;
189
+ if (backend === 'namespace' && !inv.treeKill) {
190
+ inv.dispose();
191
+ return errorResult(backend, 'process-tree termination is not implemented or verified on the namespace backend (this host cannot make the kernel kill the namespace with its supervisor); refusing to execute');
192
+ }
178
193
  const r = await superviseSpawn(inv.bin, inv.args, {
179
194
  cwd: inv.cwd, env: inv.env, timeoutMs: opts.timeoutMs ?? 10000, graceMs: opts.graceMs ?? 1000,
180
- signal: opts.signal, maxOutputBytes: opts.maxOutputBytes,
195
+ signal: opts.signal, maxOutputBytes: opts.maxOutputBytes, ...(backend === 'namespace' ? { sweepMs: 0 } : {}),
181
196
  });
197
+ let rawStderr = r.stderr;
198
+ let unsupported = inv.unsupported;
199
+ if (backend === 'namespace') {
200
+ const fin = inv.finish(r.stderr, { hadError: !!r.spawnError || r.timedOut });
201
+ if (fin.error) return fin.error;
202
+ rawStderr = fin.stderr;
203
+ unsupported = fin.unsupported;
204
+ }
182
205
  const res = buildResult({
183
- backend: 'userspace',
206
+ backend,
184
207
  spawnResult: {
185
208
  status: r.exitCode,
186
209
  stdout: r.stdout,
187
- stderr: r.stderr,
210
+ stderr: rawStderr,
188
211
  error: r.timedOut ? { code: 'ETIMEDOUT' } : r.spawnError ? { code: 'SPAWN', message: r.spawnError } : null,
189
212
  },
190
- unsupported: inv.unsupported,
213
+ unsupported,
191
214
  });
192
215
  let status = res.status;
193
216
  let stderr = res.stderr;