@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.
- package/CHANGELOG.md +16 -0
- package/bin/agentic-security.js +199 -28
- package/dist/2066.index.js +850 -0
- package/dist/2199.index.js +1 -1
- package/dist/2584.index.js +3332 -0
- package/dist/2923.index.js +13 -0
- package/dist/2946.index.js +81 -0
- package/dist/3196.index.js +27 -0
- package/dist/3727.index.js +292 -0
- package/dist/4168.index.js +509 -0
- package/dist/4281.index.js +993 -0
- package/dist/4978bec8fdf5c733dc85.json +283 -0
- package/dist/5099.index.js +96 -0
- package/dist/5616.index.js +1471 -0
- package/dist/5620.index.js +2370 -0
- package/dist/5817.index.js +714 -0
- package/dist/6333.index.js +98 -0
- package/dist/6655.index.js +3221 -0
- package/dist/7838.index.js +446 -974
- package/dist/818.index.js +4 -3
- package/dist/83.index.js +159 -0
- package/dist/8551.index.js +82 -0
- package/dist/8633.index.js +93 -0
- package/dist/8714.index.js +76 -76
- package/dist/8825.index.js +84 -0
- package/dist/932.index.js +186 -0
- package/dist/9560.index.js +358 -19
- package/dist/9800.index.js +6 -1
- package/dist/agentic-security.mjs +10 -10
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/df1e8683e7eda7b2b79c.js +29 -0
- package/package.json +5 -4
- package/src/artifact/CLAUDE.md +4 -0
- package/src/artifact/artifact-evidence.js +63 -0
- package/src/artifact/cosign.js +61 -0
- package/src/artifact/engine-source.js +72 -0
- package/src/artifact/oci-layout.js +47 -0
- package/src/artifact/registry.js +213 -0
- package/src/capabilities/tool-registry.js +2 -0
- package/src/lineage/CLAUDE.md +4 -0
- package/src/lineage/identity-token.js +11 -0
- package/src/lineage/observation-adapters.js +13 -0
- package/src/lineage/runtime-edge-states.js +139 -0
- package/src/lineage/runtime-explorer-view.js +82 -0
- package/src/lineage/runtime-observation.js +11 -0
- package/src/mcp/CLAUDE.md +3 -1
- package/src/mcp/proof-tools.js +132 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +3 -1
- package/src/posture/CLAUDE.md +23 -0
- package/src/posture/artifact-registry.js +4 -0
- package/src/posture/dnr/attack-facts.js +31 -0
- package/src/posture/dnr/cli.js +138 -0
- package/src/posture/dnr/detection-ir.js +123 -0
- package/src/posture/dnr/exposure-window.js +134 -0
- package/src/posture/dnr/hunt.js +84 -0
- package/src/posture/dnr/log-reader.js +189 -0
- package/src/posture/dnr/matchers.js +90 -0
- package/src/posture/dnr/outputs.js +262 -0
- package/src/posture/dnr/payload-classes.js +120 -0
- package/src/posture/dnr/redact.js +25 -0
- package/src/posture/dnr/store.js +57 -0
- package/src/posture/dnr/validate.js +64 -0
- package/src/posture/dnr/view.js +51 -0
- package/src/posture/evaluation/lab/arm-ollama.js +88 -0
- package/src/posture/evaluation/lab/arms.js +142 -0
- package/src/posture/evaluation/lab/cli.js +135 -0
- package/src/posture/evaluation/lab/dataset.js +139 -0
- package/src/posture/evaluation/lab/materialize.js +44 -0
- package/src/posture/evaluation/lab/neutralize.js +144 -0
- package/src/posture/evaluation/lab/report.js +176 -0
- package/src/posture/evaluation/lab/run.js +225 -0
- package/src/posture/evaluation/lab/score.js +135 -0
- package/src/posture/evaluation/lab/scoreboard.js +48 -0
- package/src/posture/evaluation/lab/stats.js +41 -0
- package/src/posture/evaluation/lab/store.js +53 -0
- package/src/posture/fleet/cli.js +248 -63
- package/src/posture/fleet/discover-edges.js +135 -0
- package/src/posture/fleet/enterprise-graph.js +148 -0
- package/src/posture/fleet/fleet-graph.js +9 -4
- package/src/posture/fleet/fleet-manifest.js +50 -5
- package/src/posture/fleet/job-runner.js +153 -0
- package/src/posture/fleet/job-store.js +136 -0
- package/src/posture/fleet/org-health.js +56 -0
- package/src/posture/fleet/scan-cache.js +112 -0
- package/src/posture/fleet/scan-child.js +29 -0
- package/src/posture/proof-fabric/cli.js +34 -6
- package/src/posture/proof-fabric/container.js +57 -0
- package/src/posture/proof-fabric/fix-proof.js +2 -1
- package/src/posture/proof-fabric/generate-python.js +129 -0
- package/src/posture/proof-fabric/generate.js +213 -0
- package/src/posture/proof-fabric/reproducer.js +3 -0
- package/src/posture/proof-fabric/runner.js +20 -11
- package/src/posture/proof-fabric/siblings.js +77 -0
- package/src/posture/routing/policy-router.js +103 -0
- package/src/posture/rule-compiler/cli.js +64 -6
- package/src/posture/rule-compiler/corpus.js +10 -2
- package/src/posture/rule-compiler/mutation.js +72 -1
- package/src/posture/rule-compiler/promotion.js +21 -5
- package/src/posture/rule-compiler/proposal.js +132 -0
- 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.
|
package/bin/agentic-security.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
3691
|
-
const
|
|
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
|
-
//
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
//
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
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
|
-
|
|
5921
|
-
|
|
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
|
-
|
|
5930
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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':
|
|
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 "
|
|
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;
|