@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,19 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* files; applies the catalogued indicator predicates against their
|
|
9
|
-
* contents.
|
|
10
|
-
*
|
|
11
|
-
* YAML parsing strategy: heuristic line-scanning, not a full YAML
|
|
12
|
-
* parser. The catalogued indicators (privileged: true, hostNetwork:
|
|
13
|
-
* true, runAsUser: 0, etc.) are well-known text patterns whose
|
|
14
|
-
* misuse is unambiguous at the line level. False positives are rare
|
|
15
|
-
* — e.g. `# privileged: true` in a comment would match, but those
|
|
16
|
-
* are also worth surfacing.
|
|
4
|
+
* Companion collector for the `containers` playbook: walks the cwd for
|
|
5
|
+
* Dockerfile / Containerfile, compose and k8s manifest files and applies the
|
|
6
|
+
* catalogued indicator predicates. YAML is line-scanned, never parsed — every
|
|
7
|
+
* catalogued indicator is an unambiguous single-line pattern.
|
|
17
8
|
*
|
|
18
9
|
* Interface: see lib/collectors/README.md
|
|
19
10
|
*/
|
|
@@ -25,8 +16,7 @@ const { codeExcludeSet, walkTree, buildEvidenceLocations } = require("./scan-exc
|
|
|
25
16
|
const COLLECTOR_ID = "containers";
|
|
26
17
|
|
|
27
18
|
const DEFAULT_MAX_DEPTH = 6;
|
|
28
|
-
// Shared code-scope exclusions
|
|
29
|
-
// agent/editor scratch including `.claude/`); no container-specific extras.
|
|
19
|
+
// Shared code-scope exclusions; no container-specific extras.
|
|
30
20
|
const DEFAULT_EXCLUDES = codeExcludeSet();
|
|
31
21
|
|
|
32
22
|
const DOCKERFILE_NAMES = new Set(["Dockerfile", "Containerfile"]);
|
|
@@ -55,21 +45,14 @@ function classify(file) {
|
|
|
55
45
|
|
|
56
46
|
function readSafe(full, max = 512 * 1024) {
|
|
57
47
|
try {
|
|
58
|
-
//
|
|
59
|
-
// Replaces a statSync-before-read with a single read; the cap is
|
|
60
|
-
// byte-based, so Buffer.length is the correct measure and an oversized
|
|
61
|
-
// file is rejected before any UTF-8 decode.
|
|
48
|
+
// Byte-based cap on Buffer.length: oversized files are rejected before decode.
|
|
62
49
|
const raw = fs.readFileSync(full);
|
|
63
50
|
if (raw.length > max) return null;
|
|
64
51
|
return raw.toString("utf8");
|
|
65
52
|
} catch { return null; }
|
|
66
53
|
}
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
* Recognise a YAML document as a k8s resource by `apiVersion: ...` +
|
|
70
|
-
* `kind: ...` lines. Doesn't require full parsing — the two lines
|
|
71
|
-
* appear at top-level indent.
|
|
72
|
-
*/
|
|
55
|
+
// Top-level `apiVersion:` + `kind:` is enough to recognise one without parsing.
|
|
73
56
|
function looksLikeK8sManifest(content) {
|
|
74
57
|
if (!/^apiVersion:\s+\S/m.test(content)) return false;
|
|
75
58
|
if (!/^kind:\s+\S/m.test(content)) return false;
|
|
@@ -81,9 +64,8 @@ function extractKind(content) {
|
|
|
81
64
|
return m ? m[1] : null;
|
|
82
65
|
}
|
|
83
66
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*/
|
|
67
|
+
// Returns { <indicator id>: [{ file, line, snippet }] }; line 0 means the hit
|
|
68
|
+
// is whole-file rather than a specific line.
|
|
87
69
|
function scanDockerfile(content, rel) {
|
|
88
70
|
const lines = content.split(/\r?\n/);
|
|
89
71
|
const hits = {
|
|
@@ -93,12 +75,8 @@ function scanDockerfile(content, rel) {
|
|
|
93
75
|
"dockerfile-curl-pipe-bash": [],
|
|
94
76
|
};
|
|
95
77
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// / WORKDIR / USER directives, it's not a runtime image. Examples:
|
|
99
|
-
// the go-version-scraping Dockerfile (cosign style) or a base-image-
|
|
100
|
-
// probe used only by `docker build` to extract a version label.
|
|
101
|
-
// The runs-as-root predicate is meaningless on those — demote.
|
|
78
|
+
// A FROM line with no execution-shape directive is a build-time probe, not a
|
|
79
|
+
// runtime image; the runs-as-root predicate means nothing there.
|
|
102
80
|
const isMetadataOnly = (() => {
|
|
103
81
|
let sawFrom = false;
|
|
104
82
|
let sawBuildOrRuntime = false;
|
|
@@ -107,8 +85,7 @@ function scanDockerfile(content, rel) {
|
|
|
107
85
|
if (!t || t.startsWith("#")) continue;
|
|
108
86
|
if (/^FROM\b/i.test(t)) { sawFrom = true; continue; }
|
|
109
87
|
if (/^(RUN|COPY|ADD|CMD|ENTRYPOINT|EXPOSE|VOLUME|WORKDIR|USER|HEALTHCHECK|ONBUILD|STOPSIGNAL|SHELL|ARG|ENV|LABEL)\b/i.test(t)) {
|
|
110
|
-
// ARG / ENV / LABEL alone
|
|
111
|
-
// image — only the directives that define execution shape do.
|
|
88
|
+
// ARG / ENV / LABEL alone do not make a runtime image.
|
|
112
89
|
if (/^(RUN|COPY|ADD|CMD|ENTRYPOINT|EXPOSE|VOLUME|WORKDIR|USER|HEALTHCHECK|ONBUILD|SHELL)\b/i.test(t)) {
|
|
113
90
|
sawBuildOrRuntime = true;
|
|
114
91
|
break;
|
|
@@ -120,18 +97,12 @@ function scanDockerfile(content, rel) {
|
|
|
120
97
|
|
|
121
98
|
let sawNonRootUser = false;
|
|
122
99
|
let sawAnyUser = false;
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// registry image — it needs no tag/digest. Without tracking these, every
|
|
126
|
-
// `FROM build-stage` in a normal multi-stage Dockerfile false-fired
|
|
127
|
-
// from-latest / no-digest-pin.
|
|
100
|
+
// Aliases from `FROM <image> AS <alias>`. A later `FROM <alias>` is an internal
|
|
101
|
+
// multi-stage reference, not a registry image, and owes no tag or digest.
|
|
128
102
|
const stageAliases = new Set();
|
|
129
|
-
// ARG defaults
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// `FROM ${BASE}`) is a legitimate pinning pattern that was invisible to the
|
|
133
|
-
// raw-text check; and an unresolvable interpolation can't be proven to be
|
|
134
|
-
// `:latest`, so flagging it from-latest was a false positive.
|
|
103
|
+
// ARG defaults, used to resolve `FROM node:${NODE_VERSION}`. A digest pinned
|
|
104
|
+
// through one is real pinning, and an unresolvable interpolation cannot be
|
|
105
|
+
// proven to be `:latest`, so neither may be flagged from raw text.
|
|
135
106
|
const argDefaults = new Map();
|
|
136
107
|
const resolveArgs = (s) => {
|
|
137
108
|
let interpolatedUnresolved = false;
|
|
@@ -149,9 +120,7 @@ function scanDockerfile(content, rel) {
|
|
|
149
120
|
const trimmed = line.trim();
|
|
150
121
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
151
122
|
|
|
152
|
-
//
|
|
153
|
-
// interpolates the name can resolve it. Quotes around the default are
|
|
154
|
-
// stripped so `ARG BASE="img@sha256:..."` resolves cleanly.
|
|
123
|
+
// `ARG NAME=default` and bare `ARG NAME`; quotes come off the default.
|
|
155
124
|
const argMatch = trimmed.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)(?:=(.*))?\s*$/i);
|
|
156
125
|
if (argMatch) {
|
|
157
126
|
const name = argMatch[1];
|
|
@@ -163,57 +132,39 @@ function scanDockerfile(content, rel) {
|
|
|
163
132
|
continue;
|
|
164
133
|
}
|
|
165
134
|
|
|
166
|
-
// FROM <image>:latest OR FROM <image> (no tag) → from-latest
|
|
167
|
-
// FROM <image>:<tag> (no @sha256:digest) → no-digest-pin
|
|
168
135
|
const fromMatch = trimmed.match(/^FROM\s+(\S+)(?:\s+AS\s+(\S+))?/i);
|
|
169
136
|
if (fromMatch) {
|
|
170
137
|
const rawRef = fromMatch[1];
|
|
171
138
|
const alias = fromMatch[2];
|
|
172
139
|
const { resolved: ref, interpolatedUnresolved } = resolveArgs(rawRef);
|
|
173
|
-
//
|
|
174
|
-
// build stage (internal multi-stage reference — not a registry image, so
|
|
175
|
-
// a tag/digest is neither present nor required).
|
|
140
|
+
// `scratch` and stage aliases are not registry images.
|
|
176
141
|
if (ref !== "scratch" && !stageAliases.has(ref.toLowerCase())) {
|
|
177
142
|
const hasDigest = /@sha256:[0-9a-f]{64}/i.test(ref);
|
|
178
|
-
//
|
|
179
|
-
// reference is unknown — it may carry a digest supplied via
|
|
180
|
-
// `--build-arg` at build time. Suppress both signals rather than
|
|
181
|
-
// emit a guess; a resolvable ARG default still produces a precise
|
|
182
|
-
// verdict below.
|
|
143
|
+
// An unresolved interpolation may carry a `--build-arg` digest: stay silent.
|
|
183
144
|
if (!hasDigest && !interpolatedUnresolved) {
|
|
184
145
|
hits["dockerfile-no-digest-pin"].push({ file: rel, line: i + 1, snippet: trimmed.slice(0, 120) });
|
|
185
146
|
}
|
|
186
|
-
//
|
|
187
|
-
// (Docker defaults to :latest when omitted).
|
|
147
|
+
// No tag counts as :latest — that is Docker's default.
|
|
188
148
|
const tagMatch = ref.match(/:([^@]+)(?:@|$)/);
|
|
189
149
|
const tag = tagMatch ? tagMatch[1] : null;
|
|
190
150
|
if (!interpolatedUnresolved && (tag === "latest" || tag === null)) {
|
|
191
151
|
hits["dockerfile-from-latest"].push({ file: rel, line: i + 1, snippet: trimmed.slice(0, 120) });
|
|
192
152
|
}
|
|
193
153
|
}
|
|
194
|
-
// USER does
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// FROM (registry image, or `scratch`) begins a fresh stage and resets
|
|
199
|
-
// the per-stage USER tracking, so the runs-as-root predicate reflects
|
|
200
|
-
// only the FINAL stage. Evaluate internal-vs-external against the
|
|
201
|
-
// ARG-resolved `ref` and BEFORE the current alias is recorded.
|
|
154
|
+
// USER does not carry across build stages: a stage runs as its base image's
|
|
155
|
+
// user (root) until its own USER directive, so an external FROM resets the
|
|
156
|
+
// tracking. A `FROM <prior-stage-alias>` inherits and must not reset. Both
|
|
157
|
+
// tests read the ARG-resolved `ref`, before this stage's alias is recorded.
|
|
202
158
|
const isInternalStageRef = stageAliases.has(ref.toLowerCase());
|
|
203
159
|
if (!isInternalStageRef) {
|
|
204
160
|
sawNonRootUser = false;
|
|
205
161
|
sawAnyUser = false;
|
|
206
162
|
}
|
|
207
|
-
// Record this stage's alias so later `FROM <alias>` lines are recognized
|
|
208
|
-
// as internal references.
|
|
209
163
|
if (alias) stageAliases.add(alias.toLowerCase());
|
|
210
164
|
continue;
|
|
211
165
|
}
|
|
212
166
|
|
|
213
|
-
//
|
|
214
|
-
// Recognises USER <uid>, USER <uid>:<gid>, USER <name>, USER <name>:<group>.
|
|
215
|
-
// Root forms (all count as root): "root", "0", "root:<anything>",
|
|
216
|
-
// "0:<anything>". Any other UID/name is non-root.
|
|
167
|
+
// Root is "root" or "0", with any group suffix; every other uid is non-root.
|
|
217
168
|
const userMatch = trimmed.match(/^USER\s+(\S+)/i);
|
|
218
169
|
if (userMatch) {
|
|
219
170
|
sawAnyUser = true;
|
|
@@ -223,7 +174,6 @@ function scanDockerfile(content, rel) {
|
|
|
223
174
|
continue;
|
|
224
175
|
}
|
|
225
176
|
|
|
226
|
-
// curl|wget | sh|bash pattern
|
|
227
177
|
if (
|
|
228
178
|
/\b(?:curl|wget)\b[^|]*\|\s*(?:sh|bash|zsh)\b/.test(trimmed) ||
|
|
229
179
|
/\b(?:curl|wget)\b[^&|;]*\s+&&\s+(?:sh|bash)\b/.test(trimmed)
|
|
@@ -232,10 +182,7 @@ function scanDockerfile(content, rel) {
|
|
|
232
182
|
}
|
|
233
183
|
}
|
|
234
184
|
|
|
235
|
-
//
|
|
236
|
-
// was seen anywhere. (sawAnyUser=false also counts — image defaults
|
|
237
|
-
// to root.) Demoted on metadata-only Dockerfiles (FROM-only, no
|
|
238
|
-
// execution-shape directives) — those aren't runtime images.
|
|
185
|
+
// Fires when the final stage carries no non-root USER; none defaults to root.
|
|
239
186
|
if (!sawNonRootUser && !isMetadataOnly) {
|
|
240
187
|
hits["dockerfile-runs-as-root"].push({ file: rel, line: 0, snippet: sawAnyUser ? "USER directive sets root/0" : "no USER directive (defaults to root)" });
|
|
241
188
|
}
|
|
@@ -243,11 +190,6 @@ function scanDockerfile(content, rel) {
|
|
|
243
190
|
return hits;
|
|
244
191
|
}
|
|
245
192
|
|
|
246
|
-
/**
|
|
247
|
-
* docker-compose pattern matchers. Heuristic: match `key: true` /
|
|
248
|
-
* `key: <value>` lines, capturing the service block path where
|
|
249
|
-
* possible.
|
|
250
|
-
*/
|
|
251
193
|
function scanCompose(content, rel) {
|
|
252
194
|
const lines = content.split(/\r?\n/);
|
|
253
195
|
const hits = {
|
|
@@ -259,22 +201,17 @@ function scanCompose(content, rel) {
|
|
|
259
201
|
|
|
260
202
|
for (let i = 0; i < lines.length; i++) {
|
|
261
203
|
const line = lines[i];
|
|
262
|
-
//
|
|
263
|
-
// crafted whitespace run can't drive regex backtracking.
|
|
204
|
+
// Skipping overlong lines keeps a crafted whitespace run from backtracking.
|
|
264
205
|
if (line.length > 4096) continue;
|
|
265
206
|
if (/^\s*#/.test(line)) continue;
|
|
266
207
|
if (/^\s*privileged:\s*true\b/i.test(line)) hits["compose-privileged"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
267
|
-
//
|
|
268
|
-
// network_mode: host, pid: host, ipc: host.
|
|
208
|
+
// Per the playbook, any of network_mode / pid / ipc set to host.
|
|
269
209
|
if (/^\s*network_mode:\s*['"]?host\b/i.test(line) ||
|
|
270
210
|
/^\s*pid:\s*['"]?host['"]?\s*$/i.test(line) ||
|
|
271
211
|
/^\s*ipc:\s*['"]?host['"]?\s*$/i.test(line)) {
|
|
272
212
|
hits["compose-host-network"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
273
213
|
}
|
|
274
|
-
//
|
|
275
|
-
// SYS_ADMIN, SYS_PTRACE, or SYS_MODULE under cap_add. Either
|
|
276
|
-
// inline (cap_add: [SYS_ADMIN, SYS_PTRACE]) or multi-line
|
|
277
|
-
// (cap_add:\n - SYS_PTRACE).
|
|
214
|
+
// Per the playbook: risky caps under cap_add, inline list or `- ` items.
|
|
278
215
|
const RISKY_CAPS_RE = /\b(?:CAP_)?(?:SYS_ADMIN|SYS_PTRACE|SYS_MODULE)\b/i;
|
|
279
216
|
if (/cap_add:.*\[/i.test(line) && RISKY_CAPS_RE.test(line)) {
|
|
280
217
|
hits["compose-cap-add-sys-admin"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
@@ -282,7 +219,6 @@ function scanCompose(content, rel) {
|
|
|
282
219
|
lines.slice(Math.max(0, i - 5), i).some(l => /cap_add:/i.test(l))) {
|
|
283
220
|
hits["compose-cap-add-sys-admin"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
284
221
|
}
|
|
285
|
-
// docker-sock mount: /var/run/docker.sock anywhere in the value
|
|
286
222
|
if (/\/var\/run\/docker\.sock/.test(line)) {
|
|
287
223
|
hits["compose-docker-sock-mount"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
288
224
|
}
|
|
@@ -290,10 +226,6 @@ function scanCompose(content, rel) {
|
|
|
290
226
|
return hits;
|
|
291
227
|
}
|
|
292
228
|
|
|
293
|
-
/**
|
|
294
|
-
* k8s manifest pattern matchers. Heuristic line-scanning + kind-
|
|
295
|
-
* level guards (e.g. ClusterRoleBinding for cluster-admin check).
|
|
296
|
-
*/
|
|
297
229
|
function scanK8s(content, rel) {
|
|
298
230
|
const lines = content.split(/\r?\n/);
|
|
299
231
|
const kind = extractKind(content);
|
|
@@ -308,36 +240,28 @@ function scanK8s(content, rel) {
|
|
|
308
240
|
|
|
309
241
|
for (let i = 0; i < lines.length; i++) {
|
|
310
242
|
const line = lines[i];
|
|
311
|
-
//
|
|
312
|
-
// crafted whitespace run can't drive regex backtracking.
|
|
243
|
+
// Skipping overlong lines keeps a crafted whitespace run from backtracking.
|
|
313
244
|
if (line.length > 4096) continue;
|
|
314
245
|
if (/^\s*#/.test(line)) continue;
|
|
315
246
|
if (/^\s*privileged:\s*true\b/i.test(line)) hits["k8s-privileged"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
316
247
|
if (/^\s*(hostNetwork|hostPID|hostIPC):\s*true\b/.test(line)) hits["k8s-host-namespaces"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// root" clause requires image inspection the collector can't do
|
|
320
|
-
// without a container runtime; leave that to the runner when
|
|
321
|
-
// operator-supplied image-inspection evidence is available.)
|
|
248
|
+
// Only the two clauses visible in the manifest; the playbook's third needs a
|
|
249
|
+
// container runtime, so it stays with the runner.
|
|
322
250
|
if (/^\s*runAsUser:\s*0\b/.test(line) ||
|
|
323
251
|
/^\s*runAsNonRoot:\s*false\b/.test(line)) {
|
|
324
252
|
hits["k8s-run-as-root"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
325
253
|
}
|
|
326
|
-
// hostPath sensitive: /, /etc, /var/lib/docker, /proc, /sys
|
|
327
254
|
const hpMatch = line.match(/^\s*path:\s*['"]?(\/(?:etc|proc|sys|var\/lib\/docker|var\/run|root|home)?\/?)['"]?\s*$/);
|
|
328
255
|
if (hpMatch && lines.slice(Math.max(0, i - 3), i).some(l => /hostPath:/i.test(l))) {
|
|
329
256
|
hits["k8s-hostpath-sensitive"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
330
257
|
}
|
|
331
|
-
//
|
|
332
|
-
// Allow optional leading `-` from a YAML list item: `- image: ...`.
|
|
333
|
-
// `^[ \t]*(?:-[ \t]*)?` anchors the indentation once, then an optional
|
|
334
|
-
// `- ` list marker — no overlapping `\s*` runs that backtrack.
|
|
258
|
+
// Indentation and the `- ` marker are anchored once each: no overlapping runs.
|
|
335
259
|
const imageMatch = line.match(/^[ \t]*(?:-[ \t]*)?image:\s*['"]?([^'"@\s]+)(?:@[^'"]+)?['"]?\s*$/);
|
|
336
260
|
if (imageMatch) {
|
|
337
261
|
const ref = imageMatch[1];
|
|
338
262
|
const tagMatch = ref.match(/:([^/]+)$/);
|
|
339
263
|
const tag = tagMatch ? tagMatch[1] : null;
|
|
340
|
-
//
|
|
264
|
+
// Tested against the whole line: the capture above drops the @-suffix.
|
|
341
265
|
const hasDigest = /@sha256:[0-9a-f]{64}/.test(line);
|
|
342
266
|
if (!hasDigest && (tag === "latest" || tag === null)) {
|
|
343
267
|
hits["k8s-image-latest"].push({ file: rel, line: i + 1, snippet: line.trim() });
|
|
@@ -345,7 +269,6 @@ function scanK8s(content, rel) {
|
|
|
345
269
|
}
|
|
346
270
|
}
|
|
347
271
|
|
|
348
|
-
// ClusterRoleBinding referencing cluster-admin
|
|
349
272
|
if (kind === "ClusterRoleBinding" || kind === "RoleBinding") {
|
|
350
273
|
if (/name:\s*['"]?cluster-admin['"]?/m.test(content)) {
|
|
351
274
|
hits["k8s-cluster-admin-binding"].push({ file: rel, line: 0, snippet: `${kind} binds cluster-admin` });
|
|
@@ -377,7 +300,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
377
300
|
}
|
|
378
301
|
}
|
|
379
302
|
|
|
380
|
-
// Aggregate hits per indicator.
|
|
381
303
|
const allHits = {};
|
|
382
304
|
for (const id of [
|
|
383
305
|
"dockerfile-from-latest", "dockerfile-no-digest-pin", "dockerfile-runs-as-root", "dockerfile-curl-pipe-bash",
|
|
@@ -409,21 +331,14 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
409
331
|
for (const [id, list] of Object.entries(fileHits)) allHits[id].push(...list);
|
|
410
332
|
}
|
|
411
333
|
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
// network-policies-absent require cluster-API access the collector
|
|
415
|
-
// doesn't have; leave them unflipped so the runner returns
|
|
416
|
-
// inconclusive rather than a forced miss.)
|
|
334
|
+
// Only the indicators decidable from files on disk; the ones needing
|
|
335
|
+
// cluster-API access stay unflipped, so the runner returns inconclusive.
|
|
417
336
|
const signal_overrides = {};
|
|
418
337
|
for (const [id, list] of Object.entries(allHits)) {
|
|
419
338
|
signal_overrides[id] = list.length > 0 ? "hit" : "miss";
|
|
420
339
|
}
|
|
421
340
|
|
|
422
|
-
//
|
|
423
|
-
// SARIF result points at the Dockerfile / compose / k8s manifest line that
|
|
424
|
-
// triggered it. Line-scanned hits carry a real line number (emitted as
|
|
425
|
-
// startLine); whole-file hits (e.g. runs-as-root, cluster-admin-binding)
|
|
426
|
-
// record line 0 and surface as file-level locations.
|
|
341
|
+
// Line 0 surfaces as a file-level SARIF location rather than a startLine.
|
|
427
342
|
const evidence_locations = {};
|
|
428
343
|
for (const [id, list] of Object.entries(allHits)) {
|
|
429
344
|
if (signal_overrides[id] === "hit") {
|
|
@@ -490,13 +405,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
490
405
|
};
|
|
491
406
|
}
|
|
492
407
|
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// tree this playbook walks — so `discover` recommends `containers` for exactly
|
|
497
|
-
// the surface the collector scans, not just a root-level exact-name Dockerfile
|
|
498
|
-
// or docker-compose.yml. Filename-only (no content reads), capped, and
|
|
499
|
-
// fault-tolerant so a walk error can't break discovery.
|
|
408
|
+
// `discover` calls this to decide whether to recommend `containers`, so it walks
|
|
409
|
+
// the same bounded tree and classifier the collector does. Filenames only,
|
|
410
|
+
// capped, and swallows a walk error so discovery cannot break on one.
|
|
500
411
|
function hasContainerArtifacts(cwd, { cap = 8 } = {}) {
|
|
501
412
|
let files;
|
|
502
413
|
try { files = walkTree(path.resolve(cwd || "."), { maxDepth: DEFAULT_MAX_DEPTH, excludes: DEFAULT_EXCLUDES }); }
|