@clear-capabilities/agentic-security-scanner 0.168.0 → 0.170.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 +74 -0
- package/bin/agentic-security.js +567 -0
- package/dist/2199.index.js +1 -1
- package/dist/2675.index.js +174 -0
- package/dist/2923.index.js +369 -1
- package/dist/4142.index.js +6 -6
- package/dist/4700.index.js +151 -0
- package/dist/5014.index.js +2 -2
- package/dist/5086.index.js +645 -0
- package/dist/5144.index.js +2 -2
- package/dist/5206.index.js +32 -0
- package/dist/642.index.js +197 -0
- package/dist/6447.index.js +131 -0
- package/dist/6645.index.js +87 -0
- package/dist/6730.index.js +2 -2
- package/dist/6932.index.js +2 -2
- package/dist/7055.index.js +369 -0
- package/dist/7232.index.js +24 -0
- package/dist/7552.index.js +2 -2
- package/dist/7952.index.js +301 -0
- package/dist/8714.index.js +2 -2
- package/dist/9102.index.js +110 -0
- package/dist/9800.index.js +1 -1
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/artifact/CLAUDE.md +100 -0
- package/src/artifact/filesystem.js +100 -0
- package/src/artifact/image-archive.js +159 -0
- package/src/artifact/inventory.js +69 -0
- package/src/artifact/layer-secrets.js +92 -0
- package/src/artifact/mismatch.js +72 -0
- package/src/artifact/provenance-graph.js +115 -0
- package/src/artifact/safe-tar.js +175 -0
- package/src/artifact/signature.js +156 -0
- package/src/artifact/vex.js +94 -0
- package/src/artifact/vuln-correlate.js +17 -0
- package/src/lineage/CLAUDE.md +19 -0
- package/src/lineage/observation-adapters.js +368 -0
- package/src/lineage/runtime-assurance.js +135 -0
- package/src/lineage/runtime-observation.js +37 -4
- package/src/posture/artifact-registry.js +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,80 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.170.0 - source-to-artifact security & provenance: a new `src/artifact/` package, `image`/`provenance` CLI, VEX
|
|
13
|
+
|
|
14
|
+
Binds a finding to the actual **built artifact** (a container image, today) that ships it — closing the gap
|
|
15
|
+
between "this dependency has a CVE" and "this specific image digest actually ships it." A new, isolated
|
|
16
|
+
`scanner/src/artifact/` package reuses the existing SCA pipeline (manifest parsers, OS-package-DB parser,
|
|
17
|
+
OSV/KEV/EPSS correlation) rather than building a parallel one.
|
|
18
|
+
|
|
19
|
+
**OCI image scanning + inventory.** A hand-rolled, hardened tar/gzip reader (`safe-tar.js`) — hard caps on entry
|
|
20
|
+
count/size/archive size, a native decompression-bomb guard, and a path-safety check that flags (never silently
|
|
21
|
+
normalizes) a `..`/absolute tar entry path. Parses a `docker save`-shaped archive or an OCI-layout archive into
|
|
22
|
+
digest/OS/architecture/base-image/layers/entrypoint/build-labels, with OS and application packages extracted
|
|
23
|
+
through the exact same parsers every other SCA finding in this codebase uses. Never executes the image — no
|
|
24
|
+
`docker run`, no daemon required for the archive path.
|
|
25
|
+
|
|
26
|
+
**Layer secret analysis.** A secret added in one layer and deleted by a later overlay whiteout is still sitting
|
|
27
|
+
in that earlier layer's raw tar bytes — detected and reported (value always masked/redacted, the detector is
|
|
28
|
+
injected, same shape `posture/secret-history.js` already uses for the git-history analog).
|
|
29
|
+
|
|
30
|
+
**Signature + attestation verification**, states exactly `verified`/`invalid`/`missing`/`unavailable` — a
|
|
31
|
+
detached-signature check over the image digest, plus REAL DSSE (Pre-Authentication Encoding) verification for
|
|
32
|
+
in-toto/SLSA provenance attestations, so a real cosign-keyed attestation bundle verifies here given the matching
|
|
33
|
+
public key. Never treats missing material as verified.
|
|
34
|
+
|
|
35
|
+
**The provenance graph** (Git Commit → Build → OCI Digest → SBOM Component → Finding → Deployment) and
|
|
36
|
+
**source-vs-artifact mismatch detection** — the PRD's own "Killer Demonstration": a developer fixes a
|
|
37
|
+
vulnerability in source, but production is still running the old image. `SOURCE: FIXED` / `DEPLOYED ARTIFACT:
|
|
38
|
+
STILL VULNERABLE` / `PRODUCTION EXPOSURE: PRESENT`, proven end-to-end against the real built CLI. **VEX**
|
|
39
|
+
statements (affected/not affected/fixed/under investigation) — `not affected` only ever from positive evidence
|
|
40
|
+
(component absent, or confirmed unreachable), never from absence of information.
|
|
41
|
+
|
|
42
|
+
**CLI:** `agentic-security image scan/verify`, `agentic-security provenance trace`, `agentic-security supply vex`
|
|
43
|
+
— the PRD's own literal CLI shape. New slash commands `/image`, `/provenance`; `/supply` gained a `--vex` mode.
|
|
44
|
+
|
|
45
|
+
**Deliberately NOT done, disclosed:** a local Docker daemon image reference and a remote OCI registry reference
|
|
46
|
+
as `image scan` inputs (only local docker-archive/oci-archive tar files); keyless Rekor-transparency-log
|
|
47
|
+
signature verification. See `docs/implementation-status/source-to-artifact-provenance.md`.
|
|
48
|
+
|
|
49
|
+
## 0.169.0 - runtime-corroborated security twin: 4 new telemetry adapters, honest runtime assurance, a `runtime` CLI
|
|
50
|
+
|
|
51
|
+
Extends the existing Milestone 5 "Runtime-Corroborated Digital Twin" machinery (closed-world `RuntimeObservation`
|
|
52
|
+
contract, a 3-rung static-to-runtime match ladder, a non-mutating overlay) rather than building a second one —
|
|
53
|
+
closing the real gap that machinery had: only one hand-authored JSONL telemetry format could feed it.
|
|
54
|
+
|
|
55
|
+
**Four new telemetry adapters.** `otel-json` (OpenTelemetry JSON trace export, one draft per span), `envoy-access-log`
|
|
56
|
+
(Envoy's default JSON access log, including `response_flags` → an allow/deny security decision), `k8s-audit-log`
|
|
57
|
+
(Kubernetes `audit.k8s.io` JSONL — `objectRef`/`responseStatus.code`, deliberately never `user.username`), and a
|
|
58
|
+
generic JSON-lines `http-access-log`. Each is a pure, zero-fs parser reading only approved, non-payload metadata
|
|
59
|
+
(destination host/port/scheme, service name, protocol/TLS, store table, queue topic) and dropping everything else
|
|
60
|
+
(query strings, headers, usernames, bodies) — matching the existing contract's own reject-not-redact discipline.
|
|
61
|
+
One new, narrowly-scoped attribute key, `network.securityDecision` (a closed `allowed|denied` enum, not a free-form
|
|
62
|
+
identifier), carries the one new fact these formats need that the existing vocabulary didn't have a slot for.
|
|
63
|
+
|
|
64
|
+
**Honest runtime assurance.** A new `runtime-assurance.js` reports `runtimeStatus: complete|partial|unavailable` —
|
|
65
|
+
`unavailable` whenever no observation store was ever consulted, never a silent "no risk." `runtime status`,
|
|
66
|
+
`runtime reconcile` (diagnostic drift detection against the current graph, never auto-rewriting a stored
|
|
67
|
+
observation) and `runtime query` (two real graph-backed queries, plus honest, named disclosure of the three
|
|
68
|
+
PRD-example queries this architecture genuinely cannot answer) round out a new top-level `runtime` CLI dispatcher;
|
|
69
|
+
`runtime ingest` is a thin shim over the already-shipped `dataflow observations import` — zero duplicated logic.
|
|
70
|
+
|
|
71
|
+
**A real, grounded demonstration, not an asserted-away fixture.** A small app (`scanner/test/fixtures/runtime-twin/`)
|
|
72
|
+
proves all 5 of the PRD's named fixture properties in one scenario against the real built CLI: a database write
|
|
73
|
+
that executes and is observed allowed; an outbound call that never executes and is never observed; an unrecognized
|
|
74
|
+
host that IS observed but matches nothing in the static graph (reported `unmatched`, never dropped); a PII flow
|
|
75
|
+
crossing a trust boundary to a third party whose destination can never be runtime-corroborated by any adapter here
|
|
76
|
+
— a genuine, disclosed architectural limit; and a queue publish observed denied by authorization.
|
|
77
|
+
|
|
78
|
+
**What this deliberately does not do, disclosed rather than silently dropped.** No Data Flow Explorer UI/HTTP-server
|
|
79
|
+
wiring (`scanner/src/server/CLAUDE.md`'s own deferred list already named `runtime/` among the endpoints the
|
|
80
|
+
read-only local server does not serve — bolting one on here without that file's own scoping-doc-plus-test-suite
|
|
81
|
+
rigor would be a real regression). No authenticated-identity field (the existing contract omits it as a
|
|
82
|
+
payload-smuggling risk). No route/function-symbol match rungs or the 3 additional PRD-named edge states
|
|
83
|
+
(`RUNTIME_BLOCKED`/`RUNTIME_POLICY_VIOLATION`/`RUNTIME_EXPLOIT_CORRELATED`) — real, separately-scoped future work.
|
|
84
|
+
See `docs/implementation-status/runtime-corroborated-twin.md` for the full itemized status.
|
|
85
|
+
|
|
12
86
|
## 0.168.0 - fleet: multi-repository scan orchestration and cross-repository dependency blast radius
|
|
13
87
|
|
|
14
88
|
A real, honestly-scoped slice of the organization-wide security graph recommendation: a new CLI command group,
|
package/bin/agentic-security.js
CHANGED
|
@@ -5561,6 +5561,512 @@ async function cmdDataflowTwin(args) {
|
|
|
5561
5561
|
return 0;
|
|
5562
5562
|
}
|
|
5563
5563
|
|
|
5564
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
5565
|
+
// PRD "Runtime-Corroborated Security Twin" — the `runtime` dispatcher.
|
|
5566
|
+
//
|
|
5567
|
+
// `runtime ingest`/`runtime status`/`runtime reconcile`/`runtime query`
|
|
5568
|
+
// is the literal CLI shape that PRD names. It is NOT a parallel command
|
|
5569
|
+
// surface: `ingest` is a thin shim over the already-shipped
|
|
5570
|
+
// `dataflow observations import` (zero duplicated logic — it reshapes
|
|
5571
|
+
// `args._` and delegates); `status`/`reconcile`/`query` are genuinely new
|
|
5572
|
+
// (the assurance report and the reconciliation/query reports did not
|
|
5573
|
+
// exist before this PRD), but all three are built entirely on the
|
|
5574
|
+
// already-shipped `correlateObservations`/`matchObservationToGraph`/
|
|
5575
|
+
// `loadObservationImports` — no second correlation engine, no second
|
|
5576
|
+
// observation store.
|
|
5577
|
+
|
|
5578
|
+
async function cmdRuntimeIngest(args) {
|
|
5579
|
+
// args._ = ['runtime', 'ingest', <path>?]. cmdDataflowObservationsImport
|
|
5580
|
+
// reads args._[3] as its own path slot (its own dispatch assumes
|
|
5581
|
+
// ['dataflow', 'observations', 'import', <path>?]) — reshape rather
|
|
5582
|
+
// than duplicate its ~150 lines of validation/import logic.
|
|
5583
|
+
const target = args._[2] || '.';
|
|
5584
|
+
const shimmed = { ...args, _: ['dataflow', 'observations', 'import', target] };
|
|
5585
|
+
return cmdDataflowObservationsImport(shimmed);
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
async function _loadRuntimeInputs(targetAbs) {
|
|
5589
|
+
const { loadSignedGraph } = await import('../src/server/graph-loader.js');
|
|
5590
|
+
const loaded = loadSignedGraph(targetAbs);
|
|
5591
|
+
if (!loaded.ok) return { ok: false, message: loaded.message };
|
|
5592
|
+
|
|
5593
|
+
const dir = statePath(targetAbs, 'runtime-observations');
|
|
5594
|
+
let importRecords = null;
|
|
5595
|
+
let observations = null;
|
|
5596
|
+
if (fs.existsSync(dir)) {
|
|
5597
|
+
const store = await import('../src/lineage/observation-store.js');
|
|
5598
|
+
importRecords = store.loadObservationImports(targetAbs);
|
|
5599
|
+
observations = store.loadObservations(targetAbs);
|
|
5600
|
+
}
|
|
5601
|
+
return { ok: true, graph: loaded.graph, importRecords, observations };
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5604
|
+
async function cmdRuntimeStatus(args) {
|
|
5605
|
+
const target = args._[2] || '.';
|
|
5606
|
+
const targetAbs = path.resolve(target);
|
|
5607
|
+
const format = args.flags.format ?? 'json';
|
|
5608
|
+
if (format !== 'json' && format !== 'markdown') {
|
|
5609
|
+
process.stderr.write(`agentic-security runtime status: --format must be one of json|markdown (got ${JSON.stringify(format)}).\n`);
|
|
5610
|
+
return 2;
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5613
|
+
const loaded = await _loadRuntimeInputs(targetAbs);
|
|
5614
|
+
if (!loaded.ok) {
|
|
5615
|
+
process.stderr.write(`agentic-security runtime status: ${loaded.message}\n`);
|
|
5616
|
+
return 1;
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
const environment = typeof args.flags.environment === 'string' ? args.flags.environment : null;
|
|
5620
|
+
const windowStart = typeof args.flags['window-start'] === 'string' ? args.flags['window-start'] : null;
|
|
5621
|
+
const windowEnd = typeof args.flags['window-end'] === 'string' ? args.flags['window-end'] : null;
|
|
5622
|
+
|
|
5623
|
+
const { correlateObservations } = await import('../src/lineage/observation-correlation.js');
|
|
5624
|
+
const correlation = correlateObservations(loaded.graph, loaded.observations, { environment, windowStart, windowEnd });
|
|
5625
|
+
|
|
5626
|
+
const { computeRuntimeAssurance } = await import('../src/lineage/runtime-assurance.js');
|
|
5627
|
+
const assurance = computeRuntimeAssurance({ importRecords: loaded.importRecords, correlation });
|
|
5628
|
+
|
|
5629
|
+
if (format === 'json') {
|
|
5630
|
+
process.stdout.write(JSON.stringify(assurance, null, 2) + '\n');
|
|
5631
|
+
return 0;
|
|
5632
|
+
}
|
|
5633
|
+
|
|
5634
|
+
const lines = [];
|
|
5635
|
+
lines.push('# Runtime Assurance', '');
|
|
5636
|
+
lines.push(`**runtimeStatus:** \`${assurance.runtimeStatus}\``, '');
|
|
5637
|
+
lines.push(`- Telemetry window: ${assurance.telemetryWindow.start ?? '(none)'} .. ${assurance.telemetryWindow.end ?? '(none)'}`);
|
|
5638
|
+
lines.push(`- Last observed: ${assurance.freshness.lastObservedAt ?? '(none)'}${assurance.freshness.stale ? ' (STALE)' : ''}`);
|
|
5639
|
+
lines.push(`- Mapping: ${assurance.mappingPercentage === null ? '(no observations)' : `${assurance.mappingPercentage}%`} (${assurance.unmatchedObservationCount} of ${assurance.totalObservationCount} unmatched)`);
|
|
5640
|
+
lines.push(`- Parse failures: ${assurance.parseFailures} — ${assurance.parseFailuresNote}`);
|
|
5641
|
+
lines.push('', '## Limitations', '');
|
|
5642
|
+
for (const l of assurance.limitations) lines.push(`- ${l}`);
|
|
5643
|
+
lines.push('');
|
|
5644
|
+
process.stdout.write(lines.join('\n'));
|
|
5645
|
+
return 0;
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
async function cmdRuntimeReconcile(args) {
|
|
5649
|
+
const target = args._[2] || '.';
|
|
5650
|
+
const targetAbs = path.resolve(target);
|
|
5651
|
+
|
|
5652
|
+
const loaded = await _loadRuntimeInputs(targetAbs);
|
|
5653
|
+
if (!loaded.ok) {
|
|
5654
|
+
process.stderr.write(`agentic-security runtime reconcile: ${loaded.message}\n`);
|
|
5655
|
+
return 1;
|
|
5656
|
+
}
|
|
5657
|
+
if (loaded.observations === null) {
|
|
5658
|
+
process.stdout.write(JSON.stringify({ checked: 0, drifted: [], note: 'No runtime observation store exists for this scan — nothing to reconcile.' }, null, 2) + '\n');
|
|
5659
|
+
return 0;
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5662
|
+
// Deliberately diagnostic-only: `matchObservationToGraph` is re-run here
|
|
5663
|
+
// purely to COMPARE against each record's own already-stored match — it
|
|
5664
|
+
// never rewrites the stored record. `observation-correlation.js`'s own
|
|
5665
|
+
// header explains why `correlateObservations` itself must never re-run
|
|
5666
|
+
// the ladder (a stored observation must stay honest about what it was
|
|
5667
|
+
// actually checked against at import time); `reconcile` exists
|
|
5668
|
+
// precisely to surface drift for an operator to decide on, not to
|
|
5669
|
+
// silently apply it.
|
|
5670
|
+
const { matchObservationToGraph } = await import('../src/lineage/observation-correlation.js');
|
|
5671
|
+
const drifted = [];
|
|
5672
|
+
for (const obs of loaded.observations) {
|
|
5673
|
+
const fresh = matchObservationToGraph(loaded.graph, obs);
|
|
5674
|
+
const same = fresh.matchMethod === obs.matchMethod
|
|
5675
|
+
&& fresh.matchConfidence === obs.matchConfidence
|
|
5676
|
+
&& JSON.stringify([...fresh.matchedFlowIds].sort()) === JSON.stringify([...obs.matchedFlowIds].sort());
|
|
5677
|
+
if (!same) {
|
|
5678
|
+
drifted.push({
|
|
5679
|
+
observationId: obs.id,
|
|
5680
|
+
stored: { matchMethod: obs.matchMethod, matchConfidence: obs.matchConfidence, matchedFlowIds: obs.matchedFlowIds },
|
|
5681
|
+
currentGraph: { matchMethod: fresh.matchMethod, matchConfidence: fresh.matchConfidence, matchedFlowIds: fresh.matchedFlowIds },
|
|
5682
|
+
});
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
process.stdout.write(JSON.stringify({
|
|
5686
|
+
checked: loaded.observations.length,
|
|
5687
|
+
drifted,
|
|
5688
|
+
note: drifted.length > 0
|
|
5689
|
+
? 'These observations would match differently against the CURRENT graph than what is stored — the graph changed since import. Re-import with `runtime ingest` to update the stored match; nothing was changed automatically.'
|
|
5690
|
+
: 'Every stored observation still matches the current graph identically.',
|
|
5691
|
+
}, null, 2) + '\n');
|
|
5692
|
+
return 0;
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5695
|
+
const _RUNTIME_SENSITIVE_CLASSES = ['PII', 'PHI', 'PCI', 'FIN', 'CREDENTIALS'];
|
|
5696
|
+
|
|
5697
|
+
const _RUNTIME_QUERIES = Object.freeze({
|
|
5698
|
+
'internet-facing-to-sensitive': {
|
|
5699
|
+
description: 'Internet-facing flows observed reaching sensitive data (PRD example: "Show Internet-facing flows observed reaching PCI data").',
|
|
5700
|
+
run(graph, correlation) {
|
|
5701
|
+
const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5702
|
+
const dataElementsById = new Map(graph.dataElements.map((d) => [d.id, d]));
|
|
5703
|
+
return graph.flows.filter((f) => {
|
|
5704
|
+
if (correlation.byFlow[f.id]?.layer !== 'runtime_observed') return false;
|
|
5705
|
+
const sink = nodesById.get(f.sink);
|
|
5706
|
+
if (sink?.externality?.value !== 'external') return false;
|
|
5707
|
+
return f.dataElementIds.some((id) => (dataElementsById.get(id)?.dataClasses ?? []).some((c) => _RUNTIME_SENSITIVE_CLASSES.includes(c)));
|
|
5708
|
+
}).map((f) => f.id);
|
|
5709
|
+
},
|
|
5710
|
+
},
|
|
5711
|
+
'sensitive-crossing-boundary': {
|
|
5712
|
+
description: 'Sensitive-data paths observed crossing a trust boundary (PRD example: "Show sensitive-data paths observed crossing trust boundaries").',
|
|
5713
|
+
run(graph, correlation) {
|
|
5714
|
+
const edgesById = new Map(graph.edges.map((e) => [e.id, e]));
|
|
5715
|
+
const dataElementsById = new Map(graph.dataElements.map((d) => [d.id, d]));
|
|
5716
|
+
return graph.flows.filter((f) => {
|
|
5717
|
+
if (correlation.byFlow[f.id]?.layer !== 'runtime_observed') return false;
|
|
5718
|
+
const crosses = f.edgeIds.some((id) => (edgesById.get(id)?.boundaryCrossings ?? []).length > 0);
|
|
5719
|
+
if (!crosses) return false;
|
|
5720
|
+
return f.dataElementIds.some((id) => (dataElementsById.get(id)?.dataClasses ?? []).some((c) => _RUNTIME_SENSITIVE_CLASSES.includes(c)));
|
|
5721
|
+
}).map((f) => f.id);
|
|
5722
|
+
},
|
|
5723
|
+
},
|
|
5724
|
+
});
|
|
5725
|
+
|
|
5726
|
+
// PRD-named example queries this architecture cannot honestly answer —
|
|
5727
|
+
// disclosed, never silently omitted. "Runtime flows absent from the
|
|
5728
|
+
// static graph" needs a config-declared/auto-correlated edge mechanism
|
|
5729
|
+
// (M5 7a / M2 F2-F3, both out of scope here — every RuntimeObservation
|
|
5730
|
+
// matches an EXISTING static node, it never mints a new one). "Vulnerable
|
|
5731
|
+
// functions actually exercised in production" and "identities using
|
|
5732
|
+
// risky paths" both need identity/function-level telemetry this
|
|
5733
|
+
// contract's closed-world schema deliberately omits (see this PRD's
|
|
5734
|
+
// implementation-status doc).
|
|
5735
|
+
const _RUNTIME_QUERIES_NOT_SUPPORTED = Object.freeze({
|
|
5736
|
+
'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.',
|
|
5737
|
+
'vulnerable-functions-in-production': 'Needs function/symbol-level runtime telemetry; this adapter set only matches destinations (route/service/table/topic), never a function symbol.',
|
|
5738
|
+
'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.',
|
|
5739
|
+
});
|
|
5740
|
+
|
|
5741
|
+
async function cmdRuntimeQuery(args) {
|
|
5742
|
+
const name = args._[2];
|
|
5743
|
+
if (!name || args.flags.list) {
|
|
5744
|
+
process.stdout.write('Supported queries:\n');
|
|
5745
|
+
for (const [k, v] of Object.entries(_RUNTIME_QUERIES)) process.stdout.write(` ${k} — ${v.description}\n`);
|
|
5746
|
+
process.stdout.write('\nNamed but NOT supported by this architecture (disclosed, not silently omitted):\n');
|
|
5747
|
+
for (const [k, v] of Object.entries(_RUNTIME_QUERIES_NOT_SUPPORTED)) process.stdout.write(` ${k} — ${v}\n`);
|
|
5748
|
+
// --list is always a successful, intentional operation (exit 0);
|
|
5749
|
+
// calling `runtime query` with neither a name nor --list is the only
|
|
5750
|
+
// real misuse here.
|
|
5751
|
+
return args.flags.list ? 0 : 2;
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5754
|
+
if (Object.prototype.hasOwnProperty.call(_RUNTIME_QUERIES_NOT_SUPPORTED, name)) {
|
|
5755
|
+
process.stdout.write(JSON.stringify({ query: name, supported: false, reason: _RUNTIME_QUERIES_NOT_SUPPORTED[name] }, null, 2) + '\n');
|
|
5756
|
+
return 0;
|
|
5757
|
+
}
|
|
5758
|
+
const query = _RUNTIME_QUERIES[name];
|
|
5759
|
+
if (!query) {
|
|
5760
|
+
process.stderr.write(`agentic-security runtime query: unrecognized query "${name}" — run with --list to see every supported (and disclosed-unsupported) query name.\n`);
|
|
5761
|
+
return 2;
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
const target = args._[3] || '.';
|
|
5765
|
+
const targetAbs = path.resolve(target);
|
|
5766
|
+
const loaded = await _loadRuntimeInputs(targetAbs);
|
|
5767
|
+
if (!loaded.ok) {
|
|
5768
|
+
process.stderr.write(`agentic-security runtime query: ${loaded.message}\n`);
|
|
5769
|
+
return 1;
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
const environment = typeof args.flags.environment === 'string' ? args.flags.environment : null;
|
|
5773
|
+
const { correlateObservations } = await import('../src/lineage/observation-correlation.js');
|
|
5774
|
+
const correlation = correlateObservations(loaded.graph, loaded.observations, { environment, windowStart: null, windowEnd: null });
|
|
5775
|
+
|
|
5776
|
+
const flowIds = query.run(loaded.graph, correlation);
|
|
5777
|
+
process.stdout.write(JSON.stringify({ query: name, supported: true, flowIds }, null, 2) + '\n');
|
|
5778
|
+
return 0;
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
// ─── PRD 5: Source-to-Artifact Security & Provenance ───────────────────────
|
|
5782
|
+
// `image scan/verify`, `provenance trace`, `supply vex`. See
|
|
5783
|
+
// docs/implementation-status/source-to-artifact-provenance.md for the full
|
|
5784
|
+
// scope, naming-collision discipline (why the graph field below is
|
|
5785
|
+
// `artifactProvenanceGraph`, never a bare `.provenance`), and what is
|
|
5786
|
+
// deliberately NOT implemented (local-daemon image / remote registry
|
|
5787
|
+
// reference inputs; keyless Rekor signature lookup).
|
|
5788
|
+
|
|
5789
|
+
function _readFileOrError(target) {
|
|
5790
|
+
const abs = path.resolve(target);
|
|
5791
|
+
if (!fs.existsSync(abs)) return { ok: false, message: `no such file: ${target}` };
|
|
5792
|
+
if (!fs.statSync(abs).isFile()) return { ok: false, message: `${target} is not a file` };
|
|
5793
|
+
try {
|
|
5794
|
+
return { ok: true, buf: fs.readFileSync(abs) };
|
|
5795
|
+
} catch (err) {
|
|
5796
|
+
return { ok: false, message: `failed to read ${target}: ${err.message}` };
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
function _artifactScanDigestHex(digestOrTarget) {
|
|
5801
|
+
const s = String(digestOrTarget || '');
|
|
5802
|
+
const hex = s.startsWith('sha256:') ? s.slice(7) : s;
|
|
5803
|
+
return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
// Parses an archive fresh and runs inventory + vuln correlation — the one
|
|
5807
|
+
// real analysis path every `image scan`/`provenance trace`/`supply vex`
|
|
5808
|
+
// invocation that isn't reading a stored result funnels through.
|
|
5809
|
+
async function _scanArchiveFresh(buf) {
|
|
5810
|
+
const { buildImageInventory } = await import('../src/artifact/inventory.js');
|
|
5811
|
+
const { ImageArchiveError } = await import('../src/artifact/image-archive.js');
|
|
5812
|
+
const { TarSafetyError } = await import('../src/artifact/safe-tar.js');
|
|
5813
|
+
const { parseManifests, queryOSV } = await import('../src/sca/index.js');
|
|
5814
|
+
const { correlateImageVulnerabilities } = await import('../src/artifact/vuln-correlate.js');
|
|
5815
|
+
const { analyzeLayerSecrets } = await import('../src/artifact/layer-secrets.js');
|
|
5816
|
+
const { scanCredentials } = await import('../src/secrets/index.js');
|
|
5817
|
+
|
|
5818
|
+
let built;
|
|
5819
|
+
try {
|
|
5820
|
+
built = buildImageInventory(buf, { manifestParser: parseManifests });
|
|
5821
|
+
} catch (err) {
|
|
5822
|
+
if (err instanceof ImageArchiveError || err instanceof TarSafetyError) return { ok: false, message: err.message };
|
|
5823
|
+
throw err;
|
|
5824
|
+
}
|
|
5825
|
+
const { vulnerabilities } = await correlateImageVulnerabilities({
|
|
5826
|
+
inventory: built.inventory, fileContents: built.mergedFilesystem.fileContents, queryOSVFn: queryOSV,
|
|
5827
|
+
});
|
|
5828
|
+
const secrets = analyzeLayerSecrets({
|
|
5829
|
+
readLayersResult: built.readLayersResult, deletedPaths: built.mergedFilesystem.deletedPaths, detectFn: scanCredentials,
|
|
5830
|
+
});
|
|
5831
|
+
return {
|
|
5832
|
+
ok: true,
|
|
5833
|
+
built,
|
|
5834
|
+
record: {
|
|
5835
|
+
inventory: built.inventory,
|
|
5836
|
+
vulnerabilities,
|
|
5837
|
+
secrets: { deletedButRecoverable: secrets.deletedButRecoverable, totalFindings: secrets.findings.length },
|
|
5838
|
+
scannedAt: new Date().toISOString(),
|
|
5839
|
+
},
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
async function _persistArtifactScan(record) {
|
|
5844
|
+
const { statePath, safeWriteState } = await import('../src/posture/state-dir.js');
|
|
5845
|
+
const root = process.cwd();
|
|
5846
|
+
const hex = _artifactScanDigestHex(record.inventory.digest);
|
|
5847
|
+
if (!hex) return false; // defensive: digest is always well-formed sha256:<hex> from image-archive.js
|
|
5848
|
+
return safeWriteState(statePath(root, 'artifact-scans', `${hex}.json`), JSON.stringify(record, null, 2), { category: 'artifact-scans' });
|
|
5849
|
+
}
|
|
5850
|
+
|
|
5851
|
+
// Resolves `target` to a scan record: a digest/sha256:<hex> that matches a
|
|
5852
|
+
// PREVIOUSLY `image scan`-ned, persisted record is loaded from state;
|
|
5853
|
+
// anything else is treated as an archive file path and parsed fresh. This
|
|
5854
|
+
// is what lets `provenance trace <digest>` and `supply vex <digest>` work
|
|
5855
|
+
// after an earlier `image scan <path>` without re-reading the archive.
|
|
5856
|
+
async function _resolveArtifactScan(target) {
|
|
5857
|
+
const hex = _artifactScanDigestHex(target);
|
|
5858
|
+
if (hex) {
|
|
5859
|
+
const { statePath } = await import('../src/posture/state-dir.js');
|
|
5860
|
+
const storedPath = statePath(process.cwd(), 'artifact-scans', `${hex}.json`);
|
|
5861
|
+
if (fs.existsSync(storedPath)) {
|
|
5862
|
+
try {
|
|
5863
|
+
return { ok: true, record: JSON.parse(fs.readFileSync(storedPath, 'utf8')) };
|
|
5864
|
+
} catch (err) {
|
|
5865
|
+
return { ok: false, message: `stored scan at ${storedPath} is corrupt: ${err.message}` };
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
return { ok: false, message: `no stored scan for digest ${target} — run "image scan" on the archive first` };
|
|
5869
|
+
}
|
|
5870
|
+
const read = _readFileOrError(target);
|
|
5871
|
+
if (!read.ok) return read;
|
|
5872
|
+
const scanned = await _scanArchiveFresh(read.buf);
|
|
5873
|
+
if (!scanned.ok) return scanned;
|
|
5874
|
+
return { ok: true, record: scanned.record };
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
async function cmdImageScan(args) {
|
|
5878
|
+
const target = args._[2];
|
|
5879
|
+
if (!target) {
|
|
5880
|
+
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');
|
|
5881
|
+
return 2;
|
|
5882
|
+
}
|
|
5883
|
+
const format = args.flags.format ?? 'json';
|
|
5884
|
+
if (format !== 'json' && format !== 'cli') {
|
|
5885
|
+
process.stderr.write('agentic-security image scan: --format must be json|cli.\n');
|
|
5886
|
+
return 2;
|
|
5887
|
+
}
|
|
5888
|
+
const read = _readFileOrError(target);
|
|
5889
|
+
if (!read.ok) { process.stderr.write(`agentic-security image scan: ${read.message}\n`); return 1; }
|
|
5890
|
+
|
|
5891
|
+
const scanned = await _scanArchiveFresh(read.buf);
|
|
5892
|
+
if (!scanned.ok) { process.stderr.write(`agentic-security image scan: ${scanned.message}\n`); return 1; }
|
|
5893
|
+
|
|
5894
|
+
await _persistArtifactScan(scanned.record);
|
|
5895
|
+
|
|
5896
|
+
if (format === 'json') {
|
|
5897
|
+
process.stdout.write(JSON.stringify(scanned.record, null, 2) + '\n');
|
|
5898
|
+
return 0;
|
|
5899
|
+
}
|
|
5900
|
+
const { inventory, vulnerabilities, secrets } = scanned.record;
|
|
5901
|
+
const vulnCount = vulnerabilities.filter((v) => v.type === 'vulnerable_dep').length;
|
|
5902
|
+
process.stdout.write([
|
|
5903
|
+
`Image digest: ${inventory.digest} (${inventory.digestKind})`,
|
|
5904
|
+
`OS/architecture: ${inventory.os}/${inventory.architecture}`,
|
|
5905
|
+
`OS packages: ${inventory.osPackages.length}, application packages: ${inventory.appPackages.length}`,
|
|
5906
|
+
`Vulnerabilities (OSV): ${vulnCount}`,
|
|
5907
|
+
`Secrets deleted-but-recoverable from an earlier layer: ${secrets.deletedButRecoverable.length}`,
|
|
5908
|
+
inventory.materializationTruncated ? 'NOTE: some layer content exceeded the scan size cap and was skipped.' : '',
|
|
5909
|
+
].filter(Boolean).join('\n') + '\n');
|
|
5910
|
+
return 0;
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
async function cmdImageVerify(args) {
|
|
5914
|
+
const target = args._[2];
|
|
5915
|
+
if (!target) {
|
|
5916
|
+
process.stderr.write('agentic-security image verify: provide a path to a docker-archive or oci-archive tar, or a previously-scanned digest.\n');
|
|
5917
|
+
return 2;
|
|
5918
|
+
}
|
|
5919
|
+
const resolved = await _resolveArtifactScan(target);
|
|
5920
|
+
if (!resolved.ok) { process.stderr.write(`agentic-security image verify: ${resolved.message}\n`); return 1; }
|
|
5921
|
+
const { inventory } = resolved.record;
|
|
5922
|
+
|
|
5923
|
+
const { verifyImageSignature, verifyProvenanceAttestation } = await import('../src/artifact/signature.js');
|
|
5924
|
+
|
|
5925
|
+
let signature = null;
|
|
5926
|
+
let publicKeyPem = null;
|
|
5927
|
+
let attestation = null;
|
|
5928
|
+
if (typeof args.flags.signature === 'string') {
|
|
5929
|
+
const r = _readFileOrError(args.flags.signature);
|
|
5930
|
+
if (!r.ok) { process.stderr.write(`agentic-security image verify: --signature: ${r.message}\n`); return 1; }
|
|
5931
|
+
signature = r.buf;
|
|
5932
|
+
}
|
|
5933
|
+
if (typeof args.flags['public-key'] === 'string') {
|
|
5934
|
+
const r = _readFileOrError(args.flags['public-key']);
|
|
5935
|
+
if (!r.ok) { process.stderr.write(`agentic-security image verify: --public-key: ${r.message}\n`); return 1; }
|
|
5936
|
+
publicKeyPem = r.buf.toString('utf8');
|
|
5937
|
+
}
|
|
5938
|
+
if (typeof args.flags.attestation === 'string') {
|
|
5939
|
+
const r = _readFileOrError(args.flags.attestation);
|
|
5940
|
+
if (!r.ok) { process.stderr.write(`agentic-security image verify: --attestation: ${r.message}\n`); return 1; }
|
|
5941
|
+
try { attestation = JSON.parse(r.buf.toString('utf8')); } catch (err) {
|
|
5942
|
+
process.stderr.write(`agentic-security image verify: --attestation is not valid JSON: ${err.message}\n`);
|
|
5943
|
+
return 1;
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
|
|
5947
|
+
const signatureResult = verifyImageSignature({ digest: inventory.digest, signature, publicKeyPem });
|
|
5948
|
+
const attestationResult = verifyProvenanceAttestation({ digest: inventory.digest, envelope: attestation, publicKeyPem });
|
|
5949
|
+
|
|
5950
|
+
const result = { digest: inventory.digest, signature: signatureResult, attestation: attestationResult };
|
|
5951
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
5952
|
+
// Exit non-zero only on a definite "invalid" — "missing"/"unavailable" are
|
|
5953
|
+
// honest reports of absent material, not a failed check, and must not be
|
|
5954
|
+
// conflated with a cryptographic failure a CI gate should actually block on.
|
|
5955
|
+
return (signatureResult.state === 'invalid' || attestationResult.state === 'invalid') ? 1 : 0;
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
async function cmdProvenanceTrace(args) {
|
|
5959
|
+
const target = args._[2];
|
|
5960
|
+
if (!target) {
|
|
5961
|
+
process.stderr.write('agentic-security provenance trace: provide a path to an image archive, or a previously-scanned digest.\n');
|
|
5962
|
+
return 2;
|
|
5963
|
+
}
|
|
5964
|
+
const resolved = await _resolveArtifactScan(target);
|
|
5965
|
+
if (!resolved.ok) { process.stderr.write(`agentic-security provenance trace: ${resolved.message}\n`); return 1; }
|
|
5966
|
+
const { inventory, vulnerabilities } = resolved.record;
|
|
5967
|
+
|
|
5968
|
+
let deployment = null;
|
|
5969
|
+
if (typeof args.flags['deployment-id'] === 'string') {
|
|
5970
|
+
deployment = {
|
|
5971
|
+
id: args.flags['deployment-id'],
|
|
5972
|
+
environment: typeof args.flags['deployment-environment'] === 'string' ? args.flags['deployment-environment'] : null,
|
|
5973
|
+
digest: typeof args.flags['deployment-digest'] === 'string' ? args.flags['deployment-digest'] : null,
|
|
5974
|
+
evidence: 'operator-declared',
|
|
5975
|
+
};
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
const { buildArtifactProvenanceGraph, traceArtifactProvenanceGraph } = await import('../src/artifact/provenance-graph.js');
|
|
5979
|
+
const artifactProvenanceGraph = buildArtifactProvenanceGraph({ inventory, vulnerabilities, deployment });
|
|
5980
|
+
const commitNode = artifactProvenanceGraph.nodes.find((n) => n.kind === 'gitCommit');
|
|
5981
|
+
const trace = traceArtifactProvenanceGraph(artifactProvenanceGraph, commitNode.id);
|
|
5982
|
+
|
|
5983
|
+
let mismatch = null;
|
|
5984
|
+
if (typeof args.flags.source === 'string') {
|
|
5985
|
+
const sourceRoot = path.resolve(args.flags.source);
|
|
5986
|
+
const lastScanPath = path.join(sourceRoot, '.agentic-security', 'last-scan.json');
|
|
5987
|
+
if (!fs.existsSync(lastScanPath)) {
|
|
5988
|
+
process.stderr.write(`agentic-security provenance trace: --source "${args.flags.source}" has no .agentic-security/last-scan.json — run "scan" there first.\n`);
|
|
5989
|
+
return 1;
|
|
5990
|
+
}
|
|
5991
|
+
const sourceScan = JSON.parse(fs.readFileSync(lastScanPath, 'utf8'));
|
|
5992
|
+
const { detectSourceArtifactMismatch } = await import('../src/artifact/mismatch.js');
|
|
5993
|
+
mismatch = detectSourceArtifactMismatch({
|
|
5994
|
+
sourceVulnerabilities: sourceScan.supplyChain || [],
|
|
5995
|
+
artifactVulnerabilities: vulnerabilities,
|
|
5996
|
+
artifactComponents: [...inventory.osPackages, ...inventory.appPackages],
|
|
5997
|
+
});
|
|
5998
|
+
}
|
|
5999
|
+
|
|
6000
|
+
const format = args.flags.format ?? 'json';
|
|
6001
|
+
if (format === 'json') {
|
|
6002
|
+
process.stdout.write(JSON.stringify({ artifactProvenanceGraph, trace, mismatch }, null, 2) + '\n');
|
|
6003
|
+
return 0;
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
const lines = [`Artifact digest: ${inventory.digest}`, `Source commit: ${commitNode.known ? commitNode.sha : '(not declared — no org.opencontainers.image.revision label)'}`];
|
|
6007
|
+
if (mismatch) {
|
|
6008
|
+
lines.push('');
|
|
6009
|
+
if (mismatch.scenarioA.length > 0) {
|
|
6010
|
+
lines.push('SOURCE: FIXED');
|
|
6011
|
+
lines.push('DEPLOYED ARTIFACT: STILL VULNERABLE');
|
|
6012
|
+
lines.push('PRODUCTION EXPOSURE: PRESENT');
|
|
6013
|
+
lines.push(` ${mismatch.scenarioA.length} vulnerabilit${mismatch.scenarioA.length === 1 ? 'y' : 'ies'} present in this artifact, already fixed in current source: ${mismatch.scenarioA.map((v) => v.osvId).join(', ')}`);
|
|
6014
|
+
}
|
|
6015
|
+
if (mismatch.scenarioB.length > 0) {
|
|
6016
|
+
lines.push(`${mismatch.scenarioA.length > 0 ? '' : 'SOURCE: VULNERABLE\n'}BUILT ARTIFACT: AFFECTED COMPONENT ABSENT`);
|
|
6017
|
+
lines.push(` ${mismatch.scenarioB.length} vulnerabilit${mismatch.scenarioB.length === 1 ? 'y' : 'ies'} flagged in current source did not actually ship in this artifact: ${mismatch.scenarioB.map((v) => v.osvId).join(', ')}`);
|
|
6018
|
+
}
|
|
6019
|
+
if (!mismatch.hasMismatch) lines.push('No mismatch: current source and this deployed artifact agree.');
|
|
6020
|
+
}
|
|
6021
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
6022
|
+
return 0;
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
async function cmdSupplyVex(args) {
|
|
6026
|
+
const target = args._[2];
|
|
6027
|
+
if (!target) {
|
|
6028
|
+
process.stderr.write('agentic-security supply vex: provide a path to an image archive, or a previously-scanned digest.\n');
|
|
6029
|
+
return 2;
|
|
6030
|
+
}
|
|
6031
|
+
const resolved = await _resolveArtifactScan(target);
|
|
6032
|
+
if (!resolved.ok) { process.stderr.write(`agentic-security supply vex: ${resolved.message}\n`); return 1; }
|
|
6033
|
+
const { inventory, vulnerabilities } = resolved.record;
|
|
6034
|
+
|
|
6035
|
+
let reachabilityByKey = new Map();
|
|
6036
|
+
if (typeof args.flags.source === 'string') {
|
|
6037
|
+
const sourceRoot = path.resolve(args.flags.source);
|
|
6038
|
+
const lastScanPath = path.join(sourceRoot, '.agentic-security', 'last-scan.json');
|
|
6039
|
+
if (fs.existsSync(lastScanPath)) {
|
|
6040
|
+
const sourceScan = JSON.parse(fs.readFileSync(lastScanPath, 'utf8'));
|
|
6041
|
+
for (const sc of (sourceScan.supplyChain || [])) {
|
|
6042
|
+
if (sc.type !== 'vulnerable_dep') continue;
|
|
6043
|
+
const key = sc.purl || `${sc.ecosystem}:${sc.name}`;
|
|
6044
|
+
const reachable = sc.functionReachable === 'reachable' ? true : sc.functionReachable === 'unreachable' ? false : null;
|
|
6045
|
+
if (reachable !== null) reachabilityByKey.set(key, { reachable, reason: sc.reachabilityTier || null });
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
|
|
6050
|
+
const { deriveVexStatus } = await import('../src/artifact/vex.js');
|
|
6051
|
+
const artifactComponents = [...inventory.osPackages, ...inventory.appPackages];
|
|
6052
|
+
const statements = vulnerabilities.filter((v) => v.type === 'vulnerable_dep').map((v) => {
|
|
6053
|
+
const key = v.purl || `${v.ecosystem}:${v.name}`;
|
|
6054
|
+
const reachability = reachabilityByKey.get(key) || null;
|
|
6055
|
+
const vex = deriveVexStatus({ vulnerability: v, artifactComponents, reachability });
|
|
6056
|
+
return { purl: v.purl || null, name: v.name, ecosystem: v.ecosystem, osvId: v.osvId, ...vex };
|
|
6057
|
+
});
|
|
6058
|
+
|
|
6059
|
+
const format = args.flags.format ?? 'json';
|
|
6060
|
+
if (format === 'json') {
|
|
6061
|
+
process.stdout.write(JSON.stringify({ digest: inventory.digest, statements }, null, 2) + '\n');
|
|
6062
|
+
return 0;
|
|
6063
|
+
}
|
|
6064
|
+
const lines = [`VEX for ${inventory.digest}:`];
|
|
6065
|
+
for (const s of statements) lines.push(` ${s.osvId} (${s.name}): ${s.status}${s.justification ? ` [${s.justification}]` : ''}`);
|
|
6066
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
6067
|
+
return 0;
|
|
6068
|
+
}
|
|
6069
|
+
|
|
5564
6070
|
// Mirrors posture/fix-history.js's own _writeAtomicAndSync (temp file in
|
|
5565
6071
|
// the same directory, fsync, then rename over the target) — a crash
|
|
5566
6072
|
// mid-write must never leave recipient-profiles.json as invalid JSON,
|
|
@@ -7288,6 +7794,67 @@ async function main() {
|
|
|
7288
7794
|
}
|
|
7289
7795
|
break;
|
|
7290
7796
|
}
|
|
7797
|
+
case 'runtime': {
|
|
7798
|
+
// NOT a `dataflow` subcommand — PRD "Runtime-Corroborated Security
|
|
7799
|
+
// Twin" names this literal CLI shape (`runtime ingest/status/
|
|
7800
|
+
// reconcile/query`). `ingest` is a thin shim over the already-
|
|
7801
|
+
// shipped `dataflow observations import`; `status`/`reconcile`/
|
|
7802
|
+
// `query` are new, built entirely on the already-shipped
|
|
7803
|
+
// correlation/match/store functions. See cmdRuntimeStatus's own
|
|
7804
|
+
// header comment above.
|
|
7805
|
+
const sub = args._[1];
|
|
7806
|
+
if (sub === 'ingest') { process.exit(await cmdRuntimeIngest(args)); }
|
|
7807
|
+
else if (sub === 'status') { process.exit(await cmdRuntimeStatus(args)); }
|
|
7808
|
+
else if (sub === 'reconcile') { process.exit(await cmdRuntimeReconcile(args)); }
|
|
7809
|
+
else if (sub === 'query') { process.exit(await cmdRuntimeQuery(args)); }
|
|
7810
|
+
else {
|
|
7811
|
+
process.stderr.write(`agentic-security runtime: unrecognized sub-command "${sub}" — must be "ingest", "status", "reconcile", or "query".\n`);
|
|
7812
|
+
process.exit(2);
|
|
7813
|
+
}
|
|
7814
|
+
break;
|
|
7815
|
+
}
|
|
7816
|
+
case 'image': {
|
|
7817
|
+
// PRD 5 (Source-to-Artifact Security & Provenance) names this
|
|
7818
|
+
// literal CLI shape. `scan` parses + correlates; `verify` checks
|
|
7819
|
+
// signature/attestation material only (never re-runs correlation).
|
|
7820
|
+
const sub = args._[1];
|
|
7821
|
+
if (sub === 'scan') { process.exit(await cmdImageScan(args)); }
|
|
7822
|
+
else if (sub === 'verify') { process.exit(await cmdImageVerify(args)); }
|
|
7823
|
+
else {
|
|
7824
|
+
process.stderr.write(`agentic-security image: unrecognized sub-command "${sub}" — must be "scan" or "verify".\n`);
|
|
7825
|
+
process.exit(2);
|
|
7826
|
+
}
|
|
7827
|
+
break;
|
|
7828
|
+
}
|
|
7829
|
+
case 'provenance': {
|
|
7830
|
+
// NOT `posture/provenance/`'s git-commit finding provenance, NOT
|
|
7831
|
+
// `sca/sigstore-verify.js`'s package-build provenance — this is PRD
|
|
7832
|
+
// 5's own artifact provenance graph (Git Commit -> Build -> OCI
|
|
7833
|
+
// Digest -> SBOM Component -> Finding -> Deployment). See
|
|
7834
|
+
// docs/implementation-status/source-to-artifact-provenance.md's
|
|
7835
|
+
// naming-discipline section.
|
|
7836
|
+
const sub = args._[1];
|
|
7837
|
+
if (sub === 'trace') { process.exit(await cmdProvenanceTrace(args)); }
|
|
7838
|
+
else {
|
|
7839
|
+
process.stderr.write(`agentic-security provenance: unrecognized sub-command "${sub}" — must be "trace".\n`);
|
|
7840
|
+
process.exit(2);
|
|
7841
|
+
}
|
|
7842
|
+
break;
|
|
7843
|
+
}
|
|
7844
|
+
case 'supply': {
|
|
7845
|
+
// The `/supply` SLASH COMMAND (commands/supply.md) is a different
|
|
7846
|
+
// layer entirely — Claude Code UI sugar that shells out to `scan
|
|
7847
|
+
// --only sca`/`cve-watch`. This is PRD 5's own literal CLI shape,
|
|
7848
|
+
// `agentic-security supply vex`, operating on an IMAGE ARTIFACT
|
|
7849
|
+
// rather than a source directory.
|
|
7850
|
+
const sub = args._[1];
|
|
7851
|
+
if (sub === 'vex') { process.exit(await cmdSupplyVex(args)); }
|
|
7852
|
+
else {
|
|
7853
|
+
process.stderr.write(`agentic-security supply: unrecognized sub-command "${sub}" — must be "vex".\n`);
|
|
7854
|
+
process.exit(2);
|
|
7855
|
+
}
|
|
7856
|
+
break;
|
|
7857
|
+
}
|
|
7291
7858
|
case 'remediation': {
|
|
7292
7859
|
// NOT a `dataflow` subcommand — this writes operator/incident
|
|
7293
7860
|
// state (the append-only remediation ledger), never the scanned
|