@clear-capabilities/agentic-security-scanner 0.165.1 → 0.166.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.
@@ -1 +1 @@
1
- 5a4520f3bff6f1e77c0bb2bfaa0346b11c1447eb3a8433bc1661d2f58c03537a agentic-security.mjs
1
+ 37394bc97ebb4c287b4baa85822494bda80e330e4d55194ad5fb63598bce4eb0 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.165.1",
3
+ "version": "0.166.0",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -68,7 +68,7 @@
68
68
  "test:extras": "AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && node --experimental-test-module-mocks --test test/fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection-k8s-admission.test.js && node --experimental-test-module-mocks --test test/lineage-fault-injection.test.js && npm run test:python",
69
69
  "loop": "node ../scripts/loop-engineering/run.mjs",
70
70
  "test:loop-real-prd": "node --test ../scripts/loop-engineering/test/loop-real-prd.test.js",
71
- "test:loop": "node --test --test-concurrency=1 ../scripts/loop-engineering/test/loop-manifest.test.js ../scripts/loop-engineering/test/loop-watchdog.test.js ../scripts/loop-engineering/test/loop-background.test.js ../scripts/loop-engineering/test/loop-dashboard.test.js ../scripts/loop-engineering/test/loop-evidence.test.js ../scripts/loop-engineering/test/loop-recovery.test.js ../scripts/loop-engineering/test/loop-remote.test.js ../scripts/loop-engineering/test/loop-runbook.test.js ../scripts/loop-engineering/test/loop-assurance-profile.test.js ../scripts/loop-engineering/test/loop-assurance-runtime.test.js ../scripts/loop-engineering/test/loop-bounds.test.js ../scripts/loop-engineering/test/loop-spend.test.js",
71
+ "test:loop": "node --test --test-concurrency=1 ../scripts/loop-engineering/test/loop-manifest.test.js ../scripts/loop-engineering/test/loop-watchdog.test.js ../scripts/loop-engineering/test/loop-background.test.js ../scripts/loop-engineering/test/loop-dashboard.test.js ../scripts/loop-engineering/test/loop-evidence.test.js ../scripts/loop-engineering/test/loop-recovery.test.js ../scripts/loop-engineering/test/loop-remote.test.js ../scripts/loop-engineering/test/loop-runbook.test.js ../scripts/loop-engineering/test/loop-assurance-profile.test.js ../scripts/loop-engineering/test/loop-assurance-runtime.test.js ../scripts/loop-engineering/test/loop-bounds.test.js ../scripts/loop-engineering/test/loop-spend.test.js ../scripts/loop-engineering/test/loop-ci-attest.test.js",
72
72
  "test:python": "node ../scripts/run-python-tests.mjs",
73
73
  "test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
74
74
  "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
@@ -110,7 +110,7 @@
110
110
  "release:closure:check": "node ../scripts/release-closure.mjs --run",
111
111
  "release:closure:static": "node ../scripts/release-closure.mjs --static",
112
112
  "test:documentation": "node --test test/documentation/drift-check.test.js test/documentation/generated-pages.test.js test/documentation/scope-and-examples.test.js test/documentation/policy-cards.test.js test/documentation/operations-and-review.test.js",
113
- "test:capabilities": "node --test test/capabilities/manifest.test.js test/capabilities/filesystem.test.js test/capabilities/exec.test.js test/capabilities/network.test.js test/capabilities/canary.test.js test/capabilities/tools.test.js test/capabilities/recovery.test.js test/capabilities/receipts.test.js test/capabilities/adversarial.test.js test/capabilities/linux-enforcement.test.js",
113
+ "test:capabilities": "node --test test/capabilities/manifest.test.js test/capabilities/filesystem.test.js test/capabilities/exec.test.js test/capabilities/network.test.js test/capabilities/canary.test.js test/capabilities/tools.test.js test/capabilities/recovery.test.js test/capabilities/receipts.test.js test/capabilities/adversarial.test.js test/capabilities/linux-enforcement.test.js test/supervise-async-sweep.test.js",
114
114
  "test:verification": "node --test test/verification/verification-record.test.js test/verification/oracle-adapters.test.js test/verification/replay-manifest.test.js test/verification/patch-negative.test.js test/verification/non-taint-classes.test.js test/verification/interface-equivalence.test.js test/verification/advisory-gating.test.js test/verification/oracle-conformance.test.js",
115
115
  "test:deployment": "node --test test/deployment/boundary-graph.test.js test/deployment/config-adapters.test.js test/deployment/trace-correlation.test.js test/deployment/cross-repo.test.js test/deployment/attack-paths.test.js test/deployment/drift.test.js test/deployment/boundary-presentation.test.js test/deployment/differentiation-ablation.test.js test/deployment/boundaries-cli.test.js",
116
116
  "test:portfolio": "node --test test/portfolio/manifest.test.js test/portfolio/bundle.test.js test/portfolio/signing.test.js test/portfolio/work-units.test.js test/portfolio/resume.test.js test/portfolio/scheduler.test.js test/portfolio/progress.test.js test/portfolio/retention.test.js test/portfolio/backend.test.js",
@@ -87,6 +87,14 @@ is ENOENT or EROFS rather than `denied`; liveness is judged by heartbeat, not ho
87
87
  pid; a declared network destination blocks the task); `sandbox-linux` fails if any
88
88
  of them skips.
89
89
 
90
+ `sandbox-linux` also writes a per-test report (`sandbox-linux-test-report`, artifact of the
91
+ job). The loop controller reads it back, for the exact commit, to accept the six
92
+ Linux-namespace tests that SKIP on a host without the backend
93
+ (`docs/guides/loop-engineering.md`, "Attested tests"). That is evidence from the
94
+ hosted run, reported as its own source (`attested-from-ci: sandbox-linux@<sha>`)
95
+ beside the unchanged local skip count; it is not a claim that the local host
96
+ enforced anything and it does not change `partially-verified`.
97
+
90
98
  ## Known limits (also in `REPORT_LIMITATIONS`)
91
99
 
92
100
  Descendant exec is not allowlisted; ancestors of declared roots expose metadata;
@@ -22,7 +22,7 @@
22
22
  // sweep interval bounds the window; it does not close it. Only a PID-namespace
23
23
  // or cgroup boundary closes it, and macOS has neither. The namespace backend
24
24
  // has the boundary (see `runConfinedSupervised`).
25
- import { spawn, spawnSync } from 'node:child_process';
25
+ import { spawn, execFile } from 'node:child_process';
26
26
  import { detectBackend } from './capabilities.js';
27
27
  import { runDisabled } from './backend-disabled.js';
28
28
  import { buildUserspaceInvocation } from './backend-userspace.js';
@@ -40,12 +40,21 @@ function groupAlive(pgid) {
40
40
  function signalPid(pid, sig) { try { process.kill(pid, sig); } catch { /* already gone */ } }
41
41
  function signalGroup(pgid, sig) { try { process.kill(-pgid, sig); } catch { /* already gone */ } }
42
42
 
43
- /** Descendants of `rootPid` plus members of its group, from one `ps` snapshot. */
43
+ /**
44
+ * Descendants of `rootPid` plus members of its group, from one `ps` snapshot. Asynchronous on purpose: a synchronous `ps` blocks the
45
+ * controller's event loop for as long as the machine takes to run it (tens of milliseconds idle, far more under load), and this runs on
46
+ * every sweep.
47
+ */
44
48
  function snapshotTree(rootPid) {
45
- const r = spawnSync('ps', ['-A', '-o', 'pid=,ppid=,pgid='], { encoding: 'utf8', timeout: 2000 });
46
- if (r.error || r.status !== 0) return [];
49
+ return new Promise((resolve) => {
50
+ execFile('ps', ['-A', '-o', 'pid=,ppid=,pgid='], { encoding: 'utf8', timeout: 2000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
51
+ resolve(err ? [] : treeFromPs(stdout, rootPid));
52
+ });
53
+ });
54
+ }
55
+ function treeFromPs(stdout, rootPid) {
47
56
  const rows = [];
48
- for (const line of String(r.stdout).split('\n')) {
57
+ for (const line of String(stdout).split('\n')) {
49
58
  const m = line.trim().split(/\s+/).map(Number);
50
59
  if (m.length === 3 && m.every(Number.isFinite)) rows.push({ pid: m[0], ppid: m[1], pgid: m[2] });
51
60
  }
@@ -86,27 +95,28 @@ export function superviseSpawn(bin, args, {
86
95
  let finishing = null;
87
96
  let timer = null;
88
97
 
89
- const sweep = () => { if (pgid) for (const p of snapshotTree(pgid)) tracked.add(p); };
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;
98
+ const sweep = async () => { if (pgid) for (const p of await snapshotTree(pgid)) tracked.add(p); };
99
+ // sweepMs <= 0 turns the periodic sweep off: on the namespace backend the kernel ends every member with the supervisor
100
+ // (--kill-child), so the periodic sweep adds nothing there. The cleanup below still takes its own snapshots. A sweep never overlaps the
101
+ // previous one, so a slow `ps` under load cannot pile up work.
102
+ let sweeping = false;
103
+ const sweeper = sweepMs > 0 ? setInterval(() => { if (sweeping) return; sweeping = true; sweep().finally(() => { sweeping = false; }); }, sweepMs) : null;
94
104
 
95
- const survivors = () => {
96
- sweep();
105
+ const survivors = async () => {
106
+ await sweep();
97
107
  return { live: [...tracked].filter(alive), group: groupAlive(pgid) };
98
108
  };
99
109
 
100
110
  async function terminate() {
101
111
  const t = _emptyTermination();
102
- const before = survivors();
112
+ const before = await survivors();
103
113
  if (!before.group && before.live.length === 0) return t;
104
114
  t.signalled = 'SIGTERM';
105
115
  signalGroup(pgid, 'SIGTERM');
106
116
  for (const p of tracked) signalPid(p, 'SIGTERM');
107
117
  const deadline = Date.now() + graceMs;
108
118
  while (Date.now() < deadline) {
109
- const s = survivors();
119
+ const s = await survivors();
110
120
  if (!s.group && s.live.length === 0) return t;
111
121
  await sleep(25);
112
122
  }
@@ -115,13 +125,13 @@ export function superviseSpawn(bin, args, {
115
125
  t.signalled = 'SIGKILL';
116
126
  const hard = Date.now() + 3000;
117
127
  while (Date.now() < hard) {
118
- const s = survivors();
128
+ const s = await survivors();
119
129
  if (!s.group && s.live.length === 0) break;
120
130
  signalGroup(pgid, 'SIGKILL');
121
131
  for (const p of s.live) { signalPid(p, 'SIGKILL'); if (!t.killedPids.includes(p)) t.killedPids.push(p); }
122
132
  await sleep(25);
123
133
  }
124
- const s = survivors();
134
+ const s = await survivors();
125
135
  t.survivors = s.live.length ? s.live : (s.group ? [-pgid] : []);
126
136
  return t;
127
137
  }