@clear-capabilities/agentic-security-scanner 0.166.0 → 0.168.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/bin/agentic-security.js +40 -4
  3. package/dist/2199.index.js +348 -0
  4. package/dist/3736.index.js +2 -1
  5. package/dist/3740.index.js +722 -0
  6. package/dist/4113.index.js +1 -1
  7. package/dist/4142.index.js +929 -0
  8. package/dist/5738.index.js +240 -0
  9. package/dist/6932.index.js +1190 -0
  10. package/dist/7838.index.js +1 -1
  11. package/dist/808.index.js +1330 -0
  12. package/dist/818.index.js +93 -0
  13. package/dist/9800.index.js +245 -0
  14. package/dist/9999.index.js +2 -2
  15. package/dist/agentic-security.mjs +2 -2
  16. package/dist/agentic-security.mjs.sha256 +1 -1
  17. package/package.json +2 -2
  18. package/src/posture/artifact-registry.js +6 -0
  19. package/src/posture/fleet/cli.js +103 -0
  20. package/src/posture/fleet/fleet-graph.js +142 -0
  21. package/src/posture/fleet/fleet-manifest.js +77 -0
  22. package/src/posture/proof-fabric/cli.js +115 -0
  23. package/src/posture/proof-fabric/fix-proof.js +73 -0
  24. package/src/posture/proof-fabric/oracles.js +101 -0
  25. package/src/posture/proof-fabric/orchestrator.js +129 -0
  26. package/src/posture/proof-fabric/reproducer.js +108 -0
  27. package/src/posture/proof-fabric/runner.js +243 -0
  28. package/src/posture/proof-fabric/states.js +53 -0
  29. package/src/posture/proof-fabric/store.js +83 -0
  30. package/src/posture/rule-compiler/cli.js +141 -0
  31. package/src/posture/rule-compiler/corpus.js +61 -0
  32. package/src/posture/rule-compiler/eligibility.js +25 -0
  33. package/src/posture/rule-compiler/ir.js +87 -0
  34. package/src/posture/rule-compiler/lifecycle.js +57 -0
  35. package/src/posture/rule-compiler/mutation.js +60 -0
  36. package/src/posture/rule-compiler/promotion.js +47 -0
  37. package/src/posture/rule-compiler/store.js +96 -0
  38. package/src/sandbox/backend-userspace.js +7 -1
  39. package/src/sandbox/trust-boundary.js +2 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,97 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.168.0 - fleet: multi-repository scan orchestration and cross-repository dependency blast radius
13
+
14
+ A real, honestly-scoped slice of the organization-wide security graph recommendation: a new CLI command group,
15
+ `fleet init|scan|status|resume|query`, built almost entirely on existing machinery rather than a new engine.
16
+
17
+ **The manifest and the scan.** `fleet.yaml` declares an organization and a list of `{id, path}` repositories
18
+ (plus `workers` and `perRepoTimeout`), validated closed-world — an unknown key, a duplicate id, a non-kebab id or
19
+ an unresolvable path refuses the whole manifest. `fleet scan fleet.yaml` drives the EXISTING `posture/fleet.js`
20
+ (`runFleet`) over that list: bounded concurrency, a resumable on-disk checkpoint, per-repository isolation (one
21
+ repo exploding never takes the run down), and an honest rollup (a failed repo is reported failed, never folded
22
+ into "0 findings"). No new scanning engine was written for this.
23
+
24
+ **The new part: a cross-repository dependency graph.** Built purely from each repository's own package manifest
25
+ (`package.json`/`go.mod`/`Cargo.toml`/`requirements.txt`/`pyproject.toml` — name plus dependencies), with no
26
+ deployment configuration required, so a fleet of plain application repositories works immediately. `fleet query
27
+ --depends-on <repo-id>` answers "which repositories depend on this one, directly or transitively, and through
28
+ what path" — a real question about organization-wide blast radius, not a guess.
29
+
30
+ **A real, grounded demonstration, not an asserted-away fixture.** A four-repository synthetic organization
31
+ (frontend, payments-api, shared-auth-lib, payments-worker) where only `shared-auth-lib` carries an actual
32
+ command-injection vulnerability the deterministic engine genuinely finds (the same confirmed-detected shape —
33
+ `req.body.host` into `child_process.exec` — already used in this repository's own fixtures). The blast-radius
34
+ query correctly reports `frontend` and `payments-api` as direct dependents and `payments-worker` as a transitive
35
+ one, through `payments-api`, at the right depth, with the right path.
36
+
37
+ **A bug this process itself caught, twice.** A first draft of the fleet CLI wrapped `runScan()`'s own
38
+ `{ scan, meta }` return value in a SECOND `{ scan }` object; `runFleet`'s internal summarizer reads fields
39
+ defensively rather than throwing on the wrong shape, so every repository silently reported zero findings with no
40
+ error of any kind. Found by running the CLI against the fixture and noticing the finding count was wrong, not by
41
+ the automated test — whose own fixture at the time also happened to use a vulnerability shape the deterministic
42
+ engine does not actually flag (an unlabeled function parameter reaching `exec`, no recognized untrusted source).
43
+ Both are fixed, and the fixture now uses the same confirmed-detected shape the test asserts against.
44
+
45
+ **Still open, and disclosed as such** (`docs/implementation-status/fleet.md`): no Internet-facing/regulated-data
46
+ overlay — the fuller "which Internet-facing app can reach PCI data through this component" question needs the
47
+ existing deployment-boundary graph's trust-zone model layered on top of this dependency graph, not attempted
48
+ here; no natural-language query parser (`--depends-on <repo-id>` is the one supported shape); no SQLite-backed
49
+ storage (reuses the existing flat-JSON fleet checkpoint; `posture/portfolio/backend.js`'s existing storage
50
+ abstraction is the natural place for a SQLite implementation later); no persisted job queue with lease/retry
51
+ (reuses in-process `runFleet` rather than the heavier, operator-only `portfolio/work-units.js` scheduler); matching
52
+ across repositories is by declared package name only, with no ecosystem disambiguation.
53
+
54
+ ## 0.167.0 - execution-backed exploit proof, and a self-learning rule compiler that turns one confirmed finding into a deterministic detection rule
55
+
56
+ Two new, independently opt-in capabilities, each off by default until explicitly invoked.
57
+
58
+ **Execution-backed exploit proof (`triage exploit --prove`, `fix --require-proof`).** A new evidence-state ladder
59
+ (`CANDIDATE` → `STATICALLY_CORROBORATED` → `REPRODUCER_GENERATED` → `REPRODUCED` → `EXPLOIT_CONFIRMED` →
60
+ `FIX_REPLAY_BLOCKED` → `FIX_VERIFIED`), separate from severity and confidence, reached only by executing a bounded,
61
+ serialized reproducer inside the existing confined sandbox (`src/sandbox/`) and judging the result with one of nine
62
+ deterministic oracles — never a model. `EXPLOIT_CONFIRMED` requires 3 of 3 reproductions (configurable) plus an
63
+ independent re-run in a fresh environment that receives only the reproducer, the finding id, the oracle and the
64
+ revision, never the reasoning that found it. `fix --apply --require-proof` extends the existing fix-verify
65
+ vocabulary (never a competing definition of "verified"): it refuses to write a patch unless the original exploit is
66
+ confirmed beforehand and the SAME reproducer is demonstrably blocked, in every attempt, afterward. New CLI: `proof
67
+ show|replay|doctor`, `triage exploit <id> --prove`, `triage list --evidence`. Eight fixture classes (SQL injection,
68
+ command injection, path traversal, SSRF, auth bypass, insecure deserialization, unsafe file write, and a real
69
+ AddressSanitizer-backed buffer overflow in C) are tested both directions: vulnerable reaches `EXPLOIT_CONFIRMED`,
70
+ mitigated never does. Guide: `docs/guides/exploit-proof-fabric.md`.
71
+
72
+ Building the fixture corpus surfaced two real, pre-existing defects, each fixed with its own regression test: the
73
+ default (non-capability) confined sandbox profile had no write allowance for `/dev/null`, so any confined command
74
+ redirecting output there (`stdio: 'ignore'`, a common pattern) failed to spawn at all with a silent `EPERM`; and
75
+ the proof runner's own loopback callback listener read the wrong `process.argv` index for a `node -e` invocation,
76
+ so every callback request crashed it (observed by the client as `ECONNRESET`).
77
+
78
+ **Self-learning finding-to-rule compiler (`rules synthesize|evaluate|promote|status|rollback`).** Turns one
79
+ confirmed finding into a reviewed, promoted, deterministic custom rule — compiled into the EXISTING pattern-rule
80
+ engine (`posture/custom-rules.js`), never a second matcher. A candidate rule (its pattern supplied by a model or a
81
+ human; nothing here calls one) must clear a promotion gate — precision ≥95%, recall ≥80%, zero protected-negative
82
+ failures, by default — against an automatically generated corpus that includes deterministic, semantics-preserving
83
+ structural mutations (variable rename, wrap-in-function, indirection) of both the vulnerable and the patched code,
84
+ before an append-only, hash-chained lifecycle ledger lets it advance from `CANDIDATE` to `VALIDATED` to `PROMOTED`.
85
+ A promoted rule is published to the existing `.agentic-security/rules/` directory, so it is live on the very next
86
+ scan with no code change, and gets regression fixtures in the project's own existing `vulnerable`/`clean`
87
+ convention. Demonstrated end to end on a real, grounded miss: the deterministic engine does not recognize
88
+ `db.rawQuery(...)` string concatenation as a SQL sink (verified by a real scan); after promotion, the rule catches
89
+ not just the original finding but an unseen structural sibling — different file, renamed identifiers, extra
90
+ indirection — with zero model calls. Guide: `docs/guides/rule-compiler.md`.
91
+
92
+ A duplicate command name was caught before shipping: the PRD's own example CLI shape collided with the pre-existing
93
+ `rules validate`/`rules lock` (rule-pack override linting and lockfile writing). A first draft added a second,
94
+ competing `case 'rules':` switch arm, which JavaScript resolves to the first match — silently making the original
95
+ two subcommands permanently unreachable. Fixed by folding the new subcommands into the existing dispatcher instead.
96
+
97
+ **Still open.** Neither capability has a model-driven proposal step (both expect the candidate pattern/reproducer
98
+ supplied by the caller); container execution for the proof fabric is detected but not implemented, so execution
99
+ always runs in the existing lower-assurance local sandbox and says so; the rule compiler implements three of the
100
+ PRD's seven listed mutator kinds. Full detail and honest remaining gaps in
101
+ `docs/implementation-status/{exploit-proof-fabric,rule-compiler}.md`.
102
+
12
103
  ## 0.166.0 - the closure accepts hosted Linux test evidence for tests that skip on a non-Linux host; the shared-portfolio drain test is deterministic
13
104
 
14
105
  **Hosted Linux evidence for skipped tests, kept separate and labelled.** On a macOS host the capabilities suite skips six Linux-namespace tests, and a skipped required test counts as failed, so requirements X-501 to X-508 could never verify there even though the same tests run and pass on the hosted `sandbox-linux` job (which fails on any skip). The controller's verification can now count a locally skipped test only when a commit-bound attestation shows that the `sandbox-linux` check run for the exact commit succeeded and that the named test passed in that job's per-test report (uploaded as the `sandbox-linux-test-report` artifact, bound to the commit, the run and a digest). Anything else (wrong or never-pushed commit, job missing, in progress, failed, cancelled or skipped, test absent or skipped in the report, malformed or forged report, `gh` unavailable, a dirty tree) produces a typed refusal and the skip stays open. Local results are never rewritten: the local skip count is unchanged, covered tests are reported as a separate source (`attested-from-ci: sandbox-linux@<sha>`), and a local failure or abnormal exit never triggers a lookup. It is opt-in per profile (`attestFromCi`, capabilities suite only) and `LOOP_ATTEST_FROM_CI=0` turns it off. This is evidence from a hosted run, not a claim that the local host enforced anything; Linux stays `partially-verified`. The new test file has 60 tests, including every refusal and forged-report cases, and 12 hand-made mutants of the new code (nine caught; two proved equivalent; the remaining three guards are only caught together). A live read against the real hosted run covered all six tests and refused an invented one. Writing the reader exposed one real defect: on pull-request runs the job's commit is the merge ref, so the report is now bound to the pull request head and records the merge ref actually tested as `testedSha`.
@@ -1528,6 +1528,7 @@ async function cmdProfile(args) {
1528
1528
  // /triage list | assign | transition | trend
1529
1529
  async function cmdTriage(args) {
1530
1530
  const target = path.resolve(args._[args._.length - 1] && !args._[args._.length - 1].startsWith('--') ? args._[args._.length - 1] : '.');
1531
+ const sub = args._[1];
1531
1532
  // ollama-offline-prd.md §34 — `triage --explain <finding-id>` is read-only
1532
1533
  // narrative generation, not triage's own assign/transition/lock state
1533
1534
  // machine, so it doesn't need the pro-tier gate below (and is checked
@@ -1599,12 +1600,17 @@ async function cmdTriage(args) {
1599
1600
  }
1600
1601
  return 0;
1601
1602
  }
1603
+ if (sub === 'exploit' || (sub === 'validate' && args.flags.execution)) {
1604
+ if (sub === 'exploit' && !args.flags.prove) { console.error('triage exploit requires --prove (it executes a reproducer in an isolated environment).'); return 4; }
1605
+ const last = args._[args._.length - 1];
1606
+ const exploitTarget = path.resolve(args.flags.root || (last && !String(last).startsWith('--') && last !== args._[1] && last !== args._[2] ? last : '.'));
1607
+ return (await import('../src/posture/proof-fabric/cli.js')).cmdProve(args, exploitTarget);
1608
+ }
1602
1609
  const profile = loadProfile(target);
1603
1610
  if (profile.profile !== 'pro') {
1604
1611
  console.error('Triage is a pro-mode feature. Run `agentic-security profile set pro` to enable.');
1605
1612
  return 4;
1606
1613
  }
1607
- const sub = args._[1];
1608
1614
  // Sync first so list reflects the latest scan.
1609
1615
  const lastScanPath = statePath(target, 'findings.json');
1610
1616
  if (fs.existsSync(lastScanPath)) {
@@ -1618,10 +1624,16 @@ async function cmdTriage(args) {
1618
1624
  if (args.flags.assignee) filter.assignee = args.flags.assignee;
1619
1625
  if (args.flags.unassigned) filter.unassigned = true;
1620
1626
  const items = triage.list(target, filter);
1621
- const hdr = ['ID', 'Severity', 'State', 'Assignee', 'File:Line', 'Vuln'].join(' ');
1627
+ // --evidence (PRD 1): an optional, read-only column naming the latest stored exploit-proof evidence state for a
1628
+ // finding, when one exists. Decoupled from triage's own open/fixed state machine on purpose — proof evidence and
1629
+ // triage workflow state answer different questions and must not be conflated.
1630
+ const showEvidence = !!args.flags.evidence;
1631
+ const latestProofFn = showEvidence ? (await import('../src/posture/proof-fabric/store.js')).latestProof : null;
1632
+ const hdr = ['ID', 'Severity', 'State', 'Assignee', 'File:Line', 'Vuln', ...(showEvidence ? ['Evidence'] : [])].join(' ');
1622
1633
  console.log(hdr);
1623
1634
  console.log('─'.repeat(80));
1624
1635
  for (const t of items.slice(0, 50)) {
1636
+ const evidence = showEvidence ? (latestProofFn(target, t.id)?.record?.state ?? 'none') : null;
1625
1637
  console.log([
1626
1638
  t.id.slice(0, 16),
1627
1639
  (t.severity || '').padEnd(8),
@@ -1629,6 +1641,7 @@ async function cmdTriage(args) {
1629
1641
  (t.assignee || '—').padEnd(20),
1630
1642
  `${t.file}:${t.line}`.padEnd(40),
1631
1643
  t.vuln,
1644
+ ...(showEvidence ? [evidence] : []),
1632
1645
  ].join(' '));
1633
1646
  }
1634
1647
  return 0;
@@ -1740,8 +1753,14 @@ async function cmdOrgScan(args) {
1740
1753
 
1741
1754
  // /rules validate | rules lock
1742
1755
  async function cmdRules(args) {
1743
- const target = path.resolve(args._[2] || '.');
1744
1756
  const sub = args._[1];
1757
+ // Self-learning finding-to-rule compiler (PRD 3): synthesize/evaluate/promote/status/rollback a DETECTION rule
1758
+ // from one confirmed finding — distinct from validate/lock below (rules.yml override linting and pack locking).
1759
+ // Dispatches to its own module rather than inlining here, same convention as `triage exploit`/`proof`.
1760
+ if (sub === 'synthesize' || sub === 'evaluate' || sub === 'promote' || sub === 'status' || sub === 'rollback') {
1761
+ return (await import('../src/posture/rule-compiler/cli.js')).cmdRules(args, path.resolve(args.flags.root || '.'));
1762
+ }
1763
+ const target = path.resolve(args._[2] || '.');
1745
1764
  if (sub === 'validate') {
1746
1765
  const r = validateOverrides(target);
1747
1766
  if (r.ok) { console.log('✓ rules.yml is valid'); return 0; }
@@ -1755,7 +1774,7 @@ async function cmdRules(args) {
1755
1774
  console.log(` scanner: ${lock.scannerVersion} rulePackHash: ${lock.rulePackHash}`);
1756
1775
  return 0;
1757
1776
  }
1758
- console.error('rules validate | rules lock'); return 4;
1777
+ console.error('rules validate | rules lock | rules synthesize <finding-id> --spec <file.json> | rules evaluate|promote|status|rollback <rule-id>'); return 4;
1759
1778
  }
1760
1779
 
1761
1780
  // `agentic-security secure [--launch]` — smart router. One command picks the
@@ -3617,6 +3636,21 @@ async function cmdFix(args) {
3617
3636
  const fixMeta = (approvedBy || approvalReason || patchAuthor)
3618
3637
  ? { approval: { approvedBy: approvedBy || '', reason: approvalReason || '' }, ...(patchAuthor ? { author: patchAuthor } : {}) }
3619
3638
  : null;
3639
+ // --require-proof: the patch is applied ONLY if the original exploit is confirmed beforehand and demonstrably blocked
3640
+ // afterwards (posture/proof-fabric/fix-proof.js). Missing reproducer, unavailable environment or an incomplete replay all refuse.
3641
+ if (args.flags['require-proof']) {
3642
+ const { loadReproducerFor } = await import('../src/posture/proof-fabric/cli.js');
3643
+ const { verifyFixWithProof, formatFixProof } = await import('../src/posture/proof-fabric/fix-proof.js');
3644
+ const { verifyFix } = await import('../src/posture/fix-verify.js');
3645
+ const loaded = loadReproducerFor(scanRoot, f.id, args.flags.reproducer);
3646
+ if (!loaded) { console.error(`--require-proof: no reproducer for ${f.id}. Refusing to apply; nothing was executed. Supply --reproducer <file>.`); return 4; }
3647
+ const files = { [f.file]: newContent };
3648
+ const staticVerification = await verifyFix({ scanRoot, originalFindingStableId: f.stableId || null, files, recordMetrics: false });
3649
+ const siblings = (last.findings || []).filter((x) => x.id !== f.id && x.file !== f.file && (x.vuln === f.vuln || x.cwe === f.cwe)).map((x) => ({ id: x.id, file: x.file, line: x.line }));
3650
+ const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings });
3651
+ console.log(formatFixProof(fp));
3652
+ if (!fp.verified) { console.error('\nRefusing to apply: the fix is not proof-verified (--require-proof).'); return 4; }
3653
+ }
3620
3654
  let result = await applyVerifiedFix({
3621
3655
  scanRoot,
3622
3656
  finding: { file: f.file, id: f.id, stableId: f.stableId || null, ruleId: f.cwe || f.title, vuln: f.vuln || f.title },
@@ -7077,9 +7111,11 @@ async function main() {
7077
7111
  case 'undo': process.exit(await cmdUndo(args));
7078
7112
  case 'accept': process.exit(await cmdAccept(args));
7079
7113
  case 'profile': process.exit(await cmdProfile(args));
7114
+ case 'proof': process.exit(await (await import('../src/posture/proof-fabric/cli.js')).cmdProof(args, path.resolve(args.flags.root || '.')));
7080
7115
  case 'triage': process.exit(await cmdTriage(args));
7081
7116
  case 'org-scan': process.exit(await cmdOrgScan(args));
7082
7117
  case 'rules': process.exit(await cmdRules(args));
7118
+ case 'fleet': process.exit(await (await import('../src/posture/fleet/cli.js')).cmdFleet(args, path.resolve(args.flags.root || '.')));
7083
7119
  case 'rule': process.exit(await cmdRule(args));
7084
7120
  case 'tickets': process.exit(await cmdTickets(args));
7085
7121
  case 'secure': process.exit(await cmdSecure(args));
@@ -0,0 +1,348 @@
1
+ export const id = 2199;
2
+ export const ids = [2199];
3
+ export const modules = {
4
+
5
+ /***/ 25517:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ $: () => (/* binding */ ORACLE_TYPES),
10
+ /* harmony export */ U: () => (/* binding */ evaluateOracle)
11
+ /* harmony export */ });
12
+ // Deterministic security oracles. A model may SELECT an oracle and propose its parameters; it may never decide that the
13
+ // oracle succeeded. Each oracle is a pure function of an Observation gathered by the TRUSTED runner (never by the model):
14
+ //
15
+ // Observation = {
16
+ // exitCode, signal, timedOut, stdout, stderr,
17
+ // canaries: { <name>: { exists, digestBefore, digestAfter } } // checked by the runner on disk, after the run
18
+ // callbacks: [{ nonce }] // hits recorded by the runner's loopback listener
19
+ // nonce // per-run random token the target cannot predict
20
+ // }
21
+ //
22
+ // An oracle returns { fired, reason }. `fired:false` with `inconclusive:true` means the observation could not decide
23
+ // (for example a timeout); it is never treated as "not exploitable".
24
+ const ORACLE_TYPES = Object.freeze([
25
+ 'crash_sanitizer', 'command_execution', 'unauthorized_http_access', 'unauthorized_db_read', 'database_mutation',
26
+ 'filesystem_mutation', 'outbound_callback', 'authentication_bypass', 'arbitrary_assertion',
27
+ ]);
28
+
29
+ const SANITIZER_RE = /(AddressSanitizer|UndefinedBehaviorSanitizer|stack-buffer-overflow|heap-buffer-overflow|SEGV on unknown address|stack smashing detected|buffer overflow detected)/i;
30
+ const CRASH_SIGNALS = new Set(['SIGSEGV', 'SIGABRT', 'SIGBUS', 'SIGILL', 'SIGFPE']);
31
+
32
+ const out = (fired, reason, extra = {}) => ({ fired, reason, ...extra });
33
+ const inconclusive = (reason) => ({ fired: false, inconclusive: true, reason });
34
+ const has = (obs, text) => typeof text === 'string' && text.length > 0 && `${obs.stdout || ''}${obs.stderr || ''}`.includes(text);
35
+
36
+ const IMPL = {
37
+ crash_sanitizer(obs) {
38
+ if (SANITIZER_RE.test(obs.stderr || '')) return out(true, 'sanitizer or runtime memory-safety report on stderr');
39
+ if (obs.signal && CRASH_SIGNALS.has(obs.signal)) return out(true, `process terminated by ${obs.signal}`);
40
+ if (obs.timedOut) return inconclusive('run timed out before a crash could be judged');
41
+ return out(false, 'no crash signal and no sanitizer report');
42
+ },
43
+ // The injected payload asks the shell/interpreter to create a file named after the per-run nonce inside the
44
+ // watched canary directory. The target cannot create it unless attacker data reached a command.
45
+ command_execution(obs, p) {
46
+ const c = obs.canaries?.[p.canary || 'command'];
47
+ if (!c) return inconclusive('command canary was not armed by the runner');
48
+ if (c.exists && !c.existedBefore) return out(true, 'nonce-named canary file created by an injected command');
49
+ if (obs.timedOut) return inconclusive('run timed out before the canary could be judged');
50
+ return out(false, 'canary file not created');
51
+ },
52
+ // A secret planted behind authorization (never shown to the exploit client). Seeing the nonce-bearing secret in the
53
+ // client's output while the actor held no credential is the violation.
54
+ unauthorized_http_access(obs, p) {
55
+ const secret = obs.secrets?.[p.secret || 'http'];
56
+ if (!secret) return inconclusive('protected secret was not armed by the runner');
57
+ if (has(obs, secret)) return out(true, 'protected resource content returned to an unauthenticated actor');
58
+ if (obs.timedOut) return inconclusive('run timed out');
59
+ return out(false, 'protected content not observed');
60
+ },
61
+ unauthorized_db_read(obs, p) {
62
+ const secret = obs.secrets?.[p.secret || 'db'];
63
+ if (!secret) return inconclusive('database canary row was not armed by the runner');
64
+ if (has(obs, secret)) return out(true, 'row the actor may not read was returned');
65
+ if (obs.timedOut) return inconclusive('run timed out');
66
+ return out(false, 'canary row value not observed');
67
+ },
68
+ database_mutation(obs, p) {
69
+ const c = obs.canaries?.[p.canary || 'db'];
70
+ if (!c) return inconclusive('database canary was not armed by the runner');
71
+ if (c.digestBefore && c.digestAfter && c.digestBefore !== c.digestAfter) return out(true, 'protected database state changed');
72
+ if (obs.timedOut) return inconclusive('run timed out');
73
+ return out(false, 'protected database state unchanged');
74
+ },
75
+ filesystem_mutation(obs, p) {
76
+ const c = obs.canaries?.[p.canary || 'fs'];
77
+ if (!c) return inconclusive('filesystem canary was not armed by the runner');
78
+ if ((c.exists && !c.existedBefore) || (c.digestBefore && c.digestAfter && c.digestBefore !== c.digestAfter)) return out(true, 'file outside the permitted area was created or modified');
79
+ if (obs.timedOut) return inconclusive('run timed out');
80
+ return out(false, 'protected path unchanged');
81
+ },
82
+ outbound_callback(obs) {
83
+ if (obs.callbackUnavailable) return { fired: false, unsupported: true, reason: 'loopback callback listener is not available on this backend' };
84
+ const hit = (obs.callbacks || []).some((c) => c && c.nonce && c.nonce === obs.nonce);
85
+ if (hit) return out(true, 'nonce-bearing request reached the runner-controlled listener');
86
+ if (obs.timedOut) return inconclusive('run timed out');
87
+ return out(false, 'no nonce-bearing callback received');
88
+ },
89
+ authentication_bypass(obs, p) {
90
+ const secret = obs.secrets?.[p.secret || 'auth'];
91
+ if (!secret) return inconclusive('protected secret was not armed by the runner');
92
+ if (has(obs, secret)) return out(true, 'privileged content returned without valid credentials');
93
+ if (obs.timedOut) return inconclusive('run timed out');
94
+ return out(false, 'privileged content not observed without credentials');
95
+ },
96
+ // Declarative only: no code, no regex supplied by a model. Operators are a closed set.
97
+ arbitrary_assertion(obs, p) {
98
+ const a = p.assert;
99
+ if (!a || typeof a !== 'object') return inconclusive('assertion missing');
100
+ if (obs.timedOut) return inconclusive('run timed out');
101
+ if ('exitCode' in a) return a.exitCode === obs.exitCode ? out(true, `exit code was ${obs.exitCode}`) : out(false, `exit code was ${obs.exitCode}`);
102
+ if ('stdoutIncludes' in a) return has({ stdout: obs.stdout }, String(a.stdoutIncludes)) ? out(true, 'stdout contained the asserted text') : out(false, 'stdout did not contain the asserted text');
103
+ if ('stderrIncludes' in a) return has({ stderr: obs.stderr }, String(a.stderrIncludes)) ? out(true, 'stderr contained the asserted text') : out(false, 'stderr did not contain the asserted text');
104
+ return inconclusive('unsupported assertion operator');
105
+ },
106
+ };
107
+
108
+ function evaluateOracle(spec, observation) {
109
+ if (!spec || !ORACLE_TYPES.includes(spec.type)) return { fired: false, unsupported: true, reason: `unknown oracle type: ${String(spec?.type)}` };
110
+ if (!observation || typeof observation !== 'object') return inconclusive('no observation');
111
+ try { return IMPL[spec.type](observation, spec.params || {}); } catch (e) { return inconclusive(`oracle error: ${e.message}`); }
112
+ }
113
+
114
+
115
+ /***/ }),
116
+
117
+ /***/ 37055:
118
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
119
+
120
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
121
+ /* harmony export */ IW: () => (/* binding */ verifierView),
122
+ /* harmony export */ V2: () => (/* binding */ substitute),
123
+ /* harmony export */ Yj: () => (/* binding */ LANGUAGES),
124
+ /* harmony export */ dw: () => (/* binding */ canonicalize),
125
+ /* harmony export */ mC: () => (/* binding */ validateReproducer),
126
+ /* harmony export */ sc: () => (/* binding */ sha256),
127
+ /* harmony export */ we: () => (/* binding */ reproducerDigest)
128
+ /* harmony export */ });
129
+ /* unused harmony exports MAX_FILES, MAX_FILE_BYTES, isSafeRelPath */
130
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
131
+ /* harmony import */ var _oracles_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25517);
132
+ // Serialized reproducer: the ONLY thing an independent verifier receives (besides revision, finding id and oracle).
133
+ // It carries no reasoning, no model output and no prior results. Everything per-run (nonce, secrets, canary paths,
134
+ // callback URL) is a placeholder substituted fresh by the trusted runner, so a replay can never reuse a stale value
135
+ // and a target cannot learn the values in advance.
136
+
137
+
138
+
139
+ const REPRODUCER_SCHEMA = 'agentic-security/reproducer';
140
+ const REPRODUCER_VERSION = 1;
141
+ const LANGUAGES = Object.freeze(['javascript', 'typescript', 'python', 'java', 'c', 'cpp', 'csharp']);
142
+ const MAX_FILES = 64;
143
+ const MAX_FILE_BYTES = 256 * 1024;
144
+ const MAX_TOTAL_BYTES = 1024 * 1024;
145
+ const PLACEHOLDER_RE = /\{\{([A-Z_]+)(?::([a-z0-9_-]{1,32}))?\}\}/g;
146
+ const ALLOWED_PLACEHOLDERS = new Set(['NONCE', 'SECRET', 'CANARY', 'CALLBACK_URL', 'CALLBACK_PORT', 'ROOT', 'NODE', 'PYTHON', 'JAVA', 'JAVAC', 'CC']);
147
+
148
+ function isSafeRelPath(rel) {
149
+ if (typeof rel !== 'string' || !rel || rel.length > 200) return false;
150
+ if (rel.startsWith('/') || rel.includes('\0') || rel.includes('\\')) return false;
151
+ if (['canary', 'tmp'].includes(rel.split('/')[0])) return false;
152
+ return rel.split('/').every((seg) => seg && seg !== '.' && seg !== '..' && !seg.startsWith('__oracle_'));
153
+ }
154
+
155
+ /** Canonical JSON (sorted keys) so identical reproducers hash identically regardless of key order. */
156
+ function canonicalize(v) {
157
+ if (Array.isArray(v)) return `[${v.map(canonicalize).join(',')}]`;
158
+ if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
159
+ return JSON.stringify(v);
160
+ }
161
+ const sha256 = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(s).digest('hex');
162
+ const reproducerDigest = (r) => sha256(canonicalize(r));
163
+
164
+ function validateReproducer(r) {
165
+ const errs = [];
166
+ if (!r || typeof r !== 'object' || Array.isArray(r)) return ['reproducer must be an object'];
167
+ if (r.schema !== REPRODUCER_SCHEMA) errs.push('unknown schema');
168
+ if (r.version !== REPRODUCER_VERSION) errs.push('unsupported reproducer version');
169
+ if (typeof r.findingId !== 'string' || !r.findingId || r.findingId.length > 200) errs.push('findingId required');
170
+ if (!LANGUAGES.includes(r.lang)) errs.push(`unsupported language: ${String(r.lang)}`);
171
+ if (!r.oracle || !_oracles_js__WEBPACK_IMPORTED_MODULE_1__/* .ORACLE_TYPES */ .$.includes(r.oracle.type)) errs.push(`unknown oracle: ${String(r.oracle?.type)}`);
172
+ const files = r.files ?? {};
173
+ if (typeof files !== 'object' || Array.isArray(files)) errs.push('files must be an object');
174
+ else {
175
+ const names = Object.keys(files);
176
+ if (names.length > MAX_FILES) errs.push(`file count must be at most ${MAX_FILES}`);
177
+ let total = 0;
178
+ for (const n of names) {
179
+ if (!isSafeRelPath(n)) { errs.push(`unsafe file path: ${n}`); continue; }
180
+ if (typeof files[n] !== 'string') { errs.push(`file ${n} must be a string`); continue; }
181
+ const b = Buffer.byteLength(files[n]);
182
+ total += b;
183
+ if (b > MAX_FILE_BYTES) errs.push(`file ${n} exceeds ${MAX_FILE_BYTES} bytes`);
184
+ }
185
+ if (total > MAX_TOTAL_BYTES) errs.push('reproducer exceeds total size limit');
186
+ }
187
+ const sourceNames = r.sources && typeof r.sources === 'object' && !Array.isArray(r.sources) ? Object.keys(r.sources) : [];
188
+ if (Object.keys(files).length + sourceNames.length === 0) errs.push('reproducer must declare at least one file or source');
189
+ for (const key of ['build', 'run']) {
190
+ const a = r[key];
191
+ if (a == null && key === 'build') continue;
192
+ if (!Array.isArray(a) || !a.length || !a.every((x) => typeof x === 'string' && x.length < 2000 && !x.includes('\0'))) errs.push(`${key} must be a non-empty argv array of strings (no shell string)`);
193
+ }
194
+ for (const text of [...Object.values(files || {}), ...(r.build || []), ...(r.run || [])]) {
195
+ if (typeof text !== 'string') continue;
196
+ for (const m of text.matchAll(PLACEHOLDER_RE)) if (!ALLOWED_PLACEHOLDERS.has(m[1])) errs.push(`unknown placeholder {{${m[1]}}}`);
197
+ }
198
+ if (r.watch != null) {
199
+ if (typeof r.watch !== 'object' || Array.isArray(r.watch)) errs.push('watch must be an object');
200
+ else for (const [k, v] of Object.entries(r.watch)) if (!/^[a-z0-9_-]{1,32}$/.test(k) || !isSafeRelPath(v)) errs.push(`bad watch entry: ${k}`);
201
+ }
202
+ if (r.env != null) {
203
+ if (typeof r.env !== 'object' || Array.isArray(r.env)) errs.push('env must be an object');
204
+ else for (const [k, v] of Object.entries(r.env)) {
205
+ if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(k)) errs.push(`bad env var name: ${k}`);
206
+ if (typeof v !== 'string' || v.length > 4096) errs.push(`env value for ${k} must be a short string`);
207
+ }
208
+ }
209
+ if (r.sources != null) {
210
+ if (typeof r.sources !== 'object' || Array.isArray(r.sources)) errs.push('sources must be an object');
211
+ else for (const [dest, src] of Object.entries(r.sources)) if (!isSafeRelPath(dest) || !isSafeRelPath(src)) errs.push(`bad sources entry: ${dest}`);
212
+ }
213
+ if (r.network != null && !['none', 'callback'].includes(r.network)) errs.push('network must be none or callback');
214
+ return errs;
215
+ }
216
+
217
+ /** The view handed to an independent verifier: exactly the reproducer, plus identifiers. Nothing else. */
218
+ function verifierView({ revision, reproducer, findingId, oracle }) {
219
+ return Object.freeze({ revision: revision ?? null, findingId, oracle, reproducer: structuredClone(reproducer) });
220
+ }
221
+
222
+ function substitute(text, ctx) {
223
+ return text.replace(PLACEHOLDER_RE, (whole, name, arg) => {
224
+ switch (name) {
225
+ case 'NONCE': return ctx.nonce;
226
+ case 'SECRET': return ctx.secret(arg || 'default');
227
+ case 'CANARY': return ctx.canary(arg || 'default');
228
+ case 'CALLBACK_URL': return ctx.callbackUrl ?? whole;
229
+ case 'CALLBACK_PORT': return String(ctx.callbackPort ?? '');
230
+ case 'ROOT': return ctx.root;
231
+ case 'NODE': return ctx.tools.node;
232
+ case 'PYTHON': return ctx.tools.python ?? whole;
233
+ case 'JAVA': return ctx.tools.java ?? whole;
234
+ case 'JAVAC': return ctx.tools.javac ?? whole;
235
+ case 'CC': return ctx.tools.cc ?? whole;
236
+ default: return whole;
237
+ }
238
+ });
239
+ }
240
+
241
+
242
+ /***/ }),
243
+
244
+ /***/ 62199:
245
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
246
+
247
+ __webpack_require__.r(__webpack_exports__);
248
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
249
+ /* harmony export */ findingKey: () => (/* binding */ findingKey),
250
+ /* harmony export */ latestProof: () => (/* binding */ latestProof),
251
+ /* harmony export */ listProofs: () => (/* binding */ listProofs),
252
+ /* harmony export */ saveProof: () => (/* binding */ saveProof),
253
+ /* harmony export */ scrubDeep: () => (/* binding */ scrubDeep)
254
+ /* harmony export */ });
255
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
256
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
257
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
258
+ /* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
259
+ /* harmony import */ var _llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(65388);
260
+ /* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(37055);
261
+ // Proof artifact store: `.agentic-security/proofs/<findingKey>/<recordDigest>.json`.
262
+ //
263
+ // Reuses the existing state machinery rather than adding another: `safeWriteState` (refuses unsafe dirs and symlinks),
264
+ // `maybeEncryptForWrite` / `maybeDecryptForRead` (the `proofs` artifact is registered `confidential`), retention
265
+ // (evidence class) and legal holds (a hold on `proofs` blocks purge). Nothing persisted contains a plaintext credential:
266
+ // records are passed through a recursive secret filter first, and the runner never returns its per-run nonce or secrets.
267
+
268
+
269
+
270
+
271
+
272
+
273
+
274
+ const PROOF_RECORD_SCHEMA = 'agentic-security/proof-record';
275
+ const PROOFS_DIR = 'proofs';
276
+ const MAX_RECORD_BYTES = 512 * 1024;
277
+
278
+ const findingKey = (id) => `${String(id).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80)}-${(0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)(String(id)).slice(0, 8)}`;
279
+
280
+ // The shared redactor catches key=value shapes; bare tokens need their own pass, so persisted evidence cannot carry one.
281
+ const BARE_SECRET_RES = [
282
+ /\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/g, /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, /\bsk-[A-Za-z0-9_-]{20,}\b/g,
283
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
284
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, /\bSECRET-[0-9a-f]{24}-[a-z0-9_-]+\b/g,
285
+ ];
286
+ function scrubString(text) {
287
+ let t = String(text);
288
+ try { t = (0,_llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__/* .redactSecrets */ .f)(t).text; } catch { return '[unredactable]'; }
289
+ for (const re of BARE_SECRET_RES) t = t.replace(re, '[REDACTED-SECRET]');
290
+ return t;
291
+ }
292
+
293
+ /** Deep-copy `v` with every string passed through the secret redactor. */
294
+ function scrubDeep(v) {
295
+ if (typeof v === 'string') return scrubString(v);
296
+ if (Array.isArray(v)) return v.map(scrubDeep);
297
+ if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, scrubDeep(x)]));
298
+ return v;
299
+ }
300
+
301
+ function recordDigest(record) {
302
+ const { recordDigest: _omit, ...rest } = record;
303
+ return (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .canonicalize */ .dw)(rest));
304
+ }
305
+
306
+ function saveProof(scanRoot, record) {
307
+ const safe = scrubDeep(record);
308
+ safe.schema = PROOF_RECORD_SCHEMA;
309
+ safe.recordDigest = recordDigest(safe);
310
+ const text = JSON.stringify(safe, null, 2);
311
+ if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'proof record exceeds the size limit' };
312
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .ensureStateDir */ .xH)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
313
+ const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(scanRoot, PROOFS_DIR, text);
314
+ if (!enc.ok) return { ok: false, reason: enc.reason };
315
+ const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(record.findingId), `${safe.recordDigest}.json`);
316
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .safeWriteState */ .Ep)(file, enc.content)) return { ok: false, reason: 'state write refused' };
317
+ return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
318
+ }
319
+
320
+ function readRecord(file) {
321
+ const text = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8'));
322
+ const rec = JSON.parse(text);
323
+ // A record whose content no longer matches its own digest was modified after it was written.
324
+ if (rec.recordDigest !== recordDigest(rec)) return { tampered: true, record: rec };
325
+ return { tampered: false, record: rec };
326
+ }
327
+
328
+ /** Newest-first records for a finding. Tampered records are returned flagged, never silently dropped or trusted. */
329
+ function listProofs(scanRoot, findingId) {
330
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(findingId));
331
+ let names = [];
332
+ try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.endsWith('.json')); } catch { return []; }
333
+ const out = [];
334
+ for (const n of names) {
335
+ const f = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
336
+ try { const { tampered, record } = readRecord(f); out.push({ file: f, tampered, record, mtime: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(f).mtimeMs }); } catch { out.push({ file: f, tampered: true, record: null, mtime: 0 }); }
337
+ }
338
+ return out.sort((a, b) => b.mtime - a.mtime);
339
+ }
340
+
341
+ function latestProof(scanRoot, findingId) {
342
+ return listProofs(scanRoot, findingId).find((p) => p.record) ?? null;
343
+ }
344
+
345
+
346
+ /***/ })
347
+
348
+ };
@@ -10,10 +10,11 @@ export const modules = {
10
10
  /* harmony export */ ensurePolicyKeyPair: () => (/* binding */ ensurePolicyKeyPair),
11
11
  /* harmony export */ loadPolicyBundles: () => (/* binding */ loadPolicyBundles),
12
12
  /* harmony export */ loadPolicyPublicKey: () => (/* binding */ loadPolicyPublicKey),
13
+ /* harmony export */ rE: () => (/* binding */ computePolicyDrift),
13
14
  /* harmony export */ resolveEffectivePolicy: () => (/* binding */ resolveEffectivePolicy),
14
15
  /* harmony export */ signPolicyBundle: () => (/* binding */ signPolicyBundle)
15
16
  /* harmony export */ });
16
- /* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle, computePolicyDrift */
17
+ /* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle */
17
18
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
18
19
  /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
19
20
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);