@clear-capabilities/agentic-security-scanner 0.175.0 → 0.177.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 (110) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/agentic-security.js +206 -28
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2066.index.js +851 -0
  5. package/dist/2199.index.js +1 -1
  6. package/dist/2584.index.js +3350 -0
  7. package/dist/2923.index.js +13 -0
  8. package/dist/2946.index.js +81 -0
  9. package/dist/3196.index.js +27 -0
  10. package/dist/3727.index.js +292 -0
  11. package/dist/37d64df006576248d0d8.json +286 -0
  12. package/dist/4168.index.js +509 -0
  13. package/dist/4281.index.js +993 -0
  14. package/dist/5099.index.js +96 -0
  15. package/dist/5456.index.js +291 -0
  16. package/dist/5616.index.js +1471 -0
  17. package/dist/5620.index.js +2494 -0
  18. package/dist/5817.index.js +714 -0
  19. package/dist/5937.index.js +672 -0
  20. package/dist/6333.index.js +98 -0
  21. package/dist/6655.index.js +3221 -0
  22. package/dist/7838.index.js +491 -974
  23. package/dist/818.index.js +4 -3
  24. package/dist/83.index.js +159 -0
  25. package/dist/8551.index.js +82 -0
  26. package/dist/8633.index.js +93 -0
  27. package/dist/8714.index.js +76 -76
  28. package/dist/8825.index.js +84 -0
  29. package/dist/932.index.js +186 -0
  30. package/dist/9560.index.js +358 -19
  31. package/dist/9800.index.js +6 -1
  32. package/dist/agentic-security.mjs +10 -10
  33. package/dist/agentic-security.mjs.sha256 +1 -1
  34. package/dist/b4289e728659450b5e19.js +32 -0
  35. package/package.json +8 -4
  36. package/src/artifact/CLAUDE.md +4 -0
  37. package/src/artifact/artifact-evidence.js +63 -0
  38. package/src/artifact/cosign.js +61 -0
  39. package/src/artifact/engine-source.js +72 -0
  40. package/src/artifact/oci-layout.js +47 -0
  41. package/src/artifact/registry.js +213 -0
  42. package/src/capabilities/tool-registry.js +2 -0
  43. package/src/lineage/CLAUDE.md +4 -0
  44. package/src/lineage/identity-token.js +11 -0
  45. package/src/lineage/observation-adapters.js +13 -0
  46. package/src/lineage/runtime-edge-states.js +139 -0
  47. package/src/lineage/runtime-explorer-view.js +82 -0
  48. package/src/lineage/runtime-observation.js +11 -0
  49. package/src/mcp/CLAUDE.md +3 -1
  50. package/src/mcp/proof-tools.js +132 -0
  51. package/src/mcp/server.js +1 -1
  52. package/src/mcp/tools.js +3 -1
  53. package/src/posture/CLAUDE.md +23 -0
  54. package/src/posture/artifact-registry.js +4 -0
  55. package/src/posture/custom-rules.js +54 -8
  56. package/src/posture/dnr/attack-facts.js +31 -0
  57. package/src/posture/dnr/cli.js +138 -0
  58. package/src/posture/dnr/detection-ir.js +123 -0
  59. package/src/posture/dnr/exploit-correlation.js +25 -0
  60. package/src/posture/dnr/exposure-window.js +134 -0
  61. package/src/posture/dnr/hunt.js +84 -0
  62. package/src/posture/dnr/log-reader.js +189 -0
  63. package/src/posture/dnr/matchers.js +90 -0
  64. package/src/posture/dnr/outputs.js +262 -0
  65. package/src/posture/dnr/payload-classes.js +120 -0
  66. package/src/posture/dnr/redact.js +25 -0
  67. package/src/posture/dnr/store.js +57 -0
  68. package/src/posture/dnr/validate.js +64 -0
  69. package/src/posture/dnr/view.js +51 -0
  70. package/src/posture/evaluation/lab/arm-ollama.js +88 -0
  71. package/src/posture/evaluation/lab/arms.js +142 -0
  72. package/src/posture/evaluation/lab/cli.js +135 -0
  73. package/src/posture/evaluation/lab/dataset.js +139 -0
  74. package/src/posture/evaluation/lab/materialize.js +44 -0
  75. package/src/posture/evaluation/lab/neutralize.js +144 -0
  76. package/src/posture/evaluation/lab/report.js +194 -0
  77. package/src/posture/evaluation/lab/run.js +225 -0
  78. package/src/posture/evaluation/lab/score.js +135 -0
  79. package/src/posture/evaluation/lab/scoreboard.js +48 -0
  80. package/src/posture/evaluation/lab/stats.js +41 -0
  81. package/src/posture/evaluation/lab/store.js +53 -0
  82. package/src/posture/fleet/cli.js +260 -63
  83. package/src/posture/fleet/discover-edges.js +135 -0
  84. package/src/posture/fleet/enterprise-graph.js +189 -0
  85. package/src/posture/fleet/fleet-graph.js +9 -4
  86. package/src/posture/fleet/fleet-manifest.js +50 -5
  87. package/src/posture/fleet/job-runner.js +153 -0
  88. package/src/posture/fleet/job-store.js +138 -0
  89. package/src/posture/fleet/lineage-summary.js +55 -0
  90. package/src/posture/fleet/org-health.js +56 -0
  91. package/src/posture/fleet/scan-cache.js +112 -0
  92. package/src/posture/fleet/scan-child.js +32 -0
  93. package/src/posture/proof-fabric/cli.js +34 -6
  94. package/src/posture/proof-fabric/container.js +57 -0
  95. package/src/posture/proof-fabric/fix-proof.js +2 -1
  96. package/src/posture/proof-fabric/generate-python.js +129 -0
  97. package/src/posture/proof-fabric/generate.js +214 -0
  98. package/src/posture/proof-fabric/reproducer.js +3 -0
  99. package/src/posture/proof-fabric/runner.js +44 -13
  100. package/src/posture/proof-fabric/siblings.js +77 -0
  101. package/src/posture/routing/policy-router.js +103 -0
  102. package/src/posture/rule-compiler/cli.js +101 -9
  103. package/src/posture/rule-compiler/corpus.js +13 -9
  104. package/src/posture/rule-compiler/ir.js +52 -9
  105. package/src/posture/rule-compiler/mutation.js +72 -1
  106. package/src/posture/rule-compiler/promotion.js +21 -5
  107. package/src/posture/rule-compiler/proposal.js +137 -0
  108. package/src/posture/rule-compiler/regression.js +95 -0
  109. package/src/posture/rule-compiler/store.js +45 -1
  110. package/src/posture/rule-pack-signing.js +4 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,31 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.177.0 - Moat PRDs, second pass: what a re-read of the PRD still required
13
+
14
+ - **Rule compiler: the IR is enforced, and promotion is honest.** Source, propagator, sink, sanitizer, dominating guard, framework constraint and exclusions are regular expressions the matcher enforces (they were free-text hints it ignored). A sanitizer is local to the hit it guards, not file-wide. Scoring goes through the scanner's own rule loader, so a rule the loader would refuse is rejected at compile time. A shared false-positive corpus is part of the promotion gate and `rules regress` re-scores every promoted rule. Found by trying the flow without test-only overrides: `rules promote` published an unsigned pack that the scanner refuses, so a "promoted" rule found nothing; promote now signs with `--sign-key`, says whether the scanner will load the rule, and `rules status` shows `live`. Underneath it, rule-pack signing was broken on current Node ("Invalid JWK OKP key") and is fixed.
15
+ - **Proof fabric.** Python and Java fixtures in both directions; toolchain detection probes the tool instead of trusting a launcher (macOS ships a `java` stub); a generated JavaScript harness no longer holds a 5 s timer open (proofs were about 20 s slower than needed); the PRD's three-finding demo and success metrics are tests and a benchmark (`bench/proof-fabric`): 39 of 39 reproductions, 0 of 39 false confirmations, 3 of 3 timeouts degraded.
16
+ - **Fleet.** Code Boundaries from each repository's own data flow graph (`fleet scan --lineage`) add data classes and destinations the manifest did not declare, with evidence; `fleet status` and `fleet resume` accept a job id; a measured scaling benchmark (`bench/fleet-scale`).
17
+ - **Runtime twin.** `explore . --runtime` and `runtime explore` write the five views as a static page; finding-level exploit correlation (proof, hunt and trace agree).
18
+ - **Eval Lab.** Real advisory pairs from the independent population can be imported (`scripts/eval-lab/import-independent.mjs`; dev and validation splits only, permitted licences only); provider variance in reports.
19
+ - **Contract.** Every status page carries the required fields; every guide has an output-schema section and a troubleshooting section; capability ledger entries for `proof` and `rules`.
20
+
21
+ ## 0.176.0 - Moat PRDs: proof, rules, fleet, runtime twin, artifacts, eval lab, detect and respond
22
+
23
+ - **Gap closure across the seven moat PRDs, second pass.** Runtime twin: `explore . --runtime` writes a static, script-free page with the five views and per-edge static+runtime evidence; `runtime query vulnerable-functions-in-production` evaluates scan findings against a runtime trace with the scan's own matcher. Detect and respond: Elastic, Google SecOps (YARA-L) and Datadog renderers that translate exactly or say why not. Eval Lab: a built-in local Ollama arm (loopback only, leak-guarded), and the unloadable `sard` source kind removed from the schema. Proof fabric: Python reproducer generation, confirmed end to end. Fleet: edges discovered from code literals (`fleet edges --undeclared`, `aliases:` in the manifest) shown beside declared edges, plus a measured scaling benchmark (`bench/fleet-scale`). Still not built or not proven against real tools: see each `docs/implementation-status/` file.
24
+
25
+ - **Runtime twin: edge states, identity tokens, two more queries (PRD 2 gap closure).** `runtime edges` derives the six edge states (observed, blocked, policy violation, exploit correlated only with explicit evidence, not observed, static only); `--record-identity-tokens` stores identity only as a keyed-hash `identity.token`; `runtime query` now answers `runtime-flows-absent-from-static` (candidates) and `identities-using-risky-paths`. Route and function rungs and the Explorer views remain unbuilt.
26
+
27
+ - **Image scan: three new sources and signed evidence (PRD 5 gap closure).** `image scan --engine docker|podman` (local image via `save`, supervised, byte-capped), `image scan --registry <ref>` (explicit remote OCI image: HTTPS-only, digest-verified, size-capped blobs, credentials never sent cross-host), `image verify --cosign` (delegated, bound to the scanned digest, never `verified` unless cosign names that digest), and `image scan --attest` writing an Ed25519 evidence bundle that `verify-attestation` checks. Proven against a mock registry and stand-in `docker`/`cosign` only.
28
+
29
+ - **Detection and response (`dnr`).** `dnr generate|validate|hunt|window|show|status <finding-id>` turns a proven vulnerability into a validated detection (Sigma, a generic HTTP rule, a detect-only WAF suggestion, a structured log query, YARA for the deserialization class, and Splunk, Sentinel and CloudWatch text), hunts local historical logs (`OBSERVED`, `NOT_OBSERVED`, `INCONCLUSIVE`, `TELEMETRY_UNAVAILABLE`, with a distinct exit code each; `NOT_OBSERVED` never means not exploited), computes the observable exposure window from deployment history, and shows Find, Prove, Hunt, Fix, Verify and Detect for one finding. Built only from facts the proof established, with no attacker address, domain, hash or user agent anywhere in the format. Local only: no network or model call. A reproducer may now carry an optional `attack` block (endpoint, parameter, payload class) that the proof runner ignores.
30
+ - **Eval Lab and model router.** `eval run|report|compare|list`, `models benchmark` and `models route explain` measure a deterministic baseline and any number of models on a dataset that includes hard negatives, and route each task to a model. Detection is scored on location; exploit proof is decided by executing the arm's reproducer in the proof fabric (a reproducer that works on fixed code is a measured false confirmation); remediation replays the original exploit against the arm's patch and runs the dataset's tests; cost, latency and run-to-run variance are reported, a mean over fewer than three runs is marked not representative, and intelligence per dollar (confirmed vulnerabilities, verified fixes, true positives per $100, recall at a fixed budget) divides by measured spend only. Every workspace is neutralized and audited before an arm sees it, and the audit result is recorded on every run. Four routing policies (best-quality, balanced, lowest-cost-above-quality-threshold, local-only) read a digest-checked scoreboard, explain the choice, and refuse to guess. A non-local arm needs `--allow-external` and is checked against the egress policy first; there is no fallback from one arm to another. No built-in provider adapters and no shipped real-vulnerability corpus.
31
+ - **Fleet job fabric and enterprise graph.** `fleet scan` now plans one idempotent job per repository in a local SQLite store (lease, heartbeat, retry count, timeout, result hash), runs each scan in a supervised child process under a hard timeout with bounded workers, and reuses a content-addressed cache that is invalidated by any change to the commit, uncommitted files, lockfiles, configuration or scanner build. `fleet resume <run-id> [--retry-failed]` continues a run after a crash, a kill or timeouts, recovers jobs held by dead local workers, fences out a worker that lost its lease, and refuses a manifest that changed. Organization health (`Complete / Partial / Failed / Timed out / Not run`) cannot be GREEN while a required repository has no result. The manifest gained per-repository and resource metadata (Internet exposure, environment, data classes, trust zone, calls, queues, databases, providers); `fleet query` gained `--blast-radius` (counts, version aware), `--internet-to <data-class> [--involving-vulnerable ...]` and one recognised question sentence; `fleet explore` writes a static page. Reach edges are declared, not discovered, and every edge keeps its evidence.
32
+ - **Rule compiler: synthesis loop, seven mutators, provenance, cost.** `rules synthesize --auto` (and the program's `labs synthesize-rule <finding-id>`) proposes a rule itself from the vulnerable and patched code, scores every attempt on the real corpus with its mutations, revises from why it failed, stops after a bounded number of attempts, and emits a CANDIDATE; a model's own proposal is tried first. All seven mutators exist now (equivalent API, statement reordering, alternate sanitizer and framework variant join rename, wrapper and indirection), and the safe side is mutated too, so a rule keyed to one sanitizer's name fails its protected sibling and cannot be promoted. The anti-cheating check covers benchmark names, advisory ids, vulnerability comments and CWE-bearing or answer-key file names and fails closed before anything is written. Each rule gets a `provenance.json` (corpus hashes, benchmark results, rule hash, promotion event) and `rules cost <rule-id>` reports the first-discovery cost against later deterministic detections.
33
+ - **Exploit proof: containers, generated reproducers, sibling search, MCP.** An attempt now runs in a container when a runtime is reachable and the image is already present (no network, not privileged, capabilities dropped, read-only root, bounded resources, host user, one bind mount, removed on every exit path); verified against a fake engine only. `triage exploit --prove` and `fix --require-proof` build a reproducer from the finding when none exists (JavaScript; command injection, path-traversal writes and SSRF; scripts reading `process.argv[2]` and exported functions or request handlers; imports limited to Node built-ins and repository files) and label it a proposal that counts only if execution confirms it; `proof generate` writes one without running it. `fix --require-proof` searches for sibling exposures (the engine's other findings plus the same line shape elsewhere) and reports a hit or a truncated search as DEGRADED. New MCP tools `prove_finding` and `prove_fix` (25 tools now), both classified mutating and gated on `confirm: true`.
34
+ - **Fixed: `triage exploit --prove` could not prove a reproducer that imports repository sources.** The command never passed a source root, so every such reproducer ended with incomplete attempts. It now reads sources from the project root, as `fix --require-proof` already did.
35
+ - **`fix --require-proof` records its result** so the finding view can show the fix and the post-fix replay.
36
+
12
37
  ## 0.175.0 - More NixOS and Haskell detection
13
38
 
14
39
  Development cases only (renamed copies and near misses that must stay silent); no accuracy measurement was run, and the frozen holdout and unseen-shapes split were not read.
@@ -1763,7 +1763,7 @@ async function cmdRules(args) {
1763
1763
  // Self-learning finding-to-rule compiler (PRD 3): synthesize/evaluate/promote/status/rollback a DETECTION rule
1764
1764
  // from one confirmed finding — distinct from validate/lock below (rules.yml override linting and pack locking).
1765
1765
  // Dispatches to its own module rather than inlining here, same convention as `triage exploit`/`proof`.
1766
- if (sub === 'synthesize' || sub === 'evaluate' || sub === 'promote' || sub === 'status' || sub === 'rollback') {
1766
+ if (sub === 'synthesize' || sub === 'evaluate' || sub === 'promote' || sub === 'status' || sub === 'rollback' || sub === 'cost' || sub === 'regress') {
1767
1767
  return (await import('../src/posture/rule-compiler/cli.js')).cmdRules(args, path.resolve(args.flags.root || '.'));
1768
1768
  }
1769
1769
  const target = path.resolve(args._[2] || '.');
@@ -1780,7 +1780,7 @@ async function cmdRules(args) {
1780
1780
  console.log(` scanner: ${lock.scannerVersion} rulePackHash: ${lock.rulePackHash}`);
1781
1781
  return 0;
1782
1782
  }
1783
- console.error('rules validate | rules lock | rules synthesize <finding-id> --spec <file.json> | rules evaluate|promote|status|rollback <rule-id>'); return 4;
1783
+ console.error('rules validate | rules lock | rules synthesize <finding-id> --spec <file.json> [--auto] | rules evaluate|promote|status|rollback|cost <rule-id>'); return 4;
1784
1784
  }
1785
1785
 
1786
1786
  // `agentic-security secure [--launch]` — smart router. One command picks the
@@ -3324,6 +3324,16 @@ async function cmdVerifyAttestation(args) {
3324
3324
  // verifyEvidenceBundle() call below, which assumes evidence-bundle.js's
3325
3325
  // own shape (`.evidence`, `.finding` with severity/vuln/file/line) and
3326
3326
  // would misinterpret a provenance bundle.
3327
+ const { verifyArtifactEvidenceBundle, ARTIFACT_BUNDLE_SCHEMA } = await import('../src/artifact/artifact-evidence.js');
3328
+ if (bundle.schema === ARTIFACT_BUNDLE_SCHEMA) {
3329
+ const ar = verifyArtifactEvidenceBundle(bundle, publicKeyPem);
3330
+ if (!ar.ok) { console.error(`✗ INVALID — ${ar.reason}`); return 1; }
3331
+ console.log('✓ VALID — the image evidence bundle is exactly what the signer produced.');
3332
+ console.log(` image: ${bundle.artifact.digest} ${bundle.artifact.os}/${bundle.artifact.architecture}`);
3333
+ console.log(` vulnerabilities listed: ${bundle.scan.vulnerabilities.length} of ${bundle.scan.truncated.vulnerabilitiesTotal}; recoverable secrets: ${bundle.scan.truncated.secretsTotal}; image signature: ${bundle.signature.state}`);
3334
+ console.log(` ${bundle.doesNotProve}`);
3335
+ return 0;
3336
+ }
3327
3337
  const { verifyProvenanceEvidenceBundle, PROVENANCE_BUNDLE_SCHEMA } = await import('../src/posture/provenance-evidence-bundle.js');
3328
3338
  if (bundle.schema === PROVENANCE_BUNDLE_SCHEMA) {
3329
3339
  const pr = verifyProvenanceEvidenceBundle(bundle, publicKeyPem);
@@ -3680,16 +3690,22 @@ async function cmdFix(args) {
3680
3690
  // --require-proof: the patch is applied ONLY if the original exploit is confirmed beforehand and demonstrably blocked
3681
3691
  // afterwards (posture/proof-fabric/fix-proof.js). Missing reproducer, unavailable environment or an incomplete replay all refuse.
3682
3692
  if (args.flags['require-proof']) {
3683
- const { loadReproducerFor } = await import('../src/posture/proof-fabric/cli.js');
3693
+ const { resolveReproducer } = await import('../src/posture/proof-fabric/cli.js');
3694
+ const { findSiblings } = await import('../src/posture/proof-fabric/siblings.js');
3684
3695
  const { verifyFixWithProof, formatFixProof } = await import('../src/posture/proof-fabric/fix-proof.js');
3685
3696
  const { verifyFix } = await import('../src/posture/fix-verify.js');
3686
- const loaded = loadReproducerFor(scanRoot, f.id, args.flags.reproducer);
3687
- if (!loaded) { console.error(`--require-proof: no reproducer for ${f.id}. Refusing to apply; nothing was executed. Supply --reproducer <file>.`); return 4; }
3697
+ const loaded = resolveReproducer(scanRoot, f, args.flags.reproducer);
3698
+ if (!loaded.reproducer) { console.error(`--require-proof: no reproducer for ${f.id}, and none could be generated: ${loaded.reason}. Refusing to apply; nothing was executed. Supply --reproducer <file>.`); return 4; }
3699
+ if (loaded.generated) console.error(`--require-proof: using a reproducer generated from the finding (${loaded.generated.shape}, ${loaded.generated.class}); it counts only if executing it confirms the exploit.`);
3688
3700
  const files = { [f.file]: newContent };
3689
3701
  const staticVerification = await verifyFix({ scanRoot, originalFindingStableId: f.stableId || null, files, recordMetrics: false });
3690
- 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 }));
3691
- const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings });
3702
+ // equivalent sibling exposures: the engine's own other findings of this kind, plus other places with the same code shape
3703
+ const sib = findSiblings({ root: scanRoot, finding: f, scanFindings: last.findings || [] });
3704
+ const siblings = sib.siblings.map((x) => ({ id: x.id ?? null, file: x.file, line: x.line, source: x.source }));
3705
+ if (sib.truncated.length) console.error(`--require-proof: the sibling search stopped early (${sib.truncated.join(', ')}); absence of siblings is not shown.`);
3706
+ const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings, siblingSearchTruncated: sib.truncated });
3692
3707
  console.log(formatFixProof(fp));
3708
+ try { (await import('../src/posture/dnr/store.js')).saveRecord(scanRoot, f.stableId || f.id, 'fix', fp); } catch { /* recording is best effort; the refusal below does not depend on it */ }
3693
3709
  if (!fp.verified) { console.error('\nRefusing to apply: the fix is not proof-verified (--require-proof).'); return 4; }
3694
3710
  }
3695
3711
  let result = await applyVerifiedFix({
@@ -4065,6 +4081,8 @@ async function cmdBoundaries(args) {
4065
4081
  // every other cmdX(args) function's "resolves to an exit code" contract
4066
4082
  // while keeping the process alive for as long as the server is listening.
4067
4083
  async function cmdExplore(args) {
4084
+ // `explore . --runtime`: the runtime views are one static, script-free page, not a route on the hardened local server.
4085
+ if (args.flags.runtime) return cmdRuntimeExplorer(args, args._[1] || '.');
4068
4086
  const target = args._[1] || '.';
4069
4087
  const targetAbs = path.resolve(target);
4070
4088
 
@@ -5302,6 +5320,11 @@ async function cmdDataflowObservationsImport(args) {
5302
5320
  retention: { expiresAt: retainUntil },
5303
5321
  version: RUNTIME_OBSERVATION_VERSION,
5304
5322
  };
5323
+ if (args.flags['record-identity-tokens']) {
5324
+ // identity is recorded only as a keyed hash under the per-install key, and only when the operator asks for it
5325
+ const { makeIdentityTokenizer } = await import('../src/lineage/identity-token.js');
5326
+ context.tokenizeIdentity = makeIdentityTokenizer();
5327
+ }
5305
5328
  const { drafts, errors: parseErrors } = adapterImpl.parse(text, context);
5306
5329
 
5307
5330
  const { matchObservationToGraph } = await import('../src/lineage/observation-correlation.js');
@@ -5764,26 +5787,115 @@ const _RUNTIME_QUERIES = Object.freeze({
5764
5787
  },
5765
5788
  });
5766
5789
 
5767
- // PRD-named example queries this architecture cannot honestly answer —
5768
- // disclosed, never silently omitted. "Runtime flows absent from the
5769
- // static graph" needs a config-declared/auto-correlated edge mechanism
5770
- // (M5 7a / M2 F2-F3, both out of scope here — every RuntimeObservation
5771
- // matches an EXISTING static node, it never mints a new one). "Vulnerable
5772
- // functions actually exercised in production" and "identities using
5773
- // risky paths" both need identity/function-level telemetry this
5774
- // contract's closed-world schema deliberately omits (see this PRD's
5775
- // implementation-status doc).
5776
- const _RUNTIME_QUERIES_NOT_SUPPORTED = Object.freeze({
5777
- 'runtime-flows-absent-from-static': 'Needs a config-declared/auto-correlated edge mechanism (not built — see M5 7a / M2 F2-F3 in scanner/src/lineage/CLAUDE.md) — every RuntimeObservation matches an existing static graph node, it never mints a new one.',
5778
- 'vulnerable-functions-in-production': 'Needs function/symbol-level runtime telemetry; this adapter set only matches destinations (route/service/table/topic), never a function symbol.',
5779
- 'identities-using-risky-paths': 'The RuntimeObservation contract deliberately carries no identity field (a payload-smuggling risk) — see this PRD\'s implementation-status doc for why.',
5790
+ // PRD-named example queries this architecture cannot honestly answer, disclosed rather than omitted. "Vulnerable functions actually
5791
+ // exercised in production" needs function/symbol-level telemetry and a function matching rung, neither of which exists.
5792
+ const _RUNTIME_QUERIES_NOT_SUPPORTED = Object.freeze({});
5793
+
5794
+ // Queries whose answer is not a list of flows. They run through the edge-state module.
5795
+ const _RUNTIME_EXTRA_QUERIES = Object.freeze({
5796
+ 'runtime-flows-absent-from-static': 'Observed destinations the matcher could not place on any static node: CANDIDATES for a path static analysis missed (or a mapping the matcher is too weak to make).',
5797
+ 'vulnerable-functions-in-production': 'Scan findings whose own function, line or route appears in the runtime trace (.agentic-security/runtime-trace.jsonl). Evaluated against the trace on disk when you ask, using the same matcher the scan uses.',
5798
+ 'identities-using-risky-paths': 'Identity TOKENS (keyed hashes, never identities) seen on edges in a policy-violation or exploit-correlated state. Needs observations imported with --record-identity-tokens.',
5780
5799
  });
5781
5800
 
5801
+ async function _loadExploitEvidence(flag) {
5802
+ if (typeof flag !== 'string') return { evidence: [], errors: [] };
5803
+ const { validateExploitEvidence } = await import('../src/lineage/runtime-edge-states.js');
5804
+ let raw;
5805
+ try { raw = JSON.parse(fs.readFileSync(path.resolve(flag), 'utf8')); } catch (e) { return { evidence: [], errors: [`could not read --exploit-evidence: ${e.code ?? e.message}`] }; }
5806
+ return validateExploitEvidence(raw);
5807
+ }
5808
+
5809
+ async function cmdRuntimeExplorer(args, target) {
5810
+ const targetAbs = path.resolve(target);
5811
+ const loaded = await _loadRuntimeInputs(targetAbs);
5812
+ if (!loaded.ok) { process.stderr.write(`agentic-security explore --runtime: ${loaded.message}\n`); return 1; }
5813
+ const environment = typeof args.flags.environment === 'string' ? args.flags.environment : null;
5814
+ const { correlateObservations } = await import('../src/lineage/observation-correlation.js');
5815
+ const mod = await import('../src/lineage/runtime-edge-states.js');
5816
+ const { renderRuntimeExplorer } = await import('../src/lineage/runtime-explorer-view.js');
5817
+ const correlation = correlateObservations(loaded.graph, loaded.observations, { environment, windowStart: null, windowEnd: null });
5818
+ const ev = await _loadExploitEvidence(args.flags['exploit-evidence']);
5819
+ if (ev.errors.length) { process.stderr.write(`agentic-security explore --runtime: ${ev.errors.join('; ')}\n`); return 2; }
5820
+ const edgeStates = mod.deriveEdgeStates(loaded.graph, correlation, loaded.observations, { exploitEvidence: ev.evidence });
5821
+ const absent = correlation.evaluated ? mod.runtimeFlowsAbsentFromStatic(loaded.observations, correlation) : { candidates: [] };
5822
+ const html = renderRuntimeExplorer({ graph: loaded.graph, edgeStates, correlation, observations: loaded.observations ?? [], absent });
5823
+ const out = typeof args.flags.out === 'string' ? path.resolve(args.flags.out) : statePath(targetAbs, 'runtime-explorer.html');
5824
+ try { fs.mkdirSync(path.dirname(out), { recursive: true }); fs.writeFileSync(out, html); } catch (e) { process.stderr.write(`agentic-security explore --runtime: could not write ${args.flags.out ?? out}: ${e.code ?? e.message}\n`); return 1; }
5825
+ process.stdout.write(`runtime explorer written to ${path.relative(process.cwd(), out) || out} (static page, no server; open it in a browser)\n`);
5826
+ return 0;
5827
+ }
5828
+
5829
+ async function cmdRuntimeEdges(args) {
5830
+ const targetAbs = path.resolve(args._[2] || '.');
5831
+ const loaded = await _loadRuntimeInputs(targetAbs);
5832
+ if (!loaded.ok) { process.stderr.write(`agentic-security runtime edges: ${loaded.message}\n`); return 1; }
5833
+ const environment = typeof args.flags.environment === 'string' ? args.flags.environment : null;
5834
+ const { correlateObservations } = await import('../src/lineage/observation-correlation.js');
5835
+ const { deriveEdgeStates } = await import('../src/lineage/runtime-edge-states.js');
5836
+ const correlation = correlateObservations(loaded.graph, loaded.observations, { environment, windowStart: null, windowEnd: null });
5837
+ const ev = await _loadExploitEvidence(args.flags['exploit-evidence']);
5838
+ if (ev.errors.length) { process.stderr.write(`agentic-security runtime edges: ${ev.errors.join('; ')}\n`); return 2; }
5839
+ const states = deriveEdgeStates(loaded.graph, correlation, loaded.observations, { exploitEvidence: ev.evidence });
5840
+ if ((args.flags.format ?? 'json') === 'markdown') {
5841
+ const lines = ['# Static vs runtime, per edge', '', '| Edge | State | Observations | Allowed | Denied | Uncertain |', '|---|---|---|---|---|---|'];
5842
+ for (const [id, e] of Object.entries(states.byEdge)) lines.push(`| ${id} | ${e.states.join(', ')} | ${e.observations} | ${e.allowed} | ${e.denied} | ${e.uncertainObservations} |`);
5843
+ if (Object.keys(states.byNode).length) {
5844
+ lines.push('', '## Observed on a node with no modelled flow', '', '| Node | State | Observations | Allowed | Denied |', '|---|---|---|---|---|');
5845
+ for (const [id, n] of Object.entries(states.byNode)) lines.push(`| ${id} | ${n.states.join(', ')} | ${n.observations} | ${n.allowed} | ${n.denied} |`);
5846
+ }
5847
+ lines.push('', ...states.limitations.map((l) => `- ${l}`), '');
5848
+ process.stdout.write(lines.join('\n'));
5849
+ } else process.stdout.write(JSON.stringify({ evaluated: states.evaluated, counts: states.counts, byEdge: states.byEdge, byNode: states.byNode, limitations: states.limitations }, null, 2) + '\n');
5850
+ return 0;
5851
+ }
5852
+
5782
5853
  async function cmdRuntimeQuery(args) {
5783
5854
  const name = args._[2];
5855
+ if (name === 'vulnerable-functions-in-production' && !args.flags.list) {
5856
+ const rootAbs = path.resolve(args._[3] || '.');
5857
+ const lsPath = statePath(rootAbs, 'last-scan.json');
5858
+ let body; try { body = fs.readFileSync(lsPath, 'utf8'); } catch { process.stderr.write('agentic-security runtime query: no .agentic-security/last-scan.json here: run a scan first.\n'); return 1; }
5859
+ if (_verifyLastScan(body, lsPath + '.sig') !== true) { process.stderr.write('agentic-security runtime query: last-scan.json failed its integrity check; re-run the scan.\n'); return 1; }
5860
+ const { vulnerableFunctionsInProduction } = await import('../src/lineage/runtime-explorer-view.js');
5861
+ const { annotateRuntimeCorrelation } = await import('../src/posture/runtime-correlation.js');
5862
+ const scan = JSON.parse(body);
5863
+ // evaluated NOW against the trace on disk, by the same annotator the scan uses, so a finding added late in a scan is not missed
5864
+ // and the answer is never older than the trace
5865
+ await annotateRuntimeCorrelation(rootAbs, scan.findings ?? [], {});
5866
+ const answer = vulnerableFunctionsInProduction(scan.findings);
5867
+ if (answer.evaluated) {
5868
+ // RUNTIME_EXPLOIT_CORRELATED at the finding level: observed in the trace AND proven exploitable AND exploit-shaped requests seen in logs
5869
+ const { exploitCorrelationOf } = await import('../src/posture/dnr/exploit-correlation.js');
5870
+ for (const f of answer.findings) f.exploitCorrelation = exploitCorrelationOf(rootAbs, f.id);
5871
+ answer.counts.exploitCorrelated = answer.findings.filter((f) => f.exploitCorrelation.correlated).length;
5872
+ }
5873
+ process.stdout.write(JSON.stringify({ query: name, supported: true, ...answer }, null, 2) + '\n');
5874
+ return 0;
5875
+ }
5876
+ if (name && Object.prototype.hasOwnProperty.call(_RUNTIME_EXTRA_QUERIES, name) && !args.flags.list) {
5877
+ const targetAbs = path.resolve(args._[3] || '.');
5878
+ const loaded = await _loadRuntimeInputs(targetAbs);
5879
+ if (!loaded.ok) { process.stderr.write(`agentic-security runtime query: ${loaded.message}\n`); return 1; }
5880
+ const environment = typeof args.flags.environment === 'string' ? args.flags.environment : null;
5881
+ const { correlateObservations } = await import('../src/lineage/observation-correlation.js');
5882
+ const mod = await import('../src/lineage/runtime-edge-states.js');
5883
+ const correlation = correlateObservations(loaded.graph, loaded.observations, { environment, windowStart: null, windowEnd: null });
5884
+ if (!correlation.evaluated) { process.stdout.write(JSON.stringify({ query: name, supported: true, evaluated: false, reason: 'no runtime observations were imported, so nothing can be said (not "no risk")' }, null, 2) + '\n'); return 0; }
5885
+ if (name === 'runtime-flows-absent-from-static') {
5886
+ process.stdout.write(JSON.stringify({ query: name, supported: true, evaluated: true, ...mod.runtimeFlowsAbsentFromStatic(loaded.observations, correlation) }, null, 2) + '\n');
5887
+ return 0;
5888
+ }
5889
+ const ev = await _loadExploitEvidence(args.flags['exploit-evidence']);
5890
+ if (ev.errors.length) { process.stderr.write(`agentic-security runtime query: ${ev.errors.join('; ')}\n`); return 2; }
5891
+ const states = mod.deriveEdgeStates(loaded.graph, correlation, loaded.observations, { exploitEvidence: ev.evidence });
5892
+ process.stdout.write(JSON.stringify({ query: name, supported: true, evaluated: true, ...mod.identitiesUsingRiskyPaths(states) }, null, 2) + '\n');
5893
+ return 0;
5894
+ }
5784
5895
  if (!name || args.flags.list) {
5785
5896
  process.stdout.write('Supported queries:\n');
5786
5897
  for (const [k, v] of Object.entries(_RUNTIME_QUERIES)) process.stdout.write(` ${k} — ${v.description}\n`);
5898
+ for (const [k, v] of Object.entries(_RUNTIME_EXTRA_QUERIES)) process.stdout.write(` ${k} — ${v}\n`);
5787
5899
  process.stdout.write('\nNamed but NOT supported by this architecture (disclosed, not silently omitted):\n');
5788
5900
  for (const [k, v] of Object.entries(_RUNTIME_QUERIES_NOT_SUPPORTED)) process.stdout.write(` ${k} — ${v}\n`);
5789
5901
  // --list is always a successful, intentional operation (exit 0);
@@ -5915,10 +6027,14 @@ async function _resolveArtifactScan(target) {
5915
6027
  return { ok: true, record: scanned.record };
5916
6028
  }
5917
6029
 
6030
+ const SCANNER_VERSION_FOR_ATTEST = () => { try { return JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version; } catch { return null; } };
6031
+
5918
6032
  async function cmdImageScan(args) {
5919
6033
  const target = args._[2];
5920
- if (!target) {
5921
- process.stderr.write('agentic-security image scan: provide a path to a docker-archive or oci-archive tar (OCI tar / Docker tar — see commands/image.md for the two supported input shapes).\n');
6034
+ const registryRef = typeof args.flags.registry === 'string' ? args.flags.registry : null;
6035
+ const engine = typeof args.flags.engine === 'string' ? args.flags.engine : null;
6036
+ if (!target && !registryRef) {
6037
+ process.stderr.write('agentic-security image scan: provide a path to a docker-archive or oci-archive tar, or --registry <ref> for an explicit remote image, or <name> --engine docker|podman for a local image (see commands/image.md).\n');
5922
6038
  return 2;
5923
6039
  }
5924
6040
  const format = args.flags.format ?? 'json';
@@ -5926,14 +6042,50 @@ async function cmdImageScan(args) {
5926
6042
  process.stderr.write('agentic-security image scan: --format must be json|cli.\n');
5927
6043
  return 2;
5928
6044
  }
5929
- const read = _readFileOrError(target);
5930
- if (!read.ok) { process.stderr.write(`agentic-security image scan: ${read.message}\n`); return 1; }
6045
+ let buf; let source = { kind: 'archive' };
6046
+ if (registryRef) {
6047
+ // An explicit remote reference only: nothing is fetched unless the operator named the image. HTTPS only (plain HTTP just for a
6048
+ // loopback registry the operator allows); every blob digest-verified; credentials from the environment or the Docker config.
6049
+ const { fetchImage, RegistryError } = await import('../src/artifact/registry.js');
6050
+ try {
6051
+ const got = await fetchImage(registryRef, { platform: typeof args.flags.platform === 'string' ? args.flags.platform : 'linux/amd64', allowLoopbackHttp: !!args.flags['allow-http-loopback'] });
6052
+ buf = got.tar; source = got.source;
6053
+ } catch (err) {
6054
+ if (err instanceof RegistryError) { process.stderr.write(`agentic-security image scan: ${err.message} (${err.code})\n`); return 1; }
6055
+ throw err;
6056
+ }
6057
+ } else if (engine) {
6058
+ const { saveFromEngine, EngineError } = await import('../src/artifact/engine-source.js');
6059
+ try { const got = await saveFromEngine(target, { runtime: engine }); buf = got.tar; source = got.source; } catch (err) {
6060
+ if (err instanceof EngineError) { process.stderr.write(`agentic-security image scan: ${err.message} (${err.code})\n`); return 1; }
6061
+ throw err;
6062
+ }
6063
+ } else {
6064
+ const read = _readFileOrError(target);
6065
+ if (!read.ok) { process.stderr.write(`agentic-security image scan: ${read.message}\n`); return 1; }
6066
+ buf = read.buf;
6067
+ }
5931
6068
 
5932
- const scanned = await _scanArchiveFresh(read.buf);
6069
+ const scanned = await _scanArchiveFresh(buf);
5933
6070
  if (!scanned.ok) { process.stderr.write(`agentic-security image scan: ${scanned.message}\n`); return 1; }
6071
+ scanned.record.source = source;
5934
6072
 
5935
6073
  await _persistArtifactScan(scanned.record);
5936
6074
 
6075
+ if (args.flags.attest) {
6076
+ const { buildArtifactEvidenceBundle, signArtifactEvidenceBundle, ensureKeyPair } = await import('../src/artifact/artifact-evidence.js');
6077
+ const { statePath, safeWriteState, ensureStateDir } = await import('../src/posture/state-dir.js');
6078
+ const bundle = buildArtifactEvidenceBundle(scanned.record, { engineVersion: SCANNER_VERSION_FOR_ATTEST(), source });
6079
+ const keys = ensureKeyPair();
6080
+ const signed = signArtifactEvidenceBundle(bundle, keys.privateKeyPem);
6081
+ const hex = _artifactScanDigestHex(scanned.record.inventory.digest);
6082
+ if (ensureStateDir(process.cwd()) && hex) {
6083
+ const file = statePath(process.cwd(), 'attestations', `artifact-${hex.slice(0, 16)}.json`);
6084
+ if (safeWriteState(file, JSON.stringify(signed, null, 2))) process.stderr.write(`agentic-security image scan: signed evidence written to ${path.relative(process.cwd(), file)}\n`);
6085
+ else process.stderr.write('agentic-security image scan: --attest could not write the signed bundle (state writes are disabled or this directory has no project marker); nothing was signed to disk.\n');
6086
+ } else process.stderr.write('agentic-security image scan: --attest could not write the signed bundle (no usable .agentic-security directory here); nothing was signed to disk.\n');
6087
+ }
6088
+
5937
6089
  if (format === 'json') {
5938
6090
  process.stdout.write(JSON.stringify(scanned.record, null, 2) + '\n');
5939
6091
  return 0;
@@ -5942,6 +6094,7 @@ async function cmdImageScan(args) {
5942
6094
  const vulnCount = vulnerabilities.filter((v) => v.type === 'vulnerable_dep').length;
5943
6095
  process.stdout.write([
5944
6096
  `Image digest: ${inventory.digest} (${inventory.digestKind})`,
6097
+ source.kind === 'registry' ? `Source: ${source.registry}/${source.repository}:${source.reference}${source.platform ? ` (${source.platform})` : ''}, ${source.bytes} bytes fetched${source.authenticated ? ', authenticated' : ''}` : source.kind === 'engine' ? `Source: local ${source.runtime} image ${source.reference}` : '',
5945
6098
  `OS/architecture: ${inventory.os}/${inventory.architecture}`,
5946
6099
  `OS packages: ${inventory.osPackages.length}, application packages: ${inventory.appPackages.length}`,
5947
6100
  `Vulnerabilities (OSV): ${vulnCount}`,
@@ -5985,7 +6138,15 @@ async function cmdImageVerify(args) {
5985
6138
  }
5986
6139
  }
5987
6140
 
5988
- const signatureResult = verifyImageSignature({ digest: inventory.digest, signature, publicKeyPem });
6141
+ let signatureResult = verifyImageSignature({ digest: inventory.digest, signature, publicKeyPem });
6142
+ if (args.flags.cosign) {
6143
+ // delegated to cosign: bound to this image's digest, and never `verified` unless cosign says so for exactly this digest
6144
+ const { verifyWithCosign } = await import('../src/artifact/cosign.js');
6145
+ const repo = typeof args.flags.repository === 'string' ? args.flags.repository : (resolved.record.source?.kind === 'registry' ? `${resolved.record.source.registry}/${resolved.record.source.repository}` : null);
6146
+ signatureResult = repo
6147
+ ? await verifyWithCosign({ repository: repo, digest: inventory.digest, publicKeyPem, certificateIdentity: args.flags['certificate-identity'], certificateOidcIssuer: args.flags['certificate-oidc-issuer'] })
6148
+ : { state: 'unavailable', via: 'cosign', reason: 'cosign verifies against a registry: give --repository <registry/repo>, or scan with --registry so the repository is known' };
6149
+ }
5989
6150
  const attestationResult = verifyProvenanceAttestation({ digest: inventory.digest, envelope: attestation, publicKeyPem });
5990
6151
 
5991
6152
  const result = { digest: inventory.digest, signature: signatureResult, attestation: attestationResult };
@@ -7659,6 +7820,16 @@ async function main() {
7659
7820
  case 'accept': process.exit(await cmdAccept(args));
7660
7821
  case 'profile': process.exit(await cmdProfile(args));
7661
7822
  case 'proof': process.exit(await (await import('../src/posture/proof-fabric/cli.js')).cmdProof(args, path.resolve(args.flags.root || '.')));
7823
+ case 'labs': {
7824
+ // PRD 3 names `labs synthesize-rule FINDING-ID`. It is the rule compiler's synthesis step (`rules synthesize`). The older
7825
+ // slash-command mode `/labs --synthesize-rule` (LLM-assisted detector-spec extraction for a CWE) is a different feature
7826
+ // and is not reachable from this subcommand.
7827
+ if (args._[1] === 'synthesize-rule' && args._[2]) { args._ = ['rules', 'synthesize', args._[2]]; process.exit(await cmdRules(args)); }
7828
+ process.stderr.write('agentic-security labs: usage: labs synthesize-rule <finding-id> --spec <file.json> [--auto]\n(the other labs modes are slash-command modes: /labs --claude-audit, --model-rescan, ...)\n');
7829
+ process.exit(2);
7830
+ break;
7831
+ }
7832
+ case 'dnr': process.exit(await (await import('../src/posture/dnr/cli.js')).cmdDnr(args, path.resolve(args.flags.root || '.')));
7662
7833
  case 'triage': process.exit(await cmdTriage(args));
7663
7834
  case 'org-scan': process.exit(await cmdOrgScan(args));
7664
7835
  case 'rules': process.exit(await cmdRules(args));
@@ -7668,7 +7839,12 @@ async function main() {
7668
7839
  case 'secure': process.exit(await cmdSecure(args));
7669
7840
  case 'packs': process.exit(await cmdPacks(args));
7670
7841
  case 'validator-cache': process.exit(await cmdValidatorCache(args));
7671
- case 'models': process.exit(await cmdModels(args));
7842
+ case 'models': {
7843
+ // `models benchmark` and `models route explain` belong to the Eval Lab and the policy router (PRD 6); the rest stays the local-model UX.
7844
+ if (args._[1] === 'benchmark' || args._[1] === 'route') process.exit(await (await import('../src/posture/evaluation/lab/cli.js')).cmdModelsLab(args, path.resolve(args.flags.root || '.')));
7845
+ process.exit(await cmdModels(args));
7846
+ }
7847
+ case 'eval': process.exit(await (await import('../src/posture/evaluation/lab/cli.js')).cmdEval(args, path.resolve(args.flags.root || '.')));
7672
7848
  case 'ask': process.exit(await cmdAsk(args));
7673
7849
  case 'verify': process.exit(await cmdVerify(args));
7674
7850
  case 'reset': process.exit(await cmdReset(args));
@@ -7850,8 +8026,10 @@ async function main() {
7850
8026
  else if (sub === 'status') { process.exit(await cmdRuntimeStatus(args)); }
7851
8027
  else if (sub === 'reconcile') { process.exit(await cmdRuntimeReconcile(args)); }
7852
8028
  else if (sub === 'query') { process.exit(await cmdRuntimeQuery(args)); }
8029
+ else if (sub === 'edges') { process.exit(await cmdRuntimeEdges(args)); }
8030
+ else if (sub === 'explore') { process.exit(await cmdRuntimeExplorer(args, args._[2] || '.')); }
7853
8031
  else {
7854
- process.stderr.write(`agentic-security runtime: unrecognized sub-command "${sub}" — must be "ingest", "status", "reconcile", or "query".\n`);
8032
+ process.stderr.write(`agentic-security runtime: unrecognized sub-command "${sub}" — must be "ingest", "status", "reconcile", "query", "edges", or "explore".\n`);
7855
8033
  process.exit(2);
7856
8034
  }
7857
8035
  break;
@@ -16,7 +16,7 @@ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependen
16
16
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(76760);
17
17
  /* harmony import */ var node_readline__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(80481);
18
18
  /* harmony import */ var _runScan_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(45950);
19
- /* harmony import */ var _posture_custom_rules_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(9049);
19
+ /* harmony import */ var _posture_custom_rules_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(69400);
20
20
  /* harmony import */ var _posture_state_dir_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(31174);
21
21
  /* harmony import */ var _mcp_redact_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(83468);
22
22
  /* harmony import */ var _report_index_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(75917);