@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.
- package/CHANGELOG.md +24 -0
- package/dist/9560.index.js +24 -10
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -4
- package/src/capabilities/CLAUDE.md +20 -13
- package/src/capabilities/attack-coverage.js +1 -1
- package/src/capabilities/manifest.js +2 -2
- package/src/capabilities/probes.js +22 -8
- package/src/capabilities/report.js +5 -4
- package/src/capabilities/runner.js +6 -1
- package/src/sandbox/CLAUDE.md +61 -18
- package/src/sandbox/backend-namespace.js +246 -117
- package/src/sandbox/capabilities.js +22 -0
- package/src/sandbox/control-probes.js +91 -7
- package/src/sandbox/limits.js +2 -0
- package/src/sandbox/linux-probes.js +179 -0
- package/src/sandbox/linux-rootfs.js +263 -0
- package/src/sandbox/supervise.js +38 -15
package/src/sandbox/supervise.js
CHANGED
|
@@ -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
|
|
20
|
-
// between two sweeps, with its parent exiting
|
|
21
|
-
// reparented away from the tree and is not found. The
|
|
22
|
-
// window; it does not close it. Only a PID-namespace
|
|
23
|
-
// it, and
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
167
|
-
*
|
|
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
|
|
206
|
+
backend,
|
|
184
207
|
spawnResult: {
|
|
185
208
|
status: r.exitCode,
|
|
186
209
|
stdout: r.stdout,
|
|
187
|
-
stderr:
|
|
210
|
+
stderr: rawStderr,
|
|
188
211
|
error: r.timedOut ? { code: 'ETIMEDOUT' } : r.spawnError ? { code: 'SPAWN', message: r.spawnError } : null,
|
|
189
212
|
},
|
|
190
|
-
unsupported
|
|
213
|
+
unsupported,
|
|
191
214
|
});
|
|
192
215
|
let status = res.status;
|
|
193
216
|
let stderr = res.stderr;
|