@blamejs/exceptd-skills 0.19.32 → 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 +22 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +8 -8
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +7 -7
- package/data/_indexes/chains.json +60118 -0
- package/data/attack-techniques.json +267 -7
- package/data/cve-catalog.json +9991 -3
- package/data/cwe-catalog.json +109 -2
- package/data/framework-control-gaps.json +578 -3
- package/data/zeroday-lessons.json +8330 -1
- 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 +2 -2
- package/sbom.cdx.json +253 -253
- 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,19 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* tree, identifies the artifact files (env / auth-config / ssh-keys /
|
|
8
|
-
* iac-credential-bearers), runs the catalogued regex set against text
|
|
9
|
-
* file contents, and stats permission posture on secret-carrier
|
|
10
|
-
* files. Emits a submission with deterministic signal_overrides per
|
|
11
|
-
* indicator that fired.
|
|
12
|
-
*
|
|
13
|
-
* Scope: any cwd. Cross-platform (Windows / macOS / Linux).
|
|
14
|
-
* Permission-posture indicator only meaningful on POSIX hosts.
|
|
15
|
-
*
|
|
16
|
-
* Interface: see lib/collectors/README.md
|
|
4
|
+
* Companion collector for the `secrets` playbook: walks the cwd tree, classifies
|
|
5
|
+
* artifact files, regex-scans text content, and stats permission posture on the
|
|
6
|
+
* carriers (POSIX only). Interface: lib/collectors/README.md
|
|
17
7
|
*/
|
|
18
8
|
|
|
19
9
|
const fs = require("node:fs");
|
|
@@ -22,19 +12,13 @@ const { codeExcludeSet, walkTree, buildEvidenceLocations, lineFromOffset } = req
|
|
|
22
12
|
|
|
23
13
|
const COLLECTOR_ID = "secrets";
|
|
24
14
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the shared code-scope policy (dependency caches, build output, VCS +
|
|
28
|
-
// agent/editor scratch including `.claude/`); no secrets-specific extras.
|
|
15
|
+
// Depth and exclusions mirror the secrets playbook's `look.artifacts[repo-tree]`;
|
|
16
|
+
// the exclusions come from the shared code-scope policy.
|
|
29
17
|
const DEFAULT_MAX_DEPTH = 6;
|
|
30
18
|
const DEFAULT_EXCLUDES = codeExcludeSet();
|
|
31
19
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// block in `cosign-test.key` or a JWT literal in `_test.go` is
|
|
35
|
-
// expected test material, not a real secret. If at least one hit
|
|
36
|
-
// exists outside these paths, the indicator still fires. Mirrors
|
|
37
|
-
// the crypto-codebase collector's isTestPath shape.
|
|
20
|
+
// Hits confined to these paths are downgraded; one hit outside them still fires.
|
|
21
|
+
// Mirrors the crypto-codebase collector's isTestPath.
|
|
38
22
|
const TEST_PATH_SEGMENTS = [
|
|
39
23
|
"/test/", "/tests/", "/spec/", "/specs/", "/__tests__/",
|
|
40
24
|
"/fixtures/", "/fixture/", "/examples/", "/example/",
|
|
@@ -47,12 +31,9 @@ function isTestPath(rel) {
|
|
|
47
31
|
for (const seg of TEST_PATH_SEGMENTS) {
|
|
48
32
|
if (norm.includes(seg)) return true;
|
|
49
33
|
}
|
|
50
|
-
// foo.test.js / bar.spec.py — dot-separated convention.
|
|
51
34
|
if (/\.(test|spec)\.[a-z]+$/i.test(rel)) return true;
|
|
52
|
-
// foo_test.go (Go) / bar_test.py (some Python) — underscore convention.
|
|
53
35
|
if (/(?:^|[\\/])[^\\/]+_test\.[a-z]+$/i.test(rel)) return true;
|
|
54
|
-
//
|
|
55
|
-
// key extension (e.g. cosign-test.key, github-test-token.json).
|
|
36
|
+
// "test" inside the name itself: cosign-test.key, github-test-token.json.
|
|
56
37
|
if (/-test[-.][^\\/]*$/i.test(rel)) return true;
|
|
57
38
|
return false;
|
|
58
39
|
}
|
|
@@ -73,11 +54,8 @@ const AUTH_CONFIG_FILES = new Set([
|
|
|
73
54
|
|
|
74
55
|
const SSH_PRIVATE_KEY_FILES = new Set(["id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"]);
|
|
75
56
|
const SSH_PRIVATE_KEY_EXTS = new Set([".pem", ".key", ".p12", ".pfx"]);
|
|
76
|
-
// `.pem` / `.key` are ambiguous
|
|
77
|
-
//
|
|
78
|
-
// carries no private key. A name-only classifier flags these as private keys.
|
|
79
|
-
// `.p12` / `.pfx` are binary keystores (private material by nature) and the
|
|
80
|
-
// id_* names are private SSH keys, so only the PEM extensions are content-gated.
|
|
57
|
+
// `.pem` / `.key` are ambiguous — a cert chain is conventionally `.pem` — so only
|
|
58
|
+
// those two are content-gated; `.p12` / `.pfx` and id_* are private by nature.
|
|
81
59
|
const PEM_PRIVATE_KEY_MARKER = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/;
|
|
82
60
|
const CONTENT_GATED_KEY_EXTS = new Set([".pem", ".key"]);
|
|
83
61
|
function carriesPrivateKey(file) {
|
|
@@ -88,15 +66,9 @@ function carriesPrivateKey(file) {
|
|
|
88
66
|
fd = fs.openSync(file.full, "r");
|
|
89
67
|
const st = fs.fstatSync(fd); // fstat on the open fd — no stat-then-read race
|
|
90
68
|
if (st.size > MAX_FILE_BYTES) return true; // too big to scan → conservative
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// past that boundary — e.g. a .pem with a leading `Bag Attributes` /
|
|
95
|
-
// subject= header block, the default OpenSSL pkcs12→PEM layout — the marker
|
|
96
|
-
// test would return false and silently drop a real private key. readFileSync
|
|
97
|
-
// on the open fd loops internally until EOF, so the test always sees the
|
|
98
|
-
// complete content. Reading from the already-open fd keeps the no-TOCTOU
|
|
99
|
-
// open→fstat ordering (no second path lookup).
|
|
69
|
+
// readFileSync on the open fd, never readSync: a short read NUL-pads the tail,
|
|
70
|
+
// and a BEGIN marker past that boundary (a .pem with a leading `Bag Attributes`
|
|
71
|
+
// header) would test false and drop a real key. Also preserves open→fstat order.
|
|
100
72
|
const content = fs.readFileSync(fd, "utf8");
|
|
101
73
|
return PEM_PRIVATE_KEY_MARKER.test(content);
|
|
102
74
|
} catch {
|
|
@@ -110,37 +82,21 @@ const IAC_EXTS = new Set([".tf", ".tfvars", ".bicep"]);
|
|
|
110
82
|
const IAC_EXACT = new Set(["terraform.tfstate", "values.yaml", "secret.yaml"]);
|
|
111
83
|
const IAC_GLOB_PREFIX = ["pulumi.", "arm."];
|
|
112
84
|
|
|
113
|
-
//
|
|
114
|
-
// detect.indicators[]
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// the runner would compute.
|
|
118
|
-
// AWS-published documentation/example access-key IDs. These appear verbatim
|
|
119
|
-
// throughout AWS docs, SDK samples, and countless READMEs, so a literal match
|
|
120
|
-
// is example material, not a leaked credential. `cred-stores` demotes the same
|
|
121
|
-
// value (its FP[0]); secrets.js must too or it false-positives on any README
|
|
122
|
-
// that quotes the AWS docs. The 40-char example secret
|
|
123
|
-
// (`wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`) carries the literal `EXAMPLE`
|
|
124
|
-
// token, which the AWS-secret-access-key pattern already requires elsewhere;
|
|
125
|
-
// the access-key ID is the one that needs an explicit allowlist.
|
|
85
|
+
// The patterns and demotions below mirror data/playbooks/secrets.json's
|
|
86
|
+
// detect.indicators[] — the playbook is the source of truth for what counts as a hit.
|
|
87
|
+
// AWS's published example access-key ID appears verbatim across docs and READMEs;
|
|
88
|
+
// `cred-stores` demotes the same value in its FP[0].
|
|
126
89
|
const AWS_EXAMPLE_ACCESS_KEY_IDS = new Set([
|
|
127
90
|
"AKIAIOSFODNN7EXAMPLE",
|
|
128
91
|
]);
|
|
129
92
|
|
|
130
|
-
// AWS
|
|
131
|
-
// throughout the AWS docs). aws-secret-access-key false_positive_checks_required[1]
|
|
132
|
-
// demotes this exact value.
|
|
93
|
+
// AWS's published sample secret key; aws-secret-access-key FP[1] demotes it.
|
|
133
94
|
const AWS_EXAMPLE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
|
|
134
95
|
|
|
135
|
-
//
|
|
136
|
-
// A literal `PLACEHOLDER` / `EXAMPLE` / `XXXX` run / a `dummy`/`test` infix
|
|
137
|
-
// marks documentation material rather than a live credential.
|
|
96
|
+
// Substrings the API-key indicators' FP[0] checks demote as documentation material.
|
|
138
97
|
const PLACEHOLDER_RE = /placeholder|example|redacted|dummy|x{4,}|0{6,}|1234567890/i;
|
|
139
98
|
|
|
140
|
-
//
|
|
141
|
-
// fixture material in addition to TEST_PATH_SEGMENTS (which already covers
|
|
142
|
-
// /examples/, /fixtures/, /test/ etc.). The secret indicators' FP prose adds
|
|
143
|
-
// /docs/ and quickstart/snippet paths.
|
|
99
|
+
// Documentation paths the FP checks treat as fixture material, atop TEST_PATH_SEGMENTS.
|
|
144
100
|
const DOC_PATH_SEGMENTS = [
|
|
145
101
|
"/docs/", "/doc/", "/sdk-quickstart/", "/quickstart/", "/docs-snippet/",
|
|
146
102
|
];
|
|
@@ -151,13 +107,9 @@ function isDocOrTestPath(rel) {
|
|
|
151
107
|
return DOC_PATH_SEGMENTS.some((seg) => norm.includes(seg));
|
|
152
108
|
}
|
|
153
109
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// survives. Indices requiring network reachability or operator judgement are
|
|
158
|
-
// deliberately omitted so the runner honestly downgrades to inconclusive.
|
|
159
|
-
// `value` is the matched credential, `file` the relative path, `window` a
|
|
160
|
-
// few-line context slice around the match.
|
|
110
|
+
// The false_positive_checks_required indices (as strings) this hit survives. Indices
|
|
111
|
+
// needing network reachability or operator judgement are omitted, so the runner
|
|
112
|
+
// downgrades to inconclusive rather than over-attesting. `window` is a context slice.
|
|
161
113
|
function fpIndicesSatisfied(indicatorId, value, file, window) {
|
|
162
114
|
const sat = new Set();
|
|
163
115
|
const notDocPath = !isDocOrTestPath(file);
|
|
@@ -165,51 +117,37 @@ function fpIndicesSatisfied(indicatorId, value, file, window) {
|
|
|
165
117
|
case "aws-secret-access-key": {
|
|
166
118
|
// [0] co-occurrence with an AKIA*/ASIA*/AGPA*/AIDA* id in a 10-line window
|
|
167
119
|
if (/\b(?:AKIA|ASIA|AGPA|AIDA)[0-9A-Z]{12,}\b/.test(window)) sat.add("0");
|
|
168
|
-
// [1] not the AWS-published sample secret
|
|
169
120
|
if (value !== AWS_EXAMPLE_SECRET_KEY) sat.add("1");
|
|
170
|
-
// [2] not under examples/ docs/ fixtures/ / a test snapshot
|
|
171
121
|
if (notDocPath) sat.add("2");
|
|
172
122
|
break;
|
|
173
123
|
}
|
|
174
124
|
case "slack-bot-or-user-token": {
|
|
175
|
-
// [0] not a placeholder / published doc fixture
|
|
176
125
|
if (!PLACEHOLDER_RE.test(value)) sat.add("0");
|
|
177
|
-
// [1]
|
|
178
|
-
// dash-separated segments after the xox? prefix
|
|
126
|
+
// [1] current Slack shape: 3+ dash-separated segments after the prefix
|
|
179
127
|
if (value.split("-").length >= 4) sat.add("1");
|
|
180
|
-
// [2] not under examples/ docs/ fixtures/
|
|
181
128
|
if (notDocPath) sat.add("2");
|
|
182
129
|
break;
|
|
183
130
|
}
|
|
184
131
|
case "stripe-secret-key": {
|
|
185
|
-
// [0] not a sk_test_
|
|
186
|
-
// test prefix; live keys are handled by [2])
|
|
132
|
+
// [0] not a published sk_test_ sample; live keys fall to [2]
|
|
187
133
|
if (!(value.startsWith("sk_test_") && PLACEHOLDER_RE.test(value))) sat.add("0");
|
|
188
|
-
// [1] not under examples/ fixtures/ docs/ / a quickstart template
|
|
189
134
|
if (notDocPath) sat.add("1");
|
|
190
|
-
// [2]
|
|
191
|
-
// key carries no live financial exposure, so the check is moot and
|
|
192
|
-
// the collector can attest it deterministically. sk_live_* still
|
|
193
|
-
// needs operator-authorised network validation — left unattested.
|
|
135
|
+
// [2] live-validity probe is moot for a test key; sk_live_* needs operator auth
|
|
194
136
|
if (value.startsWith("sk_test_") || value.startsWith("rk_test_")) sat.add("2");
|
|
195
137
|
break;
|
|
196
138
|
}
|
|
197
139
|
case "openai-api-key": {
|
|
198
|
-
// [0] not a placeholder / sk-test- / sk-dummy- fixture
|
|
199
140
|
if (!PLACEHOLDER_RE.test(value) && !/^sk-(?:test|dummy)-/i.test(value)) sat.add("0");
|
|
200
|
-
// [1] post-prefix length meets the entropy floor
|
|
141
|
+
// [1] post-prefix length meets the entropy floor
|
|
201
142
|
if (value.replace(/^sk-(?:proj-|svcacct-|admin-)?/, "").length >= 48) sat.add("1");
|
|
202
143
|
// [2] vendor disambiguation — sk-ant-* is Anthropic, not OpenAI
|
|
203
144
|
if (!/^sk-ant-/i.test(value)) sat.add("2");
|
|
204
145
|
break;
|
|
205
146
|
}
|
|
206
147
|
case "anthropic-api-key": {
|
|
207
|
-
// [0] not a placeholder / sk-ant-test- fixture
|
|
208
148
|
if (!PLACEHOLDER_RE.test(value) && !/^sk-ant-test-/i.test(value)) sat.add("0");
|
|
209
|
-
// [1] not under examples/ fixtures/ sdk-quickstart/ docs-snippet/
|
|
210
149
|
if (notDocPath) sat.add("1");
|
|
211
|
-
// [2] post-prefix length meets the entropy floor
|
|
212
|
-
// sk-ant-(api03|admin01)-)
|
|
150
|
+
// [2] post-prefix length meets the entropy floor
|
|
213
151
|
if (value.replace(/^sk-ant-(?:api03|admin01)-/, "").length >= 80) sat.add("2");
|
|
214
152
|
break;
|
|
215
153
|
}
|
|
@@ -222,33 +160,22 @@ function fpIndicesSatisfied(indicatorId, value, file, window) {
|
|
|
222
160
|
const INDICATOR_PATTERNS = [
|
|
223
161
|
{ id: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
224
162
|
{ id: "aws-secret-access-key", re: /\baws_secret_access_key\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi },
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// does not register as a real embedded key. A real key is JSON-encoded with
|
|
229
|
-
// `\n` escapes, so the body class includes backslash; `-` is excluded so the
|
|
230
|
-
// run halts at `-----END` (no backtracking, ReDoS-safe). Mirrors
|
|
231
|
-
// ssh-private-key-block below.
|
|
163
|
+
// A full PEM block, not just the BEGIN header, so a doc placeholder or a DLP
|
|
164
|
+
// library's own detection pattern does not register. The body class admits
|
|
165
|
+
// backslash for JSON `\n` escapes and excludes `-` so the run halts — ReDoS-safe.
|
|
232
166
|
{ id: "gcp-service-account-json", re: /"type"\s*:\s*"service_account"[\s\S]{0,1200}?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[A-Za-z0-9+/=\s\\]{40,4000}-----END/g },
|
|
233
167
|
{ id: "github-personal-access-token", re: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
234
168
|
{ id: "github-fine-grained-pat", re: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
|
|
235
169
|
{ id: "slack-bot-or-user-token", re: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g },
|
|
236
170
|
{ id: "stripe-secret-key", re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
|
|
237
171
|
{ id: "jwt-token-with-secret-context", re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// register as an embedded key. `-` is excluded from the body class so the
|
|
242
|
-
// run halts at the first `-` of `-----END` (no backtracking, ReDoS-safe).
|
|
243
|
-
// Private-key *files* (.pem/.key) are detected separately by file presence.
|
|
172
|
+
// A complete PEM block, so a bare BEGIN header used as a DLP library's own
|
|
173
|
+
// detection pattern does not register. `-` excluded from the body class halts
|
|
174
|
+
// the run at `-----END` — no backtracking. Key *files* are found by presence.
|
|
244
175
|
{ id: "ssh-private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED |)PRIVATE KEY-----[A-Za-z0-9+/=\s]{20,4000}-----END/g },
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// openai-api-key and anthropic-api-key — inflating the hit count and
|
|
249
|
-
// mislabeling the vendor. The optional `(?:proj-|svcacct-|admin-)?` group
|
|
250
|
-
// still admits every real OpenAI shape (proj/svcacct/admin and the bare
|
|
251
|
-
// legacy `sk-` key); the lookahead is anchored, so no backtracking / ReDoS.
|
|
176
|
+
// `(?!ant-)` stops an Anthropic key double-firing as openai-api-key too. The
|
|
177
|
+
// optional prefix group still admits every real OpenAI shape, including the bare
|
|
178
|
+
// legacy `sk-` key; the lookahead is anchored, so no ReDoS.
|
|
252
179
|
{ id: "openai-api-key", re: /\bsk-(?!ant-)(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
253
180
|
{ id: "anthropic-api-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
254
181
|
];
|
|
@@ -310,12 +237,8 @@ function redactMatch(literal) {
|
|
|
310
237
|
function scanContent(full, rel) {
|
|
311
238
|
let buf;
|
|
312
239
|
try {
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
// the hot path; reading the buffer and measuring it is byte-accurate (the
|
|
316
|
-
// cap is a byte limit) without the extra stat. Reading as a Buffer rather
|
|
317
|
-
// than decoding to a string up front means an oversized file is rejected
|
|
318
|
-
// before any UTF-8 decode work.
|
|
240
|
+
// The cap is a byte limit, so measure the Buffer rather than stat first —
|
|
241
|
+
// byte-accurate, and oversize is rejected before any UTF-8 decode.
|
|
319
242
|
const raw = fs.readFileSync(full);
|
|
320
243
|
if (raw.length > MAX_FILE_BYTES) return { skipped: "file_too_large", bytes: raw.length, hits: [] };
|
|
321
244
|
buf = raw.toString("utf8");
|
|
@@ -327,26 +250,21 @@ function scanContent(full, rel) {
|
|
|
327
250
|
const matches = buf.matchAll(p.re);
|
|
328
251
|
let count = 0;
|
|
329
252
|
for (const m of matches) {
|
|
330
|
-
//
|
|
331
|
-
// AKIAIOSFODNN7EXAMPLE). A README quoting the AWS docs must not hit.
|
|
253
|
+
// A README quoting the AWS docs must not hit.
|
|
332
254
|
if (p.id === "aws-access-key-id" && AWS_EXAMPLE_ACCESS_KEY_IDS.has(m[0])) continue;
|
|
333
|
-
//
|
|
334
|
-
// (aws_secret_access_key=<value>); otherwise the whole match.
|
|
255
|
+
// Group 1 when the pattern brackets the credential; else the whole match.
|
|
335
256
|
const value = m[1] != null ? m[1] : m[0];
|
|
336
|
-
// ±600
|
|
337
|
-
// used by the co-occurrence FP check.
|
|
257
|
+
// ±600 bytes — a deterministic proxy for "nearby lines" in the FP check.
|
|
338
258
|
const winStart = Math.max(0, m.index - 600);
|
|
339
259
|
const window = buf.slice(winStart, m.index + 600);
|
|
340
260
|
hits.push({
|
|
341
261
|
indicator_id: p.id,
|
|
342
262
|
file: rel,
|
|
343
263
|
offset: m.index,
|
|
344
|
-
// 1-based
|
|
345
|
-
// (SARIF startLine) instead of a bare file-level location.
|
|
264
|
+
// 1-based, so buildEvidenceLocations emits a SARIF startLine region.
|
|
346
265
|
line: lineFromOffset(buf, m.index),
|
|
347
266
|
redacted_match: redactMatch(m[0]),
|
|
348
|
-
//
|
|
349
|
-
// survives — attested so the runner doesn't downgrade hit → inconclusive.
|
|
267
|
+
// Attested so the runner does not downgrade hit → inconclusive.
|
|
350
268
|
fp_satisfied: fpIndicesSatisfied(p.id, value, rel, window),
|
|
351
269
|
});
|
|
352
270
|
if (++count >= 5) break; // cap per-indicator-per-file
|
|
@@ -368,10 +286,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
368
286
|
errors.push({ kind: "walk_failed", reason: e.message });
|
|
369
287
|
files = [];
|
|
370
288
|
}
|
|
371
|
-
// Subtrees pruned
|
|
372
|
-
//
|
|
373
|
-
// not a silent miss indistinguishable from "scanned, found nothing" —
|
|
374
|
-
// symmetric with the per-file `file_too_large_skipped` reporting below.
|
|
289
|
+
// Subtrees pruned at the depth cap are NOT scanned; surface them so an unscanned
|
|
290
|
+
// deep .env or id_rsa is an observable gap, not a silent miss.
|
|
375
291
|
if (truncations.length) {
|
|
376
292
|
errors.push({
|
|
377
293
|
artifact_id: "repo-tree",
|
|
@@ -423,9 +339,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
423
339
|
if (r.skipped === "read_error") {
|
|
424
340
|
errors.push({ artifact_id: "secret-regex-scan-text-files", kind: "read_failed", reason: `${f.rel}: ${r.reason}` });
|
|
425
341
|
} else if (r.skipped === "file_too_large") {
|
|
426
|
-
//
|
|
427
|
-
// dropped silently. Record the skip so the operator knows this file
|
|
428
|
-
// was NOT scanned (mirrors crypto-codebase's >1 MB read_failed entry).
|
|
342
|
+
// Record the skip, or a secret in an oversized file is dropped silently.
|
|
429
343
|
errors.push({
|
|
430
344
|
artifact_id: "secret-regex-scan-text-files",
|
|
431
345
|
kind: "file_too_large_skipped",
|
|
@@ -434,9 +348,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
434
348
|
}
|
|
435
349
|
}
|
|
436
350
|
|
|
437
|
-
//
|
|
438
|
-
// only when at least one PROD hit exists. Test-only hits stay in
|
|
439
|
-
// the artifact for operator inspection but don't flip the signal.
|
|
351
|
+
// The indicator fires only on a PROD hit; test-path hits stay in the artifact.
|
|
440
352
|
const hitsByIndicator = {};
|
|
441
353
|
const prodHitsByIndicator = {};
|
|
442
354
|
for (const h of allHits) {
|
|
@@ -445,23 +357,16 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
445
357
|
(prodHitsByIndicator[h.indicator_id] = prodHitsByIndicator[h.indicator_id] || []).push(h);
|
|
446
358
|
}
|
|
447
359
|
}
|
|
448
|
-
//
|
|
449
|
-
// artifact backs the ssh-private-key-block content scan AND the
|
|
450
|
-
// ssh-key-bad-perms posture check below). Filter out test-named
|
|
451
|
-
// private-key files (e.g. cosign-test.key) for the signal too.
|
|
360
|
+
// The same split for the file-presence indicators, so a fixture key raises neither.
|
|
452
361
|
const prodSshPrivateKeys = sshPrivateKeys.filter(f => !isTestPath(f.rel));
|
|
453
362
|
|
|
454
363
|
const signal_overrides = {};
|
|
455
364
|
for (const p of INDICATOR_PATTERNS) {
|
|
456
365
|
signal_overrides[p.id] = prodHitsByIndicator[p.id] && prodHitsByIndicator[p.id].length > 0 ? "hit" : "miss";
|
|
457
366
|
}
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// intersection — an index is only universally true if no hit fails it).
|
|
462
|
-
// Network / operator-judgement indices are never in the set, so the runner
|
|
463
|
-
// still downgrades indicators that carry one. Without this, a real secret
|
|
464
|
-
// surfaced by `collect` is downgraded to inconclusive after `run`.
|
|
367
|
+
// For each fired indicator, attest the FP indices EVERY surviving hit satisfies —
|
|
368
|
+
// the intersection, since an index is universally true only if no hit fails it.
|
|
369
|
+
// Without this, a real secret found by `collect` is downgraded after `run`.
|
|
465
370
|
for (const p of INDICATOR_PATTERNS) {
|
|
466
371
|
if (signal_overrides[p.id] !== "hit") continue;
|
|
467
372
|
const hits = prodHitsByIndicator[p.id] || [];
|
|
@@ -478,46 +383,26 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
478
383
|
signal_overrides[`${p.id}__fp_checks`] = att;
|
|
479
384
|
}
|
|
480
385
|
}
|
|
481
|
-
//
|
|
482
|
-
// key file with the matching magic bytes counts even without a
|
|
483
|
-
// content scan match — e.g. binary-only key formats). Re-flip when
|
|
484
|
-
// any non-test private-key file was discovered.
|
|
386
|
+
// Also flipped by file presence: a binary-only key format has no content match.
|
|
485
387
|
if (prodSshPrivateKeys.length > 0) signal_overrides["ssh-private-key-block"] = "hit";
|
|
486
|
-
//
|
|
487
|
-
// restricted to env-files artifact entries
|
|
488
|
-
// any .env / .env.* / .envrc with mode 0666 or 0664 (group/world writable)
|
|
489
|
-
// i.e. group-write OR world-write bit set (mode & 0o022).
|
|
388
|
+
// Per the playbook: any env-files entry with the group-write or world-write bit set.
|
|
490
389
|
const envFilePostures = process.platform === "win32" ? [] : envFiles.map(f => ({ file: f.rel, ...statPosture(f.full) }));
|
|
491
|
-
// POSIX mode bits are unreadable on win32, so
|
|
492
|
-
//
|
|
493
|
-
// bad-perms) so the runner returns `inconclusive` rather than a deterministic
|
|
494
|
-
// false `miss` for a check that physically cannot run. (The posture array is
|
|
495
|
-
// still declared — empty on win32 — for the SARIF per-indicator locations.)
|
|
390
|
+
// POSIX mode bits are unreadable on win32, so the signal is OMITTED there — the
|
|
391
|
+
// runner returns `inconclusive`, not a false `miss`. Same as cred-stores' bad-perms.
|
|
496
392
|
if (process.platform !== "win32") {
|
|
497
393
|
signal_overrides["world-writable-env-file"] = envFilePostures.some(p => p.error == null && (p.mode & 0o022) !== 0) ? "hit" : "miss";
|
|
498
394
|
}
|
|
499
395
|
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
// any private-key file with mode != 0600
|
|
503
|
-
// The collector scope is the cwd; ~/.ssh enumeration is outside this
|
|
504
|
-
// walk root. Within cwd, flag any discovered private key whose mode
|
|
505
|
-
// is anything other than 0600 (strict). Use the test-path-filtered set
|
|
506
|
-
// (prodSshPrivateKeys) — matching ssh-private-key-block — so a fixture
|
|
507
|
-
// key checked in under a test/ path doesn't raise a bad-perms posture.
|
|
396
|
+
// Per the playbook: any private key whose mode is not exactly 0600. ~/.ssh lies
|
|
397
|
+
// outside the cwd walk root, so only keys discovered here are checked.
|
|
508
398
|
const sshKeyPostures = process.platform === "win32" ? [] : prodSshPrivateKeys.map(f => ({ file: f.rel, ...statPosture(f.full) }));
|
|
509
|
-
//
|
|
510
|
-
// runner returns inconclusive instead of a forced false `miss`.
|
|
399
|
+
// Omitted on win32 for the same reason as world-writable-env-file.
|
|
511
400
|
if (process.platform !== "win32") {
|
|
512
401
|
signal_overrides["ssh-key-bad-perms"] = sshKeyPostures.some(p => p.error == null && p.mode !== 0o600) ? "hit" : "miss";
|
|
513
402
|
}
|
|
514
403
|
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
// Content-regex hits carry a 1-based `line` (derived from the match offset),
|
|
518
|
-
// so these locations include a startLine region. The file-presence and
|
|
519
|
-
// posture indicators contribute the carrier file path directly (file-level,
|
|
520
|
-
// no line).
|
|
404
|
+
// Locations for every indicator flipped to "hit", so a SARIF result points at the
|
|
405
|
+
// carrying file. A content hit carries a line and becomes a region.
|
|
521
406
|
const evidence_locations = {};
|
|
522
407
|
for (const p of INDICATOR_PATTERNS) {
|
|
523
408
|
if (signal_overrides[p.id] === "hit") {
|
|
@@ -525,8 +410,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
525
410
|
if (locs.length) evidence_locations[p.id] = locs;
|
|
526
411
|
}
|
|
527
412
|
}
|
|
528
|
-
//
|
|
529
|
-
// the discovered key files alongside any content-scan hits, de-duplicated.
|
|
413
|
+
// Fold the discovered key files in alongside content hits, de-duplicated.
|
|
530
414
|
if (signal_overrides["ssh-private-key-block"] === "hit") {
|
|
531
415
|
const locs = buildEvidenceLocations([
|
|
532
416
|
...(prodHitsByIndicator["ssh-private-key-block"] || []),
|