@clear-capabilities/agentic-security-scanner 0.175.0 → 0.176.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 (101) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/agentic-security.js +199 -28
  3. package/dist/2066.index.js +850 -0
  4. package/dist/2199.index.js +1 -1
  5. package/dist/2584.index.js +3332 -0
  6. package/dist/2923.index.js +13 -0
  7. package/dist/2946.index.js +81 -0
  8. package/dist/3196.index.js +27 -0
  9. package/dist/3727.index.js +292 -0
  10. package/dist/4168.index.js +509 -0
  11. package/dist/4281.index.js +993 -0
  12. package/dist/4978bec8fdf5c733dc85.json +283 -0
  13. package/dist/5099.index.js +96 -0
  14. package/dist/5616.index.js +1471 -0
  15. package/dist/5620.index.js +2370 -0
  16. package/dist/5817.index.js +714 -0
  17. package/dist/6333.index.js +98 -0
  18. package/dist/6655.index.js +3221 -0
  19. package/dist/7838.index.js +446 -974
  20. package/dist/818.index.js +4 -3
  21. package/dist/83.index.js +159 -0
  22. package/dist/8551.index.js +82 -0
  23. package/dist/8633.index.js +93 -0
  24. package/dist/8714.index.js +76 -76
  25. package/dist/8825.index.js +84 -0
  26. package/dist/932.index.js +186 -0
  27. package/dist/9560.index.js +358 -19
  28. package/dist/9800.index.js +6 -1
  29. package/dist/agentic-security.mjs +10 -10
  30. package/dist/agentic-security.mjs.sha256 +1 -1
  31. package/dist/df1e8683e7eda7b2b79c.js +29 -0
  32. package/package.json +5 -4
  33. package/src/artifact/CLAUDE.md +4 -0
  34. package/src/artifact/artifact-evidence.js +63 -0
  35. package/src/artifact/cosign.js +61 -0
  36. package/src/artifact/engine-source.js +72 -0
  37. package/src/artifact/oci-layout.js +47 -0
  38. package/src/artifact/registry.js +213 -0
  39. package/src/capabilities/tool-registry.js +2 -0
  40. package/src/lineage/CLAUDE.md +4 -0
  41. package/src/lineage/identity-token.js +11 -0
  42. package/src/lineage/observation-adapters.js +13 -0
  43. package/src/lineage/runtime-edge-states.js +139 -0
  44. package/src/lineage/runtime-explorer-view.js +82 -0
  45. package/src/lineage/runtime-observation.js +11 -0
  46. package/src/mcp/CLAUDE.md +3 -1
  47. package/src/mcp/proof-tools.js +132 -0
  48. package/src/mcp/server.js +1 -1
  49. package/src/mcp/tools.js +3 -1
  50. package/src/posture/CLAUDE.md +23 -0
  51. package/src/posture/artifact-registry.js +4 -0
  52. package/src/posture/dnr/attack-facts.js +31 -0
  53. package/src/posture/dnr/cli.js +138 -0
  54. package/src/posture/dnr/detection-ir.js +123 -0
  55. package/src/posture/dnr/exposure-window.js +134 -0
  56. package/src/posture/dnr/hunt.js +84 -0
  57. package/src/posture/dnr/log-reader.js +189 -0
  58. package/src/posture/dnr/matchers.js +90 -0
  59. package/src/posture/dnr/outputs.js +262 -0
  60. package/src/posture/dnr/payload-classes.js +120 -0
  61. package/src/posture/dnr/redact.js +25 -0
  62. package/src/posture/dnr/store.js +57 -0
  63. package/src/posture/dnr/validate.js +64 -0
  64. package/src/posture/dnr/view.js +51 -0
  65. package/src/posture/evaluation/lab/arm-ollama.js +88 -0
  66. package/src/posture/evaluation/lab/arms.js +142 -0
  67. package/src/posture/evaluation/lab/cli.js +135 -0
  68. package/src/posture/evaluation/lab/dataset.js +139 -0
  69. package/src/posture/evaluation/lab/materialize.js +44 -0
  70. package/src/posture/evaluation/lab/neutralize.js +144 -0
  71. package/src/posture/evaluation/lab/report.js +176 -0
  72. package/src/posture/evaluation/lab/run.js +225 -0
  73. package/src/posture/evaluation/lab/score.js +135 -0
  74. package/src/posture/evaluation/lab/scoreboard.js +48 -0
  75. package/src/posture/evaluation/lab/stats.js +41 -0
  76. package/src/posture/evaluation/lab/store.js +53 -0
  77. package/src/posture/fleet/cli.js +248 -63
  78. package/src/posture/fleet/discover-edges.js +135 -0
  79. package/src/posture/fleet/enterprise-graph.js +148 -0
  80. package/src/posture/fleet/fleet-graph.js +9 -4
  81. package/src/posture/fleet/fleet-manifest.js +50 -5
  82. package/src/posture/fleet/job-runner.js +153 -0
  83. package/src/posture/fleet/job-store.js +136 -0
  84. package/src/posture/fleet/org-health.js +56 -0
  85. package/src/posture/fleet/scan-cache.js +112 -0
  86. package/src/posture/fleet/scan-child.js +29 -0
  87. package/src/posture/proof-fabric/cli.js +34 -6
  88. package/src/posture/proof-fabric/container.js +57 -0
  89. package/src/posture/proof-fabric/fix-proof.js +2 -1
  90. package/src/posture/proof-fabric/generate-python.js +129 -0
  91. package/src/posture/proof-fabric/generate.js +213 -0
  92. package/src/posture/proof-fabric/reproducer.js +3 -0
  93. package/src/posture/proof-fabric/runner.js +20 -11
  94. package/src/posture/proof-fabric/siblings.js +77 -0
  95. package/src/posture/routing/policy-router.js +103 -0
  96. package/src/posture/rule-compiler/cli.js +64 -6
  97. package/src/posture/rule-compiler/corpus.js +10 -2
  98. package/src/posture/rule-compiler/mutation.js +72 -1
  99. package/src/posture/rule-compiler/promotion.js +21 -5
  100. package/src/posture/rule-compiler/proposal.js +132 -0
  101. package/src/posture/rule-compiler/store.js +16 -0
package/CHANGELOG.md CHANGED
@@ -9,6 +9,22 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.176.0 - Moat PRDs: proof, rules, fleet, runtime twin, artifacts, eval lab, detect and respond
13
+
14
+ - **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.
15
+
16
+ - **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.
17
+
18
+ - **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.
19
+
20
+ - **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.
21
+ - **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.
22
+ - **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.
23
+ - **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.
24
+ - **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`.
25
+ - **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.
26
+ - **`fix --require-proof` records its result** so the finding view can show the fix and the post-fix replay.
27
+
12
28
  ## 0.175.0 - More NixOS and Haskell detection
13
29
 
14
30
  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') {
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,108 @@ 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
+ process.stdout.write(JSON.stringify({ query: name, supported: true, ...vulnerableFunctionsInProduction(scan.findings) }, null, 2) + '\n');
5867
+ return 0;
5868
+ }
5869
+ if (name && Object.prototype.hasOwnProperty.call(_RUNTIME_EXTRA_QUERIES, name) && !args.flags.list) {
5870
+ const targetAbs = path.resolve(args._[3] || '.');
5871
+ const loaded = await _loadRuntimeInputs(targetAbs);
5872
+ if (!loaded.ok) { process.stderr.write(`agentic-security runtime query: ${loaded.message}\n`); return 1; }
5873
+ const environment = typeof args.flags.environment === 'string' ? args.flags.environment : null;
5874
+ const { correlateObservations } = await import('../src/lineage/observation-correlation.js');
5875
+ const mod = await import('../src/lineage/runtime-edge-states.js');
5876
+ const correlation = correlateObservations(loaded.graph, loaded.observations, { environment, windowStart: null, windowEnd: null });
5877
+ 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; }
5878
+ if (name === 'runtime-flows-absent-from-static') {
5879
+ process.stdout.write(JSON.stringify({ query: name, supported: true, evaluated: true, ...mod.runtimeFlowsAbsentFromStatic(loaded.observations, correlation) }, null, 2) + '\n');
5880
+ return 0;
5881
+ }
5882
+ const ev = await _loadExploitEvidence(args.flags['exploit-evidence']);
5883
+ if (ev.errors.length) { process.stderr.write(`agentic-security runtime query: ${ev.errors.join('; ')}\n`); return 2; }
5884
+ const states = mod.deriveEdgeStates(loaded.graph, correlation, loaded.observations, { exploitEvidence: ev.evidence });
5885
+ process.stdout.write(JSON.stringify({ query: name, supported: true, evaluated: true, ...mod.identitiesUsingRiskyPaths(states) }, null, 2) + '\n');
5886
+ return 0;
5887
+ }
5784
5888
  if (!name || args.flags.list) {
5785
5889
  process.stdout.write('Supported queries:\n');
5786
5890
  for (const [k, v] of Object.entries(_RUNTIME_QUERIES)) process.stdout.write(` ${k} — ${v.description}\n`);
5891
+ for (const [k, v] of Object.entries(_RUNTIME_EXTRA_QUERIES)) process.stdout.write(` ${k} — ${v}\n`);
5787
5892
  process.stdout.write('\nNamed but NOT supported by this architecture (disclosed, not silently omitted):\n');
5788
5893
  for (const [k, v] of Object.entries(_RUNTIME_QUERIES_NOT_SUPPORTED)) process.stdout.write(` ${k} — ${v}\n`);
5789
5894
  // --list is always a successful, intentional operation (exit 0);
@@ -5915,10 +6020,14 @@ async function _resolveArtifactScan(target) {
5915
6020
  return { ok: true, record: scanned.record };
5916
6021
  }
5917
6022
 
6023
+ const SCANNER_VERSION_FOR_ATTEST = () => { try { return JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version; } catch { return null; } };
6024
+
5918
6025
  async function cmdImageScan(args) {
5919
6026
  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');
6027
+ const registryRef = typeof args.flags.registry === 'string' ? args.flags.registry : null;
6028
+ const engine = typeof args.flags.engine === 'string' ? args.flags.engine : null;
6029
+ if (!target && !registryRef) {
6030
+ 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
6031
  return 2;
5923
6032
  }
5924
6033
  const format = args.flags.format ?? 'json';
@@ -5926,14 +6035,50 @@ async function cmdImageScan(args) {
5926
6035
  process.stderr.write('agentic-security image scan: --format must be json|cli.\n');
5927
6036
  return 2;
5928
6037
  }
5929
- const read = _readFileOrError(target);
5930
- if (!read.ok) { process.stderr.write(`agentic-security image scan: ${read.message}\n`); return 1; }
6038
+ let buf; let source = { kind: 'archive' };
6039
+ if (registryRef) {
6040
+ // An explicit remote reference only: nothing is fetched unless the operator named the image. HTTPS only (plain HTTP just for a
6041
+ // loopback registry the operator allows); every blob digest-verified; credentials from the environment or the Docker config.
6042
+ const { fetchImage, RegistryError } = await import('../src/artifact/registry.js');
6043
+ try {
6044
+ const got = await fetchImage(registryRef, { platform: typeof args.flags.platform === 'string' ? args.flags.platform : 'linux/amd64', allowLoopbackHttp: !!args.flags['allow-http-loopback'] });
6045
+ buf = got.tar; source = got.source;
6046
+ } catch (err) {
6047
+ if (err instanceof RegistryError) { process.stderr.write(`agentic-security image scan: ${err.message} (${err.code})\n`); return 1; }
6048
+ throw err;
6049
+ }
6050
+ } else if (engine) {
6051
+ const { saveFromEngine, EngineError } = await import('../src/artifact/engine-source.js');
6052
+ try { const got = await saveFromEngine(target, { runtime: engine }); buf = got.tar; source = got.source; } catch (err) {
6053
+ if (err instanceof EngineError) { process.stderr.write(`agentic-security image scan: ${err.message} (${err.code})\n`); return 1; }
6054
+ throw err;
6055
+ }
6056
+ } else {
6057
+ const read = _readFileOrError(target);
6058
+ if (!read.ok) { process.stderr.write(`agentic-security image scan: ${read.message}\n`); return 1; }
6059
+ buf = read.buf;
6060
+ }
5931
6061
 
5932
- const scanned = await _scanArchiveFresh(read.buf);
6062
+ const scanned = await _scanArchiveFresh(buf);
5933
6063
  if (!scanned.ok) { process.stderr.write(`agentic-security image scan: ${scanned.message}\n`); return 1; }
6064
+ scanned.record.source = source;
5934
6065
 
5935
6066
  await _persistArtifactScan(scanned.record);
5936
6067
 
6068
+ if (args.flags.attest) {
6069
+ const { buildArtifactEvidenceBundle, signArtifactEvidenceBundle, ensureKeyPair } = await import('../src/artifact/artifact-evidence.js');
6070
+ const { statePath, safeWriteState, ensureStateDir } = await import('../src/posture/state-dir.js');
6071
+ const bundle = buildArtifactEvidenceBundle(scanned.record, { engineVersion: SCANNER_VERSION_FOR_ATTEST(), source });
6072
+ const keys = ensureKeyPair();
6073
+ const signed = signArtifactEvidenceBundle(bundle, keys.privateKeyPem);
6074
+ const hex = _artifactScanDigestHex(scanned.record.inventory.digest);
6075
+ if (ensureStateDir(process.cwd()) && hex) {
6076
+ const file = statePath(process.cwd(), 'attestations', `artifact-${hex.slice(0, 16)}.json`);
6077
+ 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`);
6078
+ 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');
6079
+ } 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');
6080
+ }
6081
+
5937
6082
  if (format === 'json') {
5938
6083
  process.stdout.write(JSON.stringify(scanned.record, null, 2) + '\n');
5939
6084
  return 0;
@@ -5942,6 +6087,7 @@ async function cmdImageScan(args) {
5942
6087
  const vulnCount = vulnerabilities.filter((v) => v.type === 'vulnerable_dep').length;
5943
6088
  process.stdout.write([
5944
6089
  `Image digest: ${inventory.digest} (${inventory.digestKind})`,
6090
+ 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
6091
  `OS/architecture: ${inventory.os}/${inventory.architecture}`,
5946
6092
  `OS packages: ${inventory.osPackages.length}, application packages: ${inventory.appPackages.length}`,
5947
6093
  `Vulnerabilities (OSV): ${vulnCount}`,
@@ -5985,7 +6131,15 @@ async function cmdImageVerify(args) {
5985
6131
  }
5986
6132
  }
5987
6133
 
5988
- const signatureResult = verifyImageSignature({ digest: inventory.digest, signature, publicKeyPem });
6134
+ let signatureResult = verifyImageSignature({ digest: inventory.digest, signature, publicKeyPem });
6135
+ if (args.flags.cosign) {
6136
+ // delegated to cosign: bound to this image's digest, and never `verified` unless cosign says so for exactly this digest
6137
+ const { verifyWithCosign } = await import('../src/artifact/cosign.js');
6138
+ const repo = typeof args.flags.repository === 'string' ? args.flags.repository : (resolved.record.source?.kind === 'registry' ? `${resolved.record.source.registry}/${resolved.record.source.repository}` : null);
6139
+ signatureResult = repo
6140
+ ? await verifyWithCosign({ repository: repo, digest: inventory.digest, publicKeyPem, certificateIdentity: args.flags['certificate-identity'], certificateOidcIssuer: args.flags['certificate-oidc-issuer'] })
6141
+ : { state: 'unavailable', via: 'cosign', reason: 'cosign verifies against a registry: give --repository <registry/repo>, or scan with --registry so the repository is known' };
6142
+ }
5989
6143
  const attestationResult = verifyProvenanceAttestation({ digest: inventory.digest, envelope: attestation, publicKeyPem });
5990
6144
 
5991
6145
  const result = { digest: inventory.digest, signature: signatureResult, attestation: attestationResult };
@@ -7659,6 +7813,16 @@ async function main() {
7659
7813
  case 'accept': process.exit(await cmdAccept(args));
7660
7814
  case 'profile': process.exit(await cmdProfile(args));
7661
7815
  case 'proof': process.exit(await (await import('../src/posture/proof-fabric/cli.js')).cmdProof(args, path.resolve(args.flags.root || '.')));
7816
+ case 'labs': {
7817
+ // PRD 3 names `labs synthesize-rule FINDING-ID`. It is the rule compiler's synthesis step (`rules synthesize`). The older
7818
+ // slash-command mode `/labs --synthesize-rule` (LLM-assisted detector-spec extraction for a CWE) is a different feature
7819
+ // and is not reachable from this subcommand.
7820
+ if (args._[1] === 'synthesize-rule' && args._[2]) { args._ = ['rules', 'synthesize', args._[2]]; process.exit(await cmdRules(args)); }
7821
+ 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');
7822
+ process.exit(2);
7823
+ break;
7824
+ }
7825
+ case 'dnr': process.exit(await (await import('../src/posture/dnr/cli.js')).cmdDnr(args, path.resolve(args.flags.root || '.')));
7662
7826
  case 'triage': process.exit(await cmdTriage(args));
7663
7827
  case 'org-scan': process.exit(await cmdOrgScan(args));
7664
7828
  case 'rules': process.exit(await cmdRules(args));
@@ -7668,7 +7832,12 @@ async function main() {
7668
7832
  case 'secure': process.exit(await cmdSecure(args));
7669
7833
  case 'packs': process.exit(await cmdPacks(args));
7670
7834
  case 'validator-cache': process.exit(await cmdValidatorCache(args));
7671
- case 'models': process.exit(await cmdModels(args));
7835
+ case 'models': {
7836
+ // `models benchmark` and `models route explain` belong to the Eval Lab and the policy router (PRD 6); the rest stays the local-model UX.
7837
+ 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 || '.')));
7838
+ process.exit(await cmdModels(args));
7839
+ }
7840
+ case 'eval': process.exit(await (await import('../src/posture/evaluation/lab/cli.js')).cmdEval(args, path.resolve(args.flags.root || '.')));
7672
7841
  case 'ask': process.exit(await cmdAsk(args));
7673
7842
  case 'verify': process.exit(await cmdVerify(args));
7674
7843
  case 'reset': process.exit(await cmdReset(args));
@@ -7850,8 +8019,10 @@ async function main() {
7850
8019
  else if (sub === 'status') { process.exit(await cmdRuntimeStatus(args)); }
7851
8020
  else if (sub === 'reconcile') { process.exit(await cmdRuntimeReconcile(args)); }
7852
8021
  else if (sub === 'query') { process.exit(await cmdRuntimeQuery(args)); }
8022
+ else if (sub === 'edges') { process.exit(await cmdRuntimeEdges(args)); }
8023
+ else if (sub === 'explore') { process.exit(await cmdRuntimeExplorer(args, args._[2] || '.')); }
7853
8024
  else {
7854
- process.stderr.write(`agentic-security runtime: unrecognized sub-command "${sub}" — must be "ingest", "status", "reconcile", or "query".\n`);
8025
+ process.stderr.write(`agentic-security runtime: unrecognized sub-command "${sub}" — must be "ingest", "status", "reconcile", "query", "edges", or "explore".\n`);
7855
8026
  process.exit(2);
7856
8027
  }
7857
8028
  break;