@clear-capabilities/agentic-security-scanner 0.169.0 → 0.171.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 +60 -0
- package/bin/agentic-security.js +331 -0
- package/dist/2199.index.js +1 -1
- package/dist/2675.index.js +174 -0
- package/dist/4142.index.js +6 -6
- 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 +2 -2
- 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/posture/artifact-registry.js +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,66 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.171.0 - documentation: surface every shipped PRD capability in the README and docs index
|
|
13
|
+
|
|
14
|
+
A documentation-only release. Auditing the docs against the actual CLI surface after PRD 5 shipped found
|
|
15
|
+
several real gaps — not cosmetic, actively misleading in one case — and fixed them:
|
|
16
|
+
|
|
17
|
+
- **README.md said built container images are NOT scanned.** That line predated `image scan`/`image verify`
|
|
18
|
+
(PRD 5, shipped in 0.170.0) and was flatly false by the time it shipped. Corrected, with a link to the new
|
|
19
|
+
guide below.
|
|
20
|
+
- **Seven of the repo's 17 slash-command dispatchers had no entry in README's own Commands list**:
|
|
21
|
+
`dataflow`, `governance`, `remediation`, `federate`, `image`, `provenance`, `runtime`. Added, grouped under
|
|
22
|
+
two new categories ("Data Flow Explorer" and "Source-to-artifact & runtime").
|
|
23
|
+
- **`docs/guides/runtime-twin.md` (PRD 2's own user guide) was an orphan** — reachable from one internal
|
|
24
|
+
cross-link, linked from neither README.md nor docs/README.md. Linked from both.
|
|
25
|
+
- **PRD 5 (Source-to-Artifact Security & Provenance) had no user-facing guide at all**, only the internal
|
|
26
|
+
`docs/implementation-status/` tracking doc. Added `docs/guides/artifact-provenance.md`.
|
|
27
|
+
- **`docs/reference/cli.md` — "every command, flag, and exit code" — was missing `proof`, `rules`, `fleet`,
|
|
28
|
+
`runtime`, `image`, `provenance`, and `supply` entirely** from its own Commands table (the literal CLI
|
|
29
|
+
surface of PRDs 1, 3, 4, 2, and 5). Added all seven.
|
|
30
|
+
- Added two new "What makes it different" bullets (runtime corroboration, source-to-artifact tracing) and a
|
|
31
|
+
`supply --vex` mode mention to README's Commands list.
|
|
32
|
+
|
|
33
|
+
No code changed; `scanner/dist/agentic-security.mjs` is unchanged.
|
|
34
|
+
|
|
35
|
+
## 0.170.0 - source-to-artifact security & provenance: a new `src/artifact/` package, `image`/`provenance` CLI, VEX
|
|
36
|
+
|
|
37
|
+
Binds a finding to the actual **built artifact** (a container image, today) that ships it — closing the gap
|
|
38
|
+
between "this dependency has a CVE" and "this specific image digest actually ships it." A new, isolated
|
|
39
|
+
`scanner/src/artifact/` package reuses the existing SCA pipeline (manifest parsers, OS-package-DB parser,
|
|
40
|
+
OSV/KEV/EPSS correlation) rather than building a parallel one.
|
|
41
|
+
|
|
42
|
+
**OCI image scanning + inventory.** A hand-rolled, hardened tar/gzip reader (`safe-tar.js`) — hard caps on entry
|
|
43
|
+
count/size/archive size, a native decompression-bomb guard, and a path-safety check that flags (never silently
|
|
44
|
+
normalizes) a `..`/absolute tar entry path. Parses a `docker save`-shaped archive or an OCI-layout archive into
|
|
45
|
+
digest/OS/architecture/base-image/layers/entrypoint/build-labels, with OS and application packages extracted
|
|
46
|
+
through the exact same parsers every other SCA finding in this codebase uses. Never executes the image — no
|
|
47
|
+
`docker run`, no daemon required for the archive path.
|
|
48
|
+
|
|
49
|
+
**Layer secret analysis.** A secret added in one layer and deleted by a later overlay whiteout is still sitting
|
|
50
|
+
in that earlier layer's raw tar bytes — detected and reported (value always masked/redacted, the detector is
|
|
51
|
+
injected, same shape `posture/secret-history.js` already uses for the git-history analog).
|
|
52
|
+
|
|
53
|
+
**Signature + attestation verification**, states exactly `verified`/`invalid`/`missing`/`unavailable` — a
|
|
54
|
+
detached-signature check over the image digest, plus REAL DSSE (Pre-Authentication Encoding) verification for
|
|
55
|
+
in-toto/SLSA provenance attestations, so a real cosign-keyed attestation bundle verifies here given the matching
|
|
56
|
+
public key. Never treats missing material as verified.
|
|
57
|
+
|
|
58
|
+
**The provenance graph** (Git Commit → Build → OCI Digest → SBOM Component → Finding → Deployment) and
|
|
59
|
+
**source-vs-artifact mismatch detection** — the PRD's own "Killer Demonstration": a developer fixes a
|
|
60
|
+
vulnerability in source, but production is still running the old image. `SOURCE: FIXED` / `DEPLOYED ARTIFACT:
|
|
61
|
+
STILL VULNERABLE` / `PRODUCTION EXPOSURE: PRESENT`, proven end-to-end against the real built CLI. **VEX**
|
|
62
|
+
statements (affected/not affected/fixed/under investigation) — `not affected` only ever from positive evidence
|
|
63
|
+
(component absent, or confirmed unreachable), never from absence of information.
|
|
64
|
+
|
|
65
|
+
**CLI:** `agentic-security image scan/verify`, `agentic-security provenance trace`, `agentic-security supply vex`
|
|
66
|
+
— the PRD's own literal CLI shape. New slash commands `/image`, `/provenance`; `/supply` gained a `--vex` mode.
|
|
67
|
+
|
|
68
|
+
**Deliberately NOT done, disclosed:** a local Docker daemon image reference and a remote OCI registry reference
|
|
69
|
+
as `image scan` inputs (only local docker-archive/oci-archive tar files); keyless Rekor-transparency-log
|
|
70
|
+
signature verification. See `docs/implementation-status/source-to-artifact-provenance.md`.
|
|
71
|
+
|
|
12
72
|
## 0.169.0 - runtime-corroborated security twin: 4 new telemetry adapters, honest runtime assurance, a `runtime` CLI
|
|
13
73
|
|
|
14
74
|
Extends the existing Milestone 5 "Runtime-Corroborated Digital Twin" machinery (closed-world `RuntimeObservation`
|
package/bin/agentic-security.js
CHANGED
|
@@ -5778,6 +5778,295 @@ async function cmdRuntimeQuery(args) {
|
|
|
5778
5778
|
return 0;
|
|
5779
5779
|
}
|
|
5780
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
|
+
|
|
5781
6070
|
// Mirrors posture/fix-history.js's own _writeAtomicAndSync (temp file in
|
|
5782
6071
|
// the same directory, fsync, then rename over the target) — a crash
|
|
5783
6072
|
// mid-write must never leave recipient-profiles.json as invalid JSON,
|
|
@@ -7524,6 +7813,48 @@ async function main() {
|
|
|
7524
7813
|
}
|
|
7525
7814
|
break;
|
|
7526
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
|
+
}
|
|
7527
7858
|
case 'remediation': {
|
|
7528
7859
|
// NOT a `dataflow` subcommand — this writes operator/incident
|
|
7529
7860
|
// state (the append-only remediation ledger), never the scanned
|
package/dist/2199.index.js
CHANGED
|
@@ -313,7 +313,7 @@ function saveProof(scanRoot, record) {
|
|
|
313
313
|
const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(scanRoot, PROOFS_DIR, text);
|
|
314
314
|
if (!enc.ok) return { ok: false, reason: enc.reason };
|
|
315
315
|
const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(record.findingId), `${safe.recordDigest}.json`);
|
|
316
|
-
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__
|
|
316
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.safeWriteState)(file, enc.content)) return { ok: false, reason: 'state write refused' };
|
|
317
317
|
return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export const id = 2675;
|
|
2
|
+
export const ids = [2675];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 82675:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ verifyImageSignature: () => (/* binding */ verifyImageSignature),
|
|
10
|
+
/* harmony export */ verifyProvenanceAttestation: () => (/* binding */ verifyProvenanceAttestation)
|
|
11
|
+
/* harmony export */ });
|
|
12
|
+
/* unused harmony export SIGNATURE_STATES */
|
|
13
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
14
|
+
// PRD 5 P0 §5 — Signature Verification. States are exactly the PRD's own
|
|
15
|
+
// four, deliberately different from `sca/sigstore-verify.js`'s own vocabulary
|
|
16
|
+
// (`verified/unverified/tampered/unknown`) so the two are never confused by
|
|
17
|
+
// shared words — see docs/implementation-status/source-to-artifact-provenance.md's
|
|
18
|
+
// naming-discipline section:
|
|
19
|
+
//
|
|
20
|
+
// verified — a real cryptographic check ran and the signature matches.
|
|
21
|
+
// invalid — a real cryptographic check ran and the signature does NOT match.
|
|
22
|
+
// missing — no signature/attestation material was supplied at all.
|
|
23
|
+
// unavailable — material was supplied but could not be cryptographically
|
|
24
|
+
// checked (no public key given, or the check itself could
|
|
25
|
+
// not run) — the PRD's "never treat missing tooling as
|
|
26
|
+
// verified" clause means this state, not `verified`, is what
|
|
27
|
+
// an incomplete check must report.
|
|
28
|
+
//
|
|
29
|
+
// Two real, independent mechanisms are implemented:
|
|
30
|
+
// 1. A plain detached signature over the image digest string, verified
|
|
31
|
+
// with an operator-supplied public key. This is this tool's OWN local
|
|
32
|
+
// convention, not cosign's "simple signing" JSON format — disclosed in
|
|
33
|
+
// the implementation-status doc, not silently passed off as cosign-
|
|
34
|
+
// compatible.
|
|
35
|
+
// 2. Real DSSE (Dead Simple Signing Envelope) verification for in-toto/SLSA
|
|
36
|
+
// provenance attestations — the actual encoding cosign/Sigstore use for
|
|
37
|
+
// attestations, so a real cosign-keyed attestation bundle DOES verify
|
|
38
|
+
// here given the matching public key. `dssePae` is the exact PAE
|
|
39
|
+
// (Pre-Authentication Encoding) from the DSSE spec.
|
|
40
|
+
//
|
|
41
|
+
// Keyless/Rekor-transparency-log verification is NOT implemented — disclosed
|
|
42
|
+
// as a gap, never faked. `sca/sigstore-verify.js`'s Rekor-query pattern is
|
|
43
|
+
// the template a future increment could extend this module with.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const SIGNATURE_STATES = Object.freeze(['verified', 'invalid', 'missing', 'unavailable']);
|
|
47
|
+
|
|
48
|
+
function verifyRaw(payload, signature, publicKeyPem) {
|
|
49
|
+
let publicKey;
|
|
50
|
+
try {
|
|
51
|
+
publicKey = node_crypto__WEBPACK_IMPORTED_MODULE_0__.createPublicKey(publicKeyPem);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { ok: false, error: `public key is not parseable: ${err.message}` };
|
|
54
|
+
}
|
|
55
|
+
const keyType = publicKey.asymmetricKeyType;
|
|
56
|
+
const algorithm = (keyType === 'ed25519' || keyType === 'ed448') ? null : 'sha256';
|
|
57
|
+
try {
|
|
58
|
+
const ok = node_crypto__WEBPACK_IMPORTED_MODULE_0__.verify(algorithm, payload, publicKey, signature);
|
|
59
|
+
return { ok, keyType };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return { ok: false, error: `signature verification failed to run: ${err.message}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Verifies a detached signature over an image's digest string.
|
|
67
|
+
* `signature`/`publicKeyPem` are Buffer/PEM-string; either missing means
|
|
68
|
+
* `missing` (nothing was supplied to check).
|
|
69
|
+
*/
|
|
70
|
+
function verifyImageSignature({ digest, signature, publicKeyPem }) {
|
|
71
|
+
if (typeof digest !== 'string' || !digest.startsWith('sha256:')) {
|
|
72
|
+
return { state: 'unavailable', reason: 'no image digest available to verify a signature against' };
|
|
73
|
+
}
|
|
74
|
+
if (!signature || !publicKeyPem) {
|
|
75
|
+
return { state: 'missing', reason: 'no detached signature and public key were supplied for this image' };
|
|
76
|
+
}
|
|
77
|
+
const { ok, error, keyType } = verifyRaw(Buffer.from(digest, 'utf8'), signature, publicKeyPem);
|
|
78
|
+
if (error) return { state: 'invalid', reason: error };
|
|
79
|
+
return ok
|
|
80
|
+
? { state: 'verified', keyType }
|
|
81
|
+
: { state: 'invalid', reason: 'signature does not match the image digest under the supplied public key' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// DSSE Pre-Authentication Encoding: "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body.
|
|
85
|
+
function dssePae(payloadType, payloadBytes) {
|
|
86
|
+
const typeBytes = Buffer.from(payloadType, 'utf8');
|
|
87
|
+
return Buffer.concat([
|
|
88
|
+
Buffer.from('DSSEv1 ', 'ascii'),
|
|
89
|
+
Buffer.from(String(typeBytes.length), 'ascii'),
|
|
90
|
+
Buffer.from(' ', 'ascii'),
|
|
91
|
+
typeBytes,
|
|
92
|
+
Buffer.from(' ', 'ascii'),
|
|
93
|
+
Buffer.from(String(payloadBytes.length), 'ascii'),
|
|
94
|
+
Buffer.from(' ', 'ascii'),
|
|
95
|
+
payloadBytes,
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const SLSA_PREDICATE_PREFIX = 'https://slsa.dev/provenance/';
|
|
100
|
+
const INTOTO_STATEMENT_TYPES = new Set(['https://in-toto.io/Statement/v0.1', 'https://in-toto.io/Statement/v1']);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Verifies an in-toto/SLSA provenance attestation for an image.
|
|
104
|
+
* `envelope` is a DSSE envelope: `{ payloadType, payload (base64 in-toto
|
|
105
|
+
* Statement JSON), signatures: [{ sig (base64) }] }` — the real shape
|
|
106
|
+
* cosign/Sigstore attestations use. `publicKeyPem` verifies one of
|
|
107
|
+
* `signatures` against `dssePae(payloadType, payload)`.
|
|
108
|
+
*
|
|
109
|
+
* The subject-digest match is checked FIRST and independently of signature
|
|
110
|
+
* verification, so a mismatched subject is always `invalid` (never
|
|
111
|
+
* "unavailable", which would understate a real, checkable problem).
|
|
112
|
+
*/
|
|
113
|
+
function verifyProvenanceAttestation({ digest, envelope, publicKeyPem }) {
|
|
114
|
+
if (!envelope) return { state: 'missing', reason: 'no in-toto/SLSA provenance attestation was supplied for this image' };
|
|
115
|
+
|
|
116
|
+
let env;
|
|
117
|
+
try {
|
|
118
|
+
env = typeof envelope === 'string' ? JSON.parse(envelope) : envelope;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return { state: 'invalid', reason: `attestation envelope is not valid JSON: ${err.message}` };
|
|
121
|
+
}
|
|
122
|
+
if (typeof env.payload !== 'string' || typeof env.payloadType !== 'string' || !Array.isArray(env.signatures)) {
|
|
123
|
+
return { state: 'invalid', reason: 'not a recognized DSSE envelope (missing payload, payloadType, or signatures)' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let payloadBytes;
|
|
127
|
+
let stmt;
|
|
128
|
+
try {
|
|
129
|
+
payloadBytes = Buffer.from(env.payload, 'base64');
|
|
130
|
+
stmt = JSON.parse(payloadBytes.toString('utf8'));
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return { state: 'invalid', reason: `attestation payload is not a valid base64-encoded JSON statement: ${err.message}` };
|
|
133
|
+
}
|
|
134
|
+
if (!INTOTO_STATEMENT_TYPES.has(stmt._type)) {
|
|
135
|
+
return { state: 'invalid', reason: 'statement is missing a recognized in-toto "_type"' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const digestHex = typeof digest === 'string' && digest.startsWith('sha256:') ? digest.slice('sha256:'.length) : digest;
|
|
139
|
+
const subjects = Array.isArray(stmt.subject) ? stmt.subject : [];
|
|
140
|
+
const subjectMatches = subjects.some((s) => s && s.digest && s.digest.sha256 === digestHex);
|
|
141
|
+
if (!subjectMatches) {
|
|
142
|
+
return { state: 'invalid', reason: 'attestation subject digest does not match this image', predicateType: stmt.predicateType || null };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const predicateType = stmt.predicateType || null;
|
|
146
|
+
const isSlsa = typeof predicateType === 'string' && predicateType.startsWith(SLSA_PREDICATE_PREFIX);
|
|
147
|
+
|
|
148
|
+
if (!publicKeyPem || env.signatures.length === 0) {
|
|
149
|
+
return {
|
|
150
|
+
state: 'unavailable',
|
|
151
|
+
reason: 'attestation subject matches this image, but no public key was supplied to verify its signature',
|
|
152
|
+
predicateType, isSlsa,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const pae = dssePae(env.payloadType, payloadBytes);
|
|
157
|
+
for (const sigEntry of env.signatures) {
|
|
158
|
+
if (!sigEntry || typeof sigEntry.sig !== 'string') continue;
|
|
159
|
+
let sigBytes;
|
|
160
|
+
try {
|
|
161
|
+
sigBytes = Buffer.from(sigEntry.sig, 'base64');
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const { ok } = verifyRaw(pae, sigBytes, publicKeyPem);
|
|
166
|
+
if (ok) return { state: 'verified', predicateType, isSlsa };
|
|
167
|
+
}
|
|
168
|
+
return { state: 'invalid', reason: 'no signature in the attestation envelope verifies under the supplied public key', predicateType, isSlsa };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
/***/ })
|
|
173
|
+
|
|
174
|
+
};
|
package/dist/4142.index.js
CHANGED
|
@@ -712,13 +712,13 @@ function loadLedger(scanRoot) {
|
|
|
712
712
|
function saveLedger(scanRoot, ledger) {
|
|
713
713
|
if (!(0,state_dir/* ensureStateDir */.xH)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
714
714
|
const f = (0,state_dir.statePath)(scanRoot, DIR, 'ledger.json');
|
|
715
|
-
return { ok: (0,state_dir
|
|
715
|
+
return { ok: (0,state_dir.safeWriteState)(f, JSON.stringify(ledger, null, 2)) };
|
|
716
716
|
}
|
|
717
717
|
|
|
718
718
|
function saveIR(scanRoot, ir) {
|
|
719
719
|
if (!(0,state_dir/* ensureStateDir */.xH)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
720
720
|
const f = external_node_path_.join(ruleDir(scanRoot, ir.id), 'ir.json');
|
|
721
|
-
return { ok: (0,state_dir
|
|
721
|
+
return { ok: (0,state_dir.safeWriteState)(f, JSON.stringify(ir, null, 2)), file: f };
|
|
722
722
|
}
|
|
723
723
|
|
|
724
724
|
function loadIR(scanRoot, id) {
|
|
@@ -729,7 +729,7 @@ function loadIR(scanRoot, id) {
|
|
|
729
729
|
function saveEvaluation(scanRoot, id, evaluation) {
|
|
730
730
|
if (!(0,state_dir/* ensureStateDir */.xH)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
731
731
|
const f = external_node_path_.join(ruleDir(scanRoot, id), 'evaluation.json');
|
|
732
|
-
return { ok: (0,state_dir
|
|
732
|
+
return { ok: (0,state_dir.safeWriteState)(f, JSON.stringify(evaluation, null, 2)) };
|
|
733
733
|
}
|
|
734
734
|
|
|
735
735
|
/** Write the regression fixtures a promoted rule needs: the SAME vulnerable/clean convention `runRuleTests`
|
|
@@ -741,11 +741,11 @@ function writeFixtures(scanRoot, ir, corpus) {
|
|
|
741
741
|
const written = [];
|
|
742
742
|
for (const p of corpus.positives) {
|
|
743
743
|
const f = external_node_path_.join(base, 'vulnerable', `${p.label.replace(/[^a-z0-9_-]/gi, '_')}.${ext}`);
|
|
744
|
-
if ((0,state_dir
|
|
744
|
+
if ((0,state_dir.safeWriteState)(f, p.code)) written.push(f);
|
|
745
745
|
}
|
|
746
746
|
for (const n of corpus.negatives) {
|
|
747
747
|
const f = external_node_path_.join(base, 'clean', `${n.label.replace(/[^a-z0-9_-]/gi, '_')}.${ext}`);
|
|
748
|
-
if ((0,state_dir
|
|
748
|
+
if ((0,state_dir.safeWriteState)(f, n.code)) written.push(f);
|
|
749
749
|
}
|
|
750
750
|
return { ok: true, written, glob: external_node_path_.join(base, '**', '*') };
|
|
751
751
|
}
|
|
@@ -772,7 +772,7 @@ function publishCustomRule(scanRoot, ir) {
|
|
|
772
772
|
` message: ${JSON.stringify(doc.message)}`,
|
|
773
773
|
'',
|
|
774
774
|
].join('\n');
|
|
775
|
-
return { ok: (0,state_dir
|
|
775
|
+
return { ok: (0,state_dir.safeWriteState)(f, yamlText), file: f };
|
|
776
776
|
}
|
|
777
777
|
|
|
778
778
|
function unpublishCustomRule(scanRoot, id) {
|
package/dist/5014.index.js
CHANGED
|
@@ -136,7 +136,7 @@ function addLegalHold(scanRoot, { artifact, owner, reason, expires_at } = {}) {
|
|
|
136
136
|
const holds = _loadRaw(scanRoot);
|
|
137
137
|
holds.push(hold);
|
|
138
138
|
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE);
|
|
139
|
-
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__
|
|
139
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(fp, JSON.stringify(holds, null, 2) + '\n')) {
|
|
140
140
|
return { ok: false, reason: 'state writes are disabled (--no-state) or this is not a safe state directory' };
|
|
141
141
|
}
|
|
142
142
|
return { ok: true, hold };
|
|
@@ -152,7 +152,7 @@ function removeLegalHold(scanRoot, artifact) {
|
|
|
152
152
|
const removedCount = holds.length - remaining.length;
|
|
153
153
|
if (removedCount > 0) {
|
|
154
154
|
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE);
|
|
155
|
-
(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__
|
|
155
|
+
(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(fp, JSON.stringify(remaining, null, 2) + '\n');
|
|
156
156
|
}
|
|
157
157
|
return removedCount;
|
|
158
158
|
}
|