@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/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,30 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.165.0 - Linux process-count cap proved on a hosted runner; the execution suites run on Linux
|
|
13
|
+
|
|
14
|
+
**The process-count cap works on Linux and is proved.** The cap was never applied before because the resource prelude ran under `dash`, which has no `ulimit -u`. The namespace backend now applies it with `prlimit --nproc` to the task's command only, after the confinement is built. On the hosted x86_64 `sandbox-linux` job, six concurrent children all start under a cap of 512 and only one starts under a cap of 3, in default mode and in capability mode. The probe is paired with fault injection: it fails against a runner that drops the cap and against one that caps everything. A requested cap is never silently dropped; a host without `prlimit` reports it unsupported. `process-cap` is verified for Linux only after that hosted result, and stays unverified on macOS.
|
|
15
|
+
|
|
16
|
+
**The execution suites now run on Linux with Linux semantics, with no skips.** The capabilities, trust-boundary and attack-corpus suites assert ENOENT or EROFS evidence for out-of-root access instead of macOS-style denial, use heartbeats instead of host pids for liveness, and expect a declared network destination to block the task. No assertion was weakened. A skip in the trust-boundary suite is gone. The hosted `sandbox-linux` job now also runs the capabilities, verification, invariants, documentation, trust-boundary and evidence-issuer suites and fails on any skip: 737 tests ran and passed, 0 skipped, 0 failed.
|
|
17
|
+
|
|
18
|
+
**A responsiveness defect found by the hosted run.** The supervisor's periodic synchronous `ps` sweep kept the controller's event loop 58 to 62 percent busy on Linux. It is off for the namespace backend, where the kernel already kills the namespace with its supervisor.
|
|
19
|
+
|
|
20
|
+
**Still open.** The hosted release leg still runs only the static closure: running the full closure there has never been timed on a hosted runner and would duplicate the test shards. Mediated network remains unsupported on Linux. The process-count behaviour was checked on kernel 6.8 only; older kernels count the limit per real uid system-wide, so a small cap could behave differently. The real-code accuracy gates and the routing promotion gate still need independently adjudicated data that code cannot produce, so the PRD closure verification still reports not complete.
|
|
21
|
+
|
|
22
|
+
## 0.164.0 - Linux isolation proved on a hosted runner, six controller limits enforced, closure evidence from CI
|
|
23
|
+
|
|
24
|
+
Closing gaps from the Differentiation PRD that software can close. The gaps that remain are named at the end; none of them is hidden by this release.
|
|
25
|
+
|
|
26
|
+
**Linux isolation controls, proved on a real hosted Linux runner.** The namespace backend now implements capability mode: a tmpfs root holding only the runtime baseline and the declared roots (read roots read-only, write roots read-write, protected paths absent or masked), entered with `pivot_root`, inside a PID namespace created with `--kill-child` so that when the supervisor or init dies every descendant dies, and an empty network namespace for tasks that declare no network. The hosted `sandbox-linux` job (x86_64 `ubuntu-latest`) now fails loudly if the namespace suites skip or any required control is not proved, and proved all of these with active probes, each paired with a positive control and a fault-injection counterpart that must fail against a deliberately wrong runner: write confinement, read denial, environment scrub, network blocking, file-size limit, filesystem read confinement (direct, through a symlink, through `..`, and from a descendant), multi-root write, PID-namespace isolation, tree termination across four process shapes including a double-forked `setsid` member (which the macOS backend cannot stop), reaping after a SIGKILL of the supervisor and of the init, masking of protected files, and network in capability mode. Linux is recorded as `partially-verified` on exactly that evidence. **Not proved and stated as such:** mediated network (unsupported on Linux: a task that declares a destination is blocked, never allowed), process-count caps, the full attack corpus, and the oracle adapters' Linux status; the capabilities execution suites still skip on Linux because they encode macOS semantics.
|
|
27
|
+
|
|
28
|
+
**The process-cap mystery is explained.** The Linux resource prelude runs under `dash`, whose `ulimit` has no `-u` (`ulimit: Illegal option -u` is in the hosted log), so a process-count cap was never applied on Linux, which is why a cap of 1 did not refuse in 0.162.0. The cap stays unasserted until a replacement is proved.
|
|
29
|
+
|
|
30
|
+
**Six controller limits that were listed as unenforced are enforced**, each removed from the profile's `unenforced` list only after a boundary test passed in both directions: subprocess wall time (a process-group runner with TERM, a grace period, KILL, then a sweep, so a hung child cannot outlive its bound), network request time and retries with a capped backoff, a separately metered provider and infrastructure spend envelope (off unless a person preauthorizes it), an upper-bound reserve for unknown billing (never zero), per-class suite ceilings, and per-child leases inside each suite wrapper (a hung test file is named and killed while later files still run). Two entries remain: Linux enforcement of the controller's own limits, and the documentation-only note that budgets are caps, not authorization.
|
|
31
|
+
|
|
32
|
+
**Closure evidence from CI.** `scripts/release-closure.mjs --attest-from-ci` reads the hosted check runs for the exact commit and attests the two remote prerequisites (the Haskell toolchain job and both NixOS runtime legs) only when each job ran for that commit and succeeded, with no failed step beyond those the plan tolerates. Anything else (wrong commit, in progress, skipped, cancelled, partial matrix, `gh` unavailable) produces no attestation and a typed reason. Checked against real GitHub: attested for the 0.163.1 commit, refused for a commit that never ran CI and for a malformed sha. `ship.mjs` prints the result as information after a release; it never gates or undoes one.
|
|
33
|
+
|
|
34
|
+
**Still open, and why.** The PRD's real-code accuracy gates (per-language F1 of at least 0.80 on sealed, independently adjudicated data with at least 100 positives per language) and the routing promotion gate (at least 200 paired adjudicated tasks) cannot be met because no such population exists and independent human adjudication cannot be produced by code; they read `unmeasured` and every figure this programme has produced on its own fixtures is synthetic and labelled so. The PRD's closure verification therefore still reports not complete.
|
|
35
|
+
|
|
12
36
|
## 0.163.1 - a regular expression's `.exec()` on request data is no longer reported as command injection
|
|
13
37
|
|
|
14
38
|
The taint engine treated any call named `exec` as a command-execution sink regardless of its receiver, so `SEMVER.exec(req.query.v)` or `/^(\d+)/.exec(req.query.w)` produced a high-severity CWE-78 finding. Regular-expression matching on request data is everywhere in JavaScript, so this was a high-volume false positive (disclosed as a known issue in 0.163.0, present since at least 0.161.0).
|
package/dist/9560.index.js
CHANGED
|
@@ -373,8 +373,8 @@ const MANIFEST_REQUIRED = ['schema', 'schemaVersion', 'taskId', 'repository', 'p
|
|
|
373
373
|
|
|
374
374
|
// Resource limits a manifest can carry. The first three come straight from the
|
|
375
375
|
// assurance config (same ranges), the file-size cap is enforced by the sandbox
|
|
376
|
-
// prelude,
|
|
377
|
-
//
|
|
376
|
+
// prelude, the process-count cap is enforced on the Linux namespace backend only (prlimit, proved by an
|
|
377
|
+
// active probe), and the memory cap is CARRIED but never claimed enforced.
|
|
378
378
|
const RESOURCE_RANGES = Object.freeze({
|
|
379
379
|
timeoutMs: { min: config/* LIMITS */.b1.timeoutMs.min, max: config/* LIMITS */.b1.timeoutMs.max },
|
|
380
380
|
maxOutputBytes: { min: config/* LIMITS */.b1.maxOutputBytes.min, max: config/* LIMITS */.b1.maxOutputBytes.max },
|
|
@@ -1372,15 +1372,18 @@ function mk(prefix) { return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(
|
|
|
1372
1372
|
function rm(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ } }
|
|
1373
1373
|
const token = (p) => `${p}-${crypto.randomBytes(8).toString('hex')}`;
|
|
1374
1374
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1375
|
+
// `run` is a seam so a test can hand the probe a runner that is deliberately
|
|
1376
|
+
// wrong (a read root left too wide, a root left writable) and see the probe say
|
|
1377
|
+
// so: a probe that cannot fail proves nothing.
|
|
1378
|
+
async function probeFsRead(backend, runner = runConfinedSupervised) {
|
|
1379
|
+
if (backend !== 'userspace' && backend !== 'namespace') return unsupported(`read confinement to declared roots is not implemented on the ${backend} backend`);
|
|
1377
1380
|
const root = mk('agsec-cap-r-'); const ro = mk('agsec-cap-ro-'); const outside = mk('agsec-cap-out-');
|
|
1378
1381
|
try {
|
|
1379
1382
|
const okTok = token('READABLE'); const secTok = token('SEALED');
|
|
1380
1383
|
fs.writeFileSync(path.join(ro, 'r.txt'), okTok);
|
|
1381
1384
|
fs.writeFileSync(path.join(outside, 's.txt'), secTok);
|
|
1382
1385
|
fs.symlinkSync(path.join(outside, 's.txt'), path.join(ro, 'link'));
|
|
1383
|
-
const run = (script, readRoots) =>
|
|
1386
|
+
const run = (script, readRoots) => runner(['/bin/sh', '-c', script], { root, readRoots, timeoutMs: 8000, graceMs: 300 });
|
|
1384
1387
|
const pos = await run(`cat '${ro}/r.txt'`, [ro]);
|
|
1385
1388
|
if (!pos.stdout.includes(okTok)) return notProved(`positive control failed: a file inside a declared root was not readable (${pos.status})`);
|
|
1386
1389
|
const open = await run(`cat '${outside}/s.txt'`, [ro, outside]);
|
|
@@ -1399,13 +1402,13 @@ async function probeFsRead(backend) {
|
|
|
1399
1402
|
} finally { rm(root); rm(ro); rm(outside); }
|
|
1400
1403
|
}
|
|
1401
1404
|
|
|
1402
|
-
async function probeMultiWrite(backend) {
|
|
1403
|
-
if (backend !== 'userspace') return unsupported(`several declared write roots are not implemented on the ${backend} backend`);
|
|
1405
|
+
async function probeMultiWrite(backend, runner = runConfinedSupervised) {
|
|
1406
|
+
if (backend !== 'userspace' && backend !== 'namespace') return unsupported(`several declared write roots are not implemented on the ${backend} backend`);
|
|
1404
1407
|
const root = mk('agsec-cap-w-'); const w1 = mk('agsec-cap-w1-'); const w2 = mk('agsec-cap-w2-');
|
|
1405
1408
|
const ro = mk('agsec-cap-wro-'); const outside = mk('agsec-cap-wout-');
|
|
1406
1409
|
try {
|
|
1407
1410
|
const script = [`echo a > '${w1}/f'`, `echo b > '${w2}/f'`, `echo c > '${ro}/f'`, `echo d > '${outside}/f'`, 'true'].join('; ');
|
|
1408
|
-
await
|
|
1411
|
+
await runner(['/bin/sh', '-c', script], { root, readRoots: [ro], writeRoots: [w1, w2], timeoutMs: 8000, graceMs: 300 });
|
|
1409
1412
|
if (!fs.existsSync(path.join(w1, 'f')) || !fs.existsSync(path.join(w2, 'f'))) return notProved('positive control failed: a declared write root was not writable');
|
|
1410
1413
|
if (fs.existsSync(path.join(ro, 'f'))) return notProved('a root declared read-only was writable');
|
|
1411
1414
|
if (fs.existsSync(path.join(outside, 'f'))) return notProved('an undeclared directory was writable');
|
|
@@ -1521,8 +1524,19 @@ function isAdvertisedBackend(backend, platform = process.platform) {
|
|
|
1521
1524
|
function platformStatements() {
|
|
1522
1525
|
return Object.freeze({
|
|
1523
1526
|
linux: {
|
|
1524
|
-
backend: 'namespace', status: '
|
|
1525
|
-
|
|
1527
|
+
backend: 'namespace', status: 'partially-verified',
|
|
1528
|
+
// Evidence is the hosted sandbox-linux CI job (ubuntu-latest, x86_64), whose active probes
|
|
1529
|
+
// (attack plus positive control, with fault-injection counterparts) proved these controls.
|
|
1530
|
+
// Anything not listed here is not claimed: a verified control is verified on that runner image
|
|
1531
|
+
// and kernel, not on every Linux host, which is why the job runs on every push.
|
|
1532
|
+
evidence: 'sandbox-linux CI job (hosted ubuntu-latest, x86_64); scripts/sandbox-linux-verify.mjs',
|
|
1533
|
+
verifiedControls: Object.freeze([
|
|
1534
|
+
'write-confinement', 'read-denial', 'env-scrub', 'network', 'tree-termination', 'file-size-limit',
|
|
1535
|
+
'fs-read-confinement', 'fs-multi-root-write', 'process-cap',
|
|
1536
|
+
]),
|
|
1537
|
+
unsupportedControls: Object.freeze(['network-mediation']),
|
|
1538
|
+
unassertedControls: Object.freeze([]),
|
|
1539
|
+
note: 'advertised for enforced mode. Proved by active probes on the hosted Linux runner (not on every Linux host): write-confinement, read-denial, env-scrub, network, tree-termination, file-size-limit, fs-read-confinement, fs-multi-root-write, process-cap (a per-user-namespace process-count cap applied with prlimit after the confinement is built; the shell ulimit it replaces does not exist in dash). Mediated network is NOT implemented on the namespace backend (an empty network namespace has no path to a proxy), so a task that declares a network destination is blocked on Linux, never allowed. Process-count caps are claimed on Linux only, and only as that probe shows them; memory caps are not enforced. The attack corpus is not run in the sandbox-linux job, so corpus coverage stays unverified on Linux.',
|
|
1526
1540
|
},
|
|
1527
1541
|
darwin: {
|
|
1528
1542
|
backend: 'userspace', status: 'host-proved-not-advertised',
|