@blamejs/exceptd-skills 0.19.33 → 0.19.34
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 +10 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* credential carriers (~/.aws/credentials, ~/.kube/config, ~/.docker/
|
|
8
|
-
* config.json, ~/.npmrc, ~/.pypirc, ~/.config/gcloud/application_
|
|
9
|
-
* default_credentials.json, project-level .npmrc / .pypirc), and
|
|
10
|
-
* flips signal_overrides for the deterministic indicators. Defers
|
|
11
|
-
* non-deterministic indicators (ssh-key-rsa-short-bits, ssh-key-old,
|
|
12
|
-
* gpg-key-old-or-weak, all-stores-empty-or-federated) so the runner
|
|
4
|
+
* Companion collector for the `cred-stores` playbook. Reads $HOME credential
|
|
5
|
+
* dotfiles plus project .npmrc / .pypirc under cwd, flipping signal_overrides
|
|
6
|
+
* for the deterministic indicators only — the rest stay unset so the runner
|
|
13
7
|
* returns inconclusive rather than a forced miss.
|
|
14
|
-
*
|
|
15
|
-
* Scope: $HOME credential dotfiles + project-level .npmrc / .pypirc
|
|
16
|
-
* under cwd. Posix-mode-bits indicators are skipped on win32 (ACL
|
|
17
|
-
* audit out of scope).
|
|
18
|
-
*
|
|
19
8
|
* Interface: see lib/collectors/README.md
|
|
20
9
|
*/
|
|
21
10
|
|
|
@@ -31,10 +20,8 @@ function readSafe(full, max = 512 * 1024) {
|
|
|
31
20
|
fd = fs.openSync(full, "r");
|
|
32
21
|
const s = fs.fstatSync(fd);
|
|
33
22
|
if (s.size > max) return null;
|
|
34
|
-
// readFileSync(fd) loops
|
|
35
|
-
//
|
|
36
|
-
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
37
|
-
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
23
|
+
// readFileSync(fd) loops to EOF — a single readSync can come back short on a
|
|
24
|
+
// network fd and NUL-fill the tail. Reading the open fd is also TOCTOU-free.
|
|
38
25
|
return fs.readFileSync(fd, "utf8");
|
|
39
26
|
} catch { return null; }
|
|
40
27
|
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
@@ -54,8 +41,6 @@ function fileExists(full) {
|
|
|
54
41
|
try { return fs.statSync(full).isFile(); } catch { return false; }
|
|
55
42
|
}
|
|
56
43
|
|
|
57
|
-
// AWS credentials INI: any [profile] block carrying
|
|
58
|
-
// `aws_access_key_id` AND no `sso_session` / `credential_process`.
|
|
59
44
|
function parseAwsCredentials(content) {
|
|
60
45
|
if (!content) return { staticProfiles: [], federatedProfiles: [], staticKeys: {} };
|
|
61
46
|
const lines = content.split(/\r?\n/);
|
|
@@ -77,22 +62,13 @@ function parseAwsCredentials(content) {
|
|
|
77
62
|
}
|
|
78
63
|
const staticProfiles = [];
|
|
79
64
|
const federatedProfiles = [];
|
|
80
|
-
// Per-
|
|
81
|
-
// the
|
|
82
|
-
// A duplicate profile name resolves to the LAST occurrence's keys here, which
|
|
83
|
-
// is the same precedence the AWS SDK applies.
|
|
65
|
+
// Per-profile aws_access_key_id. A duplicate profile name resolves to the last
|
|
66
|
+
// occurrence, the precedence the AWS SDK applies.
|
|
84
67
|
const staticKeys = {};
|
|
85
68
|
for (const [name, kv] of Object.entries(profiles)) {
|
|
86
69
|
const hasKey = !!kv["aws_access_key_id"];
|
|
87
|
-
// role_arn is
|
|
88
|
-
//
|
|
89
|
-
// canonical source_profile/assume-role setup where the static key IS the
|
|
90
|
-
// long-lived credential that bootstraps the assumed role — a real static-key
|
|
91
|
-
// exposure. The playbook's aws-static-key-present predicate lists only
|
|
92
|
-
// sso_session / credential_process as federation; treating role_arn as
|
|
93
|
-
// federation here suppressed a genuinely-present IAM user key. A pure
|
|
94
|
-
// assume-role profile (role_arn, no own access key) is still excluded by the
|
|
95
|
-
// hasKey gate below.
|
|
70
|
+
// role_arn is not federation: a static key alongside role_arn is the
|
|
71
|
+
// source_profile bootstrap. Federation is sso_session / credential_process.
|
|
96
72
|
const hasFederation = !!(kv["sso_session"] || kv["credential_process"]);
|
|
97
73
|
if (hasKey && !hasFederation) {
|
|
98
74
|
staticProfiles.push(name);
|
|
@@ -103,14 +79,10 @@ function parseAwsCredentials(content) {
|
|
|
103
79
|
return { staticProfiles, federatedProfiles, staticKeys };
|
|
104
80
|
}
|
|
105
81
|
|
|
106
|
-
// kubeconfig: users[].user.token
|
|
107
|
-
//
|
|
108
|
-
// than pulling a YAML parser into the stdlib-only contract.
|
|
82
|
+
// kubeconfig: a non-empty users[].user.token with no users[].user.exec sibling.
|
|
83
|
+
// Line-based rather than parsed, to stay inside the stdlib-only contract.
|
|
109
84
|
function parseKubeConfig(content) {
|
|
110
85
|
if (!content) return { hasStaticToken: false, hasExec: false };
|
|
111
|
-
// Find every users: block + each `- name: ...` user entry and
|
|
112
|
-
// its sub-keys. The kubeconfig schema is regular enough that a
|
|
113
|
-
// line-window scan is reliable.
|
|
114
86
|
const lines = content.split(/\r?\n/);
|
|
115
87
|
let inUsers = false;
|
|
116
88
|
let userIndent = -1;
|
|
@@ -129,7 +101,6 @@ function parseKubeConfig(content) {
|
|
|
129
101
|
continue;
|
|
130
102
|
}
|
|
131
103
|
if (buf.length) {
|
|
132
|
-
// If we leave the users list (dedent), close current block.
|
|
133
104
|
if (raw.trim() === "" || /^\S/.test(raw)) {
|
|
134
105
|
if (/^\S/.test(raw) && !/^users:/.test(raw)) {
|
|
135
106
|
blocks.push(buf.join("\n")); buf = [];
|
|
@@ -143,11 +114,8 @@ function parseKubeConfig(content) {
|
|
|
143
114
|
}
|
|
144
115
|
if (buf.length) blocks.push(buf.join("\n"));
|
|
145
116
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// `access-token`, `id-token`, `refresh-token` which are dynamic /
|
|
149
|
-
// cached tokens, not static-credential evidence. Use a line-prefix
|
|
150
|
-
// anchor + auth-provider-vs-user proximity check to refuse those.
|
|
117
|
+
// `token:` / `token-data:` counts only under `user:` — an auth-provider block's
|
|
118
|
+
// tokens are cached dynamic ones, not static-credential evidence.
|
|
151
119
|
let hasStaticToken = false;
|
|
152
120
|
let hasExec = false;
|
|
153
121
|
const userKvRe = /^(\s+)(token|token-data)\s*:\s*(\S[^\n]*)/gm;
|
|
@@ -157,16 +125,12 @@ function parseKubeConfig(content) {
|
|
|
157
125
|
let blockHasStatic = false;
|
|
158
126
|
for (const m of block.matchAll(userKvRe)) {
|
|
159
127
|
const upto = block.slice(0, m.index);
|
|
160
|
-
// Indentation-agnostic
|
|
161
|
-
//
|
|
162
|
-
// token under any other indentation and mislabeled it as an auth-provider
|
|
163
|
-
// (dynamic) token.
|
|
128
|
+
// Indentation-agnostic anchors: kubeconfig indent varies, and a fixed-width
|
|
129
|
+
// anchor mislabels a user-level static token as an auth-provider one.
|
|
164
130
|
const userMatches = [...upto.matchAll(/\n[ \t]*user\s*:/g)];
|
|
165
131
|
const lastUserAt = userMatches.length ? userMatches[userMatches.length - 1].index : -1;
|
|
166
132
|
const apMatches = [...upto.matchAll(/\n[ \t]*auth-provider\s*:/g)];
|
|
167
133
|
const lastAuthProviderAt = apMatches.length ? apMatches[apMatches.length - 1].index : -1;
|
|
168
|
-
// Reject when the closest enclosing key is auth-provider rather
|
|
169
|
-
// than user — those are dynamic tokens, not static credentials.
|
|
170
134
|
if (lastAuthProviderAt > lastUserAt) continue;
|
|
171
135
|
const value = m[3];
|
|
172
136
|
if (!value || value.startsWith("null")) continue;
|
|
@@ -236,7 +200,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
236
200
|
const presence = {};
|
|
237
201
|
for (const [id, p] of Object.entries(carriers)) presence[id] = fileExists(p);
|
|
238
202
|
|
|
239
|
-
// Read carriers we care about.
|
|
240
203
|
const awsCredsContent = presence["aws-credentials"] ? readSafe(carriers["aws-credentials"]) : null;
|
|
241
204
|
const awsCfgContent = presence["aws-config"] ? readSafe(carriers["aws-config"]) : null;
|
|
242
205
|
const kubeContent = presence["kube-config"] ? readSafe(carriers["kube-config"]) : null;
|
|
@@ -247,28 +210,18 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
247
210
|
const pypirProjContent = presence["pypirc-project"] ? readSafe(carriers["pypirc-project"]) : null;
|
|
248
211
|
const gcloudAdcContent = presence["gcloud-adc"] ? readSafe(carriers["gcloud-adc"]) : null;
|
|
249
212
|
|
|
250
|
-
// Indicator predicates.
|
|
251
213
|
const awsCredsParsed = parseAwsCredentials(awsCredsContent);
|
|
252
214
|
const awsCfgParsed = parseAwsCredentials(awsCfgContent);
|
|
253
215
|
const ssoCacheFiles = (() => {
|
|
254
216
|
try { return fs.readdirSync(ssoCacheDir).filter(f => f.endsWith(".json")); }
|
|
255
217
|
catch { return []; }
|
|
256
218
|
})();
|
|
257
|
-
// aws-static-key-present: any AKIA* key with no federation.
|
|
258
|
-
//
|
|
259
|
-
// and FP[2] (break-glass profile-name pattern) directly in the
|
|
260
|
-
// collector — they're deterministic and the collector has the
|
|
261
|
-
// evidence locally. FP[1] requires `aws sts get-caller-identity`
|
|
262
|
-
// which is out of stdlib scope, so the collector cannot attest it;
|
|
263
|
-
// the runner downgrades hit → inconclusive with that one
|
|
264
|
-
// unsatisfied, which is the honest outcome.
|
|
219
|
+
// aws-static-key-present: any AKIA* key with no federation. FP[0] doc-fixture
|
|
220
|
+
// key and FP[2] break-glass profile are deterministic and applied here.
|
|
265
221
|
const AWS_DOC_FIXTURE_KEY = "AKIAIOSFODNN7EXAMPLE";
|
|
266
222
|
const realAwsProfiles = awsCredsParsed.staticProfiles.filter(p => {
|
|
267
|
-
// Demote
|
|
268
|
-
//
|
|
269
|
-
// name matches — means a duplicate profile name whose first occurrence
|
|
270
|
-
// holds the doc-fixture key cannot demote the later real key under the
|
|
271
|
-
// same name (the parser resolves the live last-occurrence value).
|
|
223
|
+
// Demote on the exact parsed key, so a duplicate name whose first occurrence
|
|
224
|
+
// holds the fixture key cannot demote the real one.
|
|
272
225
|
const keyVal = awsCredsParsed.staticKeys[p];
|
|
273
226
|
if (keyVal === AWS_DOC_FIXTURE_KEY) return false; // FP[0]
|
|
274
227
|
if (/^breakglass-/i.test(p) || /^break-glass-/i.test(p)) return false; // FP[2]
|
|
@@ -280,15 +233,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
280
233
|
const gcloudParsed = parseGcloudAdc(gcloudAdcContent);
|
|
281
234
|
const dockerParsed = parseDockerConfig(dockerContent);
|
|
282
235
|
|
|
283
|
-
// docker-cleartext-auth FP checks
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// treat as a deliberately published convention, demote.
|
|
287
|
-
// FP[1] — local-only registry (loopback IP, *.local, *.svc.
|
|
288
|
-
// cluster.local, kind.local) on a dev workstation, demote.
|
|
289
|
-
// FP[2] — global credsStore overrides per-registry omission —
|
|
290
|
-
// the collector already accounts for this via
|
|
291
|
-
// parseDockerConfig.hasCredHelper.
|
|
236
|
+
// docker-cleartext-auth FP checks: FP[0] vendor-token user in the decoded
|
|
237
|
+
// `user:pass`, FP[1] local-only registry, FP[2] a global credsStore, which
|
|
238
|
+
// parseDockerConfig already folds into hasCredHelper.
|
|
292
239
|
const VENDOR_TOKEN_USERS = new Set([
|
|
293
240
|
"<token>", "AWS", "oauth2accesstoken",
|
|
294
241
|
"00000000-0000-0000-0000-000000000000",
|
|
@@ -324,10 +271,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
324
271
|
(pypirHomeContent && PYPI_TOKEN_RE.test(pypirHomeContent)) ||
|
|
325
272
|
(pypirProjContent && PYPI_TOKEN_RE.test(pypirProjContent));
|
|
326
273
|
|
|
327
|
-
// credentials-file-bad-perms: POSIX only
|
|
328
|
-
// carriers with mode != 0600. Per playbook the indicator covers
|
|
329
|
-
// `~/.config/gcloud/*` too, so include the gcloud ADC file (and
|
|
330
|
-
// its parent dir mode != 0700 expectation per the spec).
|
|
274
|
+
// credentials-file-bad-perms: POSIX only — carriers at 0600, the gcloud dir at 0700.
|
|
331
275
|
let credsFileBadPerms;
|
|
332
276
|
const permViolations = [];
|
|
333
277
|
if (isPosix) {
|
|
@@ -342,8 +286,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
342
286
|
];
|
|
343
287
|
for (const [id, p, expectedMode] of permTargets) {
|
|
344
288
|
if (!presence[id]) continue;
|
|
345
|
-
// FP[1]: 0-byte placeholder
|
|
346
|
-
//
|
|
289
|
+
// FP[1]: a 0-byte placeholder or a symlink to a broker socket carries no
|
|
290
|
+
// blast radius in its mode bits.
|
|
347
291
|
let lstat;
|
|
348
292
|
try { lstat = fs.lstatSync(p); } catch { continue; }
|
|
349
293
|
if (lstat.size === 0) continue;
|
|
@@ -354,7 +298,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
354
298
|
permViolations.push({ id, mode_octal: "0" + m.toString(8) });
|
|
355
299
|
}
|
|
356
300
|
}
|
|
357
|
-
// Also check the gcloud directory itself (expected 0700 per spec).
|
|
358
301
|
const gcloudDir = path.join(home, ".config", "gcloud");
|
|
359
302
|
try {
|
|
360
303
|
const gs = fs.statSync(gcloudDir);
|
|
@@ -380,28 +323,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
380
323
|
signal_overrides["credentials-file-bad-perms"] = credsFileBadPerms;
|
|
381
324
|
}
|
|
382
325
|
|
|
383
|
-
// Per-indicator __fp_checks attestation
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// the checks the collector itself ran (don't attest network /
|
|
387
|
-
// operator-judgement checks). Use the index-keyed form because
|
|
388
|
-
// false_positive_checks_required entries are free-text prose, not
|
|
389
|
-
// ids — the index is the stable cross-reference.
|
|
390
|
-
//
|
|
391
|
-
// aws-static-key-present:
|
|
392
|
-
// [0] doc-fixture demotion (AKIAIOSFODNN7EXAMPLE) — DONE
|
|
393
|
-
// [1] live-key sts check — NOT DONE (needs network)
|
|
394
|
-
// [2] break-glass profile-name pattern — DONE
|
|
395
|
-
//
|
|
396
|
-
// docker-cleartext-auth:
|
|
397
|
-
// [0] vendor-token user pattern — DONE
|
|
398
|
-
// [1] local-only registry — DONE
|
|
399
|
-
// [2] global credsStore — DONE
|
|
400
|
-
//
|
|
401
|
-
// credentials-file-bad-perms:
|
|
402
|
-
// [0] Windows / WSL skip — DONE (POSIX guard)
|
|
403
|
-
// [1] 0-byte / symlink skip — DONE
|
|
404
|
-
// [2] ACL-by-design (operator interview) — NOT DONE
|
|
326
|
+
// Per-indicator __fp_checks attestation: the runner downgrades a 'hit' to
|
|
327
|
+
// 'inconclusive' for every unattested false_positive_checks_required[] entry.
|
|
328
|
+
// Keys are indices into it; aws[1] (network) and perms[2] stay unattested.
|
|
405
329
|
if (awsStaticKey) {
|
|
406
330
|
signal_overrides["aws-static-key-present__fp_checks"] = { "0": true, "2": true };
|
|
407
331
|
}
|
|
@@ -412,11 +336,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
412
336
|
signal_overrides["credentials-file-bad-perms__fp_checks"] = { "0": true, "1": true };
|
|
413
337
|
}
|
|
414
338
|
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// populate the ones the collector actually reads; the rest are
|
|
418
|
-
// marked captured=false with a "reason" so the runner records
|
|
419
|
-
// partial-evidence coverage rather than a phantom miss.
|
|
339
|
+
// One entry per artifact id in data/playbooks/cred-stores.json look.artifacts[].
|
|
340
|
+
// What cannot be read is captured=false with a reason, not a phantom miss.
|
|
420
341
|
const artifacts = {
|
|
421
342
|
"aws-credentials": presence["aws-credentials"]
|
|
422
343
|
? { value: `present (${awsCredsParsed.staticProfiles.length} static profile(s), ${awsCredsParsed.federatedProfiles.length} federated)`, captured: true }
|
|
@@ -470,12 +391,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
470
391
|
captured: false,
|
|
471
392
|
reason: "secret-tool / security dump-keychain require interactive auth or platform-specific binaries; out of stdlib collector scope",
|
|
472
393
|
},
|
|
473
|
-
//
|
|
474
|
-
// indicator was considered but skipped, mirroring the secrets
|
|
475
|
-
// collector's world-writable-secret-files skip artifact. Without
|
|
476
|
-
// this, credentials-file-bad-perms silently absent from
|
|
477
|
-
// signal_overrides looks indistinguishable from "indicator not
|
|
478
|
-
// catalogued".
|
|
394
|
+
// Names the win32 skip explicitly; an absent override would look uncatalogued.
|
|
479
395
|
"credentials-file-perms-check": isPosix
|
|
480
396
|
? {
|
|
481
397
|
value: permViolations.length > 0
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* cwd tree, grepping source files for hash / cipher / KEX / signature
|
|
8
|
-
* / KDF / RNG / TLS / PQC / FIPS call sites. Flips signal_overrides
|
|
9
|
-
* only for indicators whose verdict can be determined deterministically
|
|
10
|
-
* from the codebase scan; behavioral indicators that require operator
|
|
11
|
-
* judgement (e.g. crypto-agility abstraction shape) are left unflipped
|
|
12
|
-
* so the runner returns inconclusive rather than a forced miss.
|
|
13
|
-
*
|
|
14
|
-
* Interface: see lib/collectors/README.md
|
|
4
|
+
* Companion collector for the `crypto-codebase` playbook: greps the cwd source
|
|
5
|
+
* tree for hash / cipher / KEX / signature / KDF / RNG / TLS / PQC / FIPS call
|
|
6
|
+
* sites. Interface: see lib/collectors/README.md
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
const fs = require("node:fs");
|
|
@@ -21,10 +13,7 @@ const { codeExcludeSet, walkTree, buildEvidenceLocations, lineFromOffset } = req
|
|
|
21
13
|
const COLLECTOR_ID = "crypto-codebase";
|
|
22
14
|
|
|
23
15
|
const DEFAULT_MAX_DEPTH = 6;
|
|
24
|
-
// Shared code-scope exclusions
|
|
25
|
-
// agent/editor scratch (including `.claude/`). No crypto-codebase-specific
|
|
26
|
-
// extras — the shared defaults already cover every directory this scan
|
|
27
|
-
// should never descend into.
|
|
16
|
+
// Shared code-scope exclusions; no collector-specific extras needed.
|
|
28
17
|
const DEFAULT_EXCLUDES = codeExcludeSet();
|
|
29
18
|
|
|
30
19
|
const SOURCE_EXTS = new Set([
|
|
@@ -41,12 +30,9 @@ const SOURCE_EXTS = new Set([
|
|
|
41
30
|
".m", ".mm",
|
|
42
31
|
]);
|
|
43
32
|
|
|
44
|
-
// The exact marker set the
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// scanned cwd — NOT by counting SOURCE_EXTS files — so a valid package repo
|
|
48
|
-
// whose only marker is a manifest or an empty src/ dir (no extension-matched
|
|
49
|
-
// source yet) still attests true, matching what the gate would compute.
|
|
33
|
+
// The exact marker set the playbook's `repo-has-source-tree` gate evaluates
|
|
34
|
+
// (data/playbooks/crypto-codebase.json: exists_any([...])). Mirrored here, NOT
|
|
35
|
+
// counted from SOURCE_EXTS files, so an empty src/ attests what the gate computes.
|
|
50
36
|
const SOURCE_TREE_MARKERS = [
|
|
51
37
|
"package.json", "pyproject.toml", "go.mod", "Cargo.toml",
|
|
52
38
|
"pom.xml", "build.gradle", "src", "lib", "crates",
|
|
@@ -57,10 +43,7 @@ const TEST_PATH_SEGMENTS = [
|
|
|
57
43
|
"/fixtures/", "/fixture/", "/examples/", "/example/",
|
|
58
44
|
"/docs/", "/doc/", "/sample/", "/samples/", "/demo/", "/demos/",
|
|
59
45
|
"/benchmarks/", "/benchmark/", "/bench/",
|
|
60
|
-
//
|
|
61
|
-
// literally contains the patterns, so production-scope scans would
|
|
62
|
-
// match the scanner itself. The crypto-codebase playbook's intent
|
|
63
|
-
// is the consumer's source, not the scanner's regex catalogue.
|
|
46
|
+
// These files contain the scan patterns; scanning them matches the scanner.
|
|
64
47
|
"/lib/collectors/", "/scripts/check-version-tags",
|
|
65
48
|
];
|
|
66
49
|
|
|
@@ -71,19 +54,15 @@ function isTestPath(rel) {
|
|
|
71
54
|
for (const seg of TEST_PATH_SEGMENTS) {
|
|
72
55
|
if (norm.includes(seg)) return true;
|
|
73
56
|
}
|
|
74
|
-
// `foo.test.js`, `bar.spec.py` (dot-separated)
|
|
75
57
|
if (/\.(test|spec)\.[a-z]+$/i.test(rel)) return true;
|
|
76
|
-
// `foo_test.go`
|
|
58
|
+
// Go's `foo_test.go` convention.
|
|
77
59
|
if (/(?:^|[\\/])[^\\/]+_test\.[a-z]+$/i.test(rel)) return true;
|
|
78
60
|
return false;
|
|
79
61
|
}
|
|
80
62
|
|
|
81
63
|
function readSafe(full) {
|
|
82
64
|
try {
|
|
83
|
-
//
|
|
84
|
-
// Replaces a statSync-before-read on the hot path with a single read; the
|
|
85
|
-
// cap is byte-based, so Buffer.length is the correct measure and an
|
|
86
|
-
// oversized file is rejected before any UTF-8 decode.
|
|
65
|
+
// The cap is byte-based: enforced on the buffer, before any UTF-8 decode.
|
|
87
66
|
const raw = fs.readFileSync(full);
|
|
88
67
|
if (raw.length > MAX_FILE_BYTES) return null;
|
|
89
68
|
return raw.toString("utf8");
|
|
@@ -91,25 +70,15 @@ function readSafe(full) {
|
|
|
91
70
|
}
|
|
92
71
|
|
|
93
72
|
const WEAK_HASH_RE = /(?:crypto\.createHash\(\s*['"](?:md5|sha1|sha-1)['"]|hashlib\.(?:md5|sha1)\s*\(|MessageDigest\.getInstance\(\s*['"](?:MD5|SHA-1|SHA1)['"]|crypto\/(?:md5|sha1)\b|Digest::(?:MD5|SHA1)\b)/i;
|
|
94
|
-
// Token vocabulary
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// "integrity" was previously in this set but matched non-security
|
|
99
|
-
// integrity contexts (Hugo's content-integrity fingerprinting, sphinx
|
|
100
|
-
// docs build-ids, etag generators) — too broad. Drop it; the
|
|
101
|
-
// remaining vocabulary still catches crypto-security uses.
|
|
73
|
+
// Token vocabulary signalling a SECURITY-CRITICAL use of the hash primitive, as
|
|
74
|
+
// opposed to fingerprinting / cache-key / build-id use where MD5 and SHA-1 are
|
|
75
|
+
// legitimate. "integrity" stays out: it also matches fingerprinting and etags.
|
|
102
76
|
const WEAK_HASH_VAR_FLOW_RE = /(hmac|sign|signature|token|jwt|verify|password|hash[-_]?(?:credential|secret|password|key|auth))/i;
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// filename — a file named `hashing.go` that genuinely uses MD5 on
|
|
106
|
-
// a `password` / `token` / `jwt` is a real positive, no matter
|
|
107
|
-
// what the filename suggests.
|
|
77
|
+
// Unambiguous security-context tokens: any of these fires the hit regardless of
|
|
78
|
+
// the filename — `hashing.go` running MD5 over a `password` is a real positive.
|
|
108
79
|
const STRONG_SECURITY_VAR_RE = /\b(token|password|jwt|secret|credential|api[-_]?key|access[-_]?key|private[-_]?key)\b/i;
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// Demotion fires only when STRONG_SECURITY_VAR_RE did NOT match —
|
|
112
|
-
// the filename is a tiebreaker, never an override.
|
|
80
|
+
// Paths suggesting fingerprint / etag / cache-key concerns. Demotion fires only
|
|
81
|
+
// when STRONG_SECURITY_VAR_RE did NOT match: the filename is a tiebreaker only.
|
|
113
82
|
const NON_SECURITY_HASH_FILE_RE = /(?:^|[\\/])(?:integrity|hashing|fingerprint|content[-_]?hash|cache[-_]?key|etag|build[-_]?id)\.(?:go|py|rs|java|js|ts|rb|php|cs|swift|m|cpp|cc|c|h|hpp)$/i;
|
|
114
83
|
|
|
115
84
|
const WEAK_CIPHER_ECB_RE = /(?:aes-\d+-ecb|AES\/ECB\/|Cipher\.getInstance\(\s*['"]AES['"]\s*\))/i;
|
|
@@ -124,31 +93,17 @@ const SECURITY_VAR_RE = /\b(?:token|secret|key|salt|nonce|iv|seed|state|jwt|jti|
|
|
|
124
93
|
const PBKDF2_BLOCK_GLOBAL_RE = /\b(?:pbkdf2(?:Sync)?|hashlib\.pbkdf2_hmac)\s*\([^)]{0,400}/g;
|
|
125
94
|
|
|
126
95
|
const BCRYPT_BLOCK_GLOBAL_RE = /\b(?:bcrypt\.(?:hash|hashSync|gen_salt|genSalt|genSaltSync)|BCrypt::Password\.create)\s*\([^)]{0,200}/g;
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// `)`, so the trailing-int branch must match `, <digits>` followed
|
|
131
|
-
// by either end-of-block or whitespace, not a closing paren.
|
|
96
|
+
// Named-arg form (`cost: 12` / `rounds=12`) or the positional trailing integer.
|
|
97
|
+
// The captured block truncates at the first `)`, so the trailing-int branch ends
|
|
98
|
+
// at end-of-block, not at a paren.
|
|
132
99
|
const BCRYPT_COST_RE = /(?:cost|rounds)\s*[:=]\s*(\d+)|,\s*(\d+)\s*$/;
|
|
133
100
|
|
|
134
|
-
// `hardcoded-key-material`
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// header is `PRIVATE KEY` only.
|
|
141
|
-
// - Requiring the base64 body + `END` marker distinguishes a real pasted
|
|
142
|
-
// key from a bare marker used as a *detection pattern* (a redaction
|
|
143
|
-
// library's `/-----BEGIN OPENSSH PRIVATE KEY-----/` regex literal) or a
|
|
144
|
-
// documentation placeholder (`privateKeyPem: "-----BEGIN PRIVATE KEY-----
|
|
145
|
-
// ..."`), neither of which carries a body or an `END`.
|
|
146
|
-
//
|
|
147
|
-
// The body class is base64 + whitespace only; `-` is excluded so the run
|
|
148
|
-
// halts at the first `-` of `-----END` — no backtracking, ReDoS-safe. The
|
|
149
|
-
// `-----END` requirement is the primary discriminator (detectors and
|
|
150
|
-
// placeholders have no closing marker); the {20,4000} bound additionally
|
|
151
|
-
// rejects the ~1-char run a bare marker leaves before the next punctuation.
|
|
101
|
+
// `hardcoded-key-material` matches only a full PEM block: a `BEGIN ... PRIVATE
|
|
102
|
+
// KEY` header, a base64 body, and a matching `END`. Public keys and certificates
|
|
103
|
+
// are published by design, so the header is PRIVATE KEY only; requiring the body
|
|
104
|
+
// and the `END` separates a pasted key from a bare marker in a detection pattern
|
|
105
|
+
// or a placeholder. The body class excludes `-`, so the run halts at the first
|
|
106
|
+
// `-` of `-----END`, with no backtracking.
|
|
152
107
|
const PEM_PRIVATE_RE = /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----[A-Za-z0-9+/=\s]{20,4000}-----END/;
|
|
153
108
|
|
|
154
109
|
const TLS_OLD_PROTO_RE = /(?:secureProtocol\s*:\s*['"](?:TLSv1_method|TLSv1_1_method|SSLv23_method|SSLv3_method)['"]|minVersion\s*:\s*['"]TLSv1(?:\.0|\.1)?['"]|ssl_version\s*=\s*ssl\.PROTOCOL_TLSv1(?:_1)?|MinTlsVersion::TLSv1\b)/i;
|
|
@@ -167,17 +122,9 @@ const VENDORED_PQC_NAMES_RE = /(?:kyber|dilithium|sphincs|ml[-_]?kem|ml[-_]?dsa|
|
|
|
167
122
|
|
|
168
123
|
function scanWeakHash(content, rel) {
|
|
169
124
|
if (!WEAK_HASH_RE.test(content)) return false;
|
|
170
|
-
// Strong security tokens fire the indicator regardless of the
|
|
171
|
-
// filename — a file named `integrity.go` that genuinely uses
|
|
172
|
-
// md5 on a password is a real positive.
|
|
173
125
|
if (STRONG_SECURITY_VAR_RE.test(content)) return true;
|
|
174
|
-
// Otherwise the var-flow regex needs to match (hash / hmac /
|
|
175
|
-
// sign / signature / verify / ...). If neither STRONG nor
|
|
176
|
-
// WEAK_HASH_VAR_FLOW fired, this isn't a security-context use.
|
|
177
126
|
if (!WEAK_HASH_VAR_FLOW_RE.test(content)) return false;
|
|
178
|
-
//
|
|
179
|
-
// as a tiebreaker: paths like `integrity.go` / `hashing.go` /
|
|
180
|
-
// `fingerprint.py` indicate content-addressable use, demote.
|
|
127
|
+
// Only a soft keyword matched, so the filename is the tiebreaker.
|
|
181
128
|
if (rel && NON_SECURITY_HASH_FILE_RE.test(rel)) return false;
|
|
182
129
|
return true;
|
|
183
130
|
}
|
|
@@ -202,11 +149,9 @@ function scanPbkdf2(content) {
|
|
|
202
149
|
let threshold = 210000;
|
|
203
150
|
if (/sha[-_]?256/i.test(block)) threshold = 600000;
|
|
204
151
|
else if (/sha1\b/i.test(block) || /sha-1\b/i.test(block)) threshold = 1300000;
|
|
205
|
-
//
|
|
206
|
-
// pre-
|
|
207
|
-
//
|
|
208
|
-
// iterated at 1024 and must hit. The max() picks iteration over
|
|
209
|
-
// keylen in the typical positional shape (iter, keylen, algo).
|
|
152
|
+
// The max 4+ digit literal is the iteration count. Common key-bit sizes are
|
|
153
|
+
// deliberately not pre-filtered — `pbkdf2Sync(pw, salt, 1024, 32, 'sha256')`
|
|
154
|
+
// IS under-iterated — and max() picks iteration over keylen in (iter, keylen).
|
|
210
155
|
const nums = [];
|
|
211
156
|
for (const nm of block.matchAll(/\b(\d{4,8})\b/g)) {
|
|
212
157
|
nums.push(Number(nm[1]));
|
|
@@ -297,9 +242,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
297
242
|
if (RSA_1024_RE.test(content)) {
|
|
298
243
|
hits["rsa-1024-anywhere"].push({ file: f.rel });
|
|
299
244
|
}
|
|
300
|
-
//
|
|
301
|
-
// location carries a SARIF startLine region rather than pointing at
|
|
302
|
-
// the file. Does not change hit/miss — the same matches still fire.
|
|
245
|
+
// The 1-based `line` gives the evidence location a SARIF startLine region.
|
|
303
246
|
const mrHits = scanMathRandom(content);
|
|
304
247
|
for (const h of mrHits) hits["math-random-in-security-path"].push({ file: f.rel, offset: h.offset, line: lineFromOffset(content, h.offset) });
|
|
305
248
|
|
|
@@ -317,11 +260,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
317
260
|
}
|
|
318
261
|
}
|
|
319
262
|
|
|
320
|
-
// Cross-file evidence for the conditional indicators
|
|
321
|
-
//
|
|
322
|
-
// without-runtime-activation). Production-context only — a
|
|
323
|
-
// PQC / FIPS reference inside `tests/` / `fixtures/` / `examples/`
|
|
324
|
-
// doesn't count as evidence the library SHIPS that capability.
|
|
263
|
+
// Cross-file evidence for the conditional indicators, production context only:
|
|
264
|
+
// a PQC / FIPS reference in tests is not evidence the library SHIPS it.
|
|
325
265
|
if (!isTest) {
|
|
326
266
|
if (CLASSICAL_SIG_RE.test(content)) sawClassicalSig = true;
|
|
327
267
|
if (PQC_SIG_IMPL_RE.test(content)) sawPqcSigImpl = true;
|
|
@@ -348,19 +288,13 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
348
288
|
const vendoredPqcFiles = files.filter(f => isVendored(f.rel) && VENDORED_PQC_NAMES_RE.test(f.rel));
|
|
349
289
|
let vendoredPqcNoProvenance = "miss";
|
|
350
290
|
if (vendoredPqcFiles.length > 0) {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
// files already classified as vendored, so a MANIFEST.json found while
|
|
355
|
-
// climbing the vendor tree is a genuine provenance marker, not a stray
|
|
356
|
-
// app/package manifest at the repo root.
|
|
291
|
+
// MANIFEST.json is a common provenance record for a vendored tree. The walk
|
|
292
|
+
// runs only over already-vendored files, so a hit is a marker, not the root
|
|
293
|
+
// app manifest.
|
|
357
294
|
const provenanceMarkers = new Set(["_PROVENANCE.json", "UPSTREAM", "ORIGIN", ".upstream-commit", "PROVENANCE.md", "MANIFEST.json"]);
|
|
358
|
-
// Walk from the file's directory up to the repo root
|
|
359
|
-
//
|
|
360
|
-
// the
|
|
361
|
-
// the vendor root (`vendor/_PROVENANCE.json`), or anywhere in
|
|
362
|
-
// between for deeply-nested vendor trees. Stop at the repo root
|
|
363
|
-
// (cwd) so we don't escape into the parent filesystem.
|
|
295
|
+
// Walk from the file's directory up to the repo root: the marker may sit
|
|
296
|
+
// beside the file, at the vendor root, or between. Stopping at the root
|
|
297
|
+
// keeps the search inside the repo.
|
|
364
298
|
let anyMissing = false;
|
|
365
299
|
for (const f of vendoredPqcFiles) {
|
|
366
300
|
let dir = path.dirname(f.full);
|
|
@@ -405,28 +339,22 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
405
339
|
if (noMlKemImpl !== undefined) signal_overrides["no-ml-kem-implementation"] = noMlKemImpl;
|
|
406
340
|
if (fipsTheater !== undefined) signal_overrides["fips-claim-without-runtime-activation"] = fipsTheater;
|
|
407
341
|
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// (scanWeakHash). The remaining false_positive_checks_required entries are
|
|
412
|
-
// legacy-protocol-shim / feature-flag / later-override judgements the
|
|
413
|
-
// collector does not make, so they stay unattested and the runner keeps
|
|
414
|
-
// those indicators inconclusive. Without attesting what it DID check, the
|
|
415
|
-
// collector's real call-site hits are downgraded to inconclusive after run.
|
|
342
|
+
// __fp_checks attest what the collector actually checked: every surviving hit is
|
|
343
|
+
// in a non-test source file and, for weak-hash, flows into a security sink. The
|
|
344
|
+
// unattested entries are judgements it does not make, and stay inconclusive.
|
|
416
345
|
if (signal_overrides["weak-hash-import"] === "hit") {
|
|
417
|
-
// [0] not under test
|
|
418
|
-
// authn/integrity sink. [1] legacy-protocol shim is operator judgement.
|
|
346
|
+
// [0] not under test, plus non-security-file demotion; [2] the hash flows to
|
|
347
|
+
// an authn/integrity sink. [1] legacy-protocol shim is operator judgement.
|
|
419
348
|
signal_overrides["weak-hash-import__fp_checks"] = { "0": true, "2": true };
|
|
420
349
|
}
|
|
421
350
|
if (signal_overrides["weak-cipher-mode"] === "hit") {
|
|
422
|
-
// [0] not under test/KAT-vector path; [2] construction is in a
|
|
423
|
-
//
|
|
351
|
+
// [0] not under a test / KAT-vector path; [2] the construction is in a
|
|
352
|
+
// production source file. [1] legacy-protocol-parser scope is operator.
|
|
424
353
|
signal_overrides["weak-cipher-mode__fp_checks"] = { "0": true, "2": true };
|
|
425
354
|
}
|
|
426
355
|
if (signal_overrides["tls-old-protocol"] === "hit") {
|
|
427
|
-
// [0] the hit is
|
|
428
|
-
//
|
|
429
|
-
// [2] later-override are not inspected by the collector.
|
|
356
|
+
// [0] the hit is production code, not a test asserting the library REJECTS the
|
|
357
|
+
// legacy protocol. [1] feature-flag default and [2] later override are not.
|
|
430
358
|
signal_overrides["tls-old-protocol__fp_checks"] = { "0": true };
|
|
431
359
|
}
|
|
432
360
|
|
|
@@ -488,15 +416,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
488
416
|
},
|
|
489
417
|
};
|
|
490
418
|
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
// vendored-pqc-no-provenance) describe a whole-repo state rather than a
|
|
495
|
-
// single offending file, so they carry no file-level location. The
|
|
496
|
-
// offset-bearing call-site scans (math-random / pbkdf2 / bcrypt) now record
|
|
497
|
-
// a 1-based `line`, so their locations include a startLine region; the
|
|
498
|
-
// remaining whole-file scans (weak-hash / weak-cipher / rsa-1024 /
|
|
499
|
-
// hardcoded-key / tls) stay file-level (no startLine).
|
|
419
|
+
// File locations for the call-site indicators flipped to "hit". The cross-file
|
|
420
|
+
// derived indicators describe a whole-repo state, so they carry no location; the
|
|
421
|
+
// offset-bearing scans record a line, the whole-file scans stay file-level.
|
|
500
422
|
const evidence_locations = {};
|
|
501
423
|
for (const id of Object.keys(hits)) {
|
|
502
424
|
if (signal_overrides[id] === "hit") {
|
|
@@ -507,15 +429,10 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
507
429
|
|
|
508
430
|
return {
|
|
509
431
|
precondition_checks: {
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
// exists_any() branch, so a repo with a recognizable source tree would
|
|
515
|
-
// otherwise surface a spurious precondition_unverified warning. Keying to
|
|
516
|
-
// the gate's exact id + predicate mirrors the sbom / library-author
|
|
517
|
-
// collectors; `repo-context` is not a precondition this playbook
|
|
518
|
-
// references.
|
|
432
|
+
// Mirrors the playbook's own exists_any(SOURCE_TREE_MARKERS) predicate
|
|
433
|
+
// against the scanned cwd. The runner's autoDetectPreconditions probes the
|
|
434
|
+
// run process's cwd and has no exists_any branch, so without this a real
|
|
435
|
+
// source tree surfaces a spurious precondition_unverified warning.
|
|
519
436
|
"repo-has-source-tree": SOURCE_TREE_MARKERS.some((m) => fs.existsSync(path.join(cwd, m))),
|
|
520
437
|
},
|
|
521
438
|
artifacts,
|