@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
package/lib/collectors/crypto.js
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Include expansion) to flip post-quantum-readiness indicators.
|
|
10
|
-
*
|
|
11
|
-
* Skipped indicators (require operator judgement or live behavioural
|
|
12
|
-
* data, left unflipped so the runner returns inconclusive rather
|
|
13
|
-
* than a forced miss):
|
|
14
|
-
*
|
|
15
|
-
* tls-no-hybrid-group needs a real TLS handshake against
|
|
16
|
-
* a target server
|
|
17
|
-
* rsa-2048-cert-long-life cert content + chain walk; sensitivity-
|
|
18
|
-
* horizon comparison is operator review
|
|
19
|
-
* no-crypto-inventory governance / process indicator
|
|
4
|
+
* Companion collector for the `crypto` playbook, Linux only. Reads the host's
|
|
5
|
+
* TLS library state (openssl version, KEM and signature catalogues) and the
|
|
6
|
+
* effective sshd_config after Include expansion, to flip post-quantum readiness
|
|
7
|
+
* indicators. Indicators needing a live handshake, a certificate-chain review
|
|
8
|
+
* or governance evidence are left unflipped.
|
|
20
9
|
*
|
|
21
10
|
* Interface: see lib/collectors/README.md
|
|
22
11
|
*/
|
|
@@ -33,18 +22,16 @@ function readFileSafe(p, max = 256 * 1024) {
|
|
|
33
22
|
fd = fs.openSync(p, "r");
|
|
34
23
|
const st = fs.fstatSync(fd);
|
|
35
24
|
if (st.size > max) return null;
|
|
36
|
-
// readFileSync(fd)
|
|
37
|
-
//
|
|
38
|
-
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
39
|
-
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
25
|
+
// readFileSync(fd), never a single readSync: on network/FUSE fds a short read
|
|
26
|
+
// leaves the tail NUL-filled. The open fd also keeps fstat-then-read TOCTOU-free.
|
|
40
27
|
return fs.readFileSync(fd, "utf8");
|
|
41
28
|
} catch { return null; }
|
|
42
29
|
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
32
|
+
// Drop-in files are inlined in lexical order at the textual position of the
|
|
33
|
+
// `Include` directive, so a later first-match-wins parse sees what sshd sees.
|
|
34
|
+
// The hardening collector expands sshd_config the same way.
|
|
48
35
|
function expandSshdConfig(baseContent, configDPath) {
|
|
49
36
|
if (!baseContent) return "";
|
|
50
37
|
const out = [];
|
|
@@ -74,8 +61,7 @@ function expandSshdConfig(baseContent, configDPath) {
|
|
|
74
61
|
return out.join("\n");
|
|
75
62
|
}
|
|
76
63
|
|
|
77
|
-
// First
|
|
78
|
-
// PermitRootLogin from the effective sshd_config content.
|
|
64
|
+
// First match wins, as in sshd itself; an unset directive stays null.
|
|
79
65
|
function parseSshdEffective(content) {
|
|
80
66
|
if (content == null) return { kex: null, macs: null, ciphers: null };
|
|
81
67
|
const out = { kex: null, macs: null, ciphers: null };
|
|
@@ -92,9 +78,8 @@ function parseSshdEffective(content) {
|
|
|
92
78
|
return out;
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// could not be parsed — collector returns inconclusive).
|
|
81
|
+
// 3.5.0 is the native-ML-KEM cutoff: "hit" below it, "miss" at or above, and
|
|
82
|
+
// undefined when the banner does not parse, which leaves it inconclusive.
|
|
98
83
|
function compareOpensslVersion(verStr) {
|
|
99
84
|
if (!verStr) return undefined;
|
|
100
85
|
const m = verStr.match(/OpenSSL\s+(\d+)\.(\d+)\.(\d+)/);
|
|
@@ -106,21 +91,16 @@ function compareOpensslVersion(verStr) {
|
|
|
106
91
|
return "miss";
|
|
107
92
|
}
|
|
108
93
|
|
|
109
|
-
//
|
|
110
|
-
// present-without sntrup761x25519 / mlkem768x25519 / mlkem1024.
|
|
94
|
+
// Hits when KexAlgorithms is absent (no PQC kex by default) or lacks a hybrid.
|
|
111
95
|
function parsePqcKex(content, hasSshdContent) {
|
|
112
96
|
if (!hasSshdContent) return undefined;
|
|
113
97
|
if (content == null) return "hit";
|
|
114
98
|
return /sntrup761x25519|mlkem768x25519|mlkem1024/.test(content) ? "miss" : "hit";
|
|
115
99
|
}
|
|
116
100
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// mode as weak under modern SSH cipher policy (BEAST / padding
|
|
121
|
-
// oracle / chosen-plaintext considerations) — aes128-cbc /
|
|
122
|
-
// aes192-cbc / aes256-cbc still appear in legacy sshd configs and
|
|
123
|
-
// must be flagged. Both fields absent → undefined (inconclusive).
|
|
101
|
+
// The playbook treats EVERY CBC mode as weak, aes256-cbc included. hmac-md5 and
|
|
102
|
+
// hmac-sha1 are weak only without the -etm suffix. Both fields absent leaves the
|
|
103
|
+
// indicator undefined, hence inconclusive.
|
|
124
104
|
function parseWeakMacOrCipher(macs, ciphers) {
|
|
125
105
|
if (macs == null && ciphers == null) return undefined;
|
|
126
106
|
const macsWeak = macs && /(?:^|,)(?:hmac-md5(?!-etm)|hmac-sha1(?!-etm))(?:,|$)/.test(macs);
|
|
@@ -128,13 +108,10 @@ function parseWeakMacOrCipher(macs, ciphers) {
|
|
|
128
108
|
return (macsWeak || cipherWeak) ? "hit" : "miss";
|
|
129
109
|
}
|
|
130
110
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// their banner to stderr (e.g. `ssh -V`); others to stdout
|
|
136
|
-
// (`openssl version`). Prefer stdout, fall back to stderr when
|
|
137
|
-
// stdout is empty so the banner is not lost.
|
|
111
|
+
// A path override reads a fixture; otherwise the binary is spawned argv-style,
|
|
112
|
+
// never through a shell. A missing binary returns null, which the caller
|
|
113
|
+
// surfaces as an unflipped indicator. `ssh -V` writes its banner to stderr and
|
|
114
|
+
// `openssl version` to stdout, so stdout wins and stderr is the fallback.
|
|
138
115
|
function readOrSpawn(pathOverride, cmd, args, errors) {
|
|
139
116
|
if (pathOverride != null) return readFileSafe(pathOverride);
|
|
140
117
|
const r = spawnSync(cmd, args, {
|
|
@@ -179,25 +156,20 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
179
156
|
};
|
|
180
157
|
}
|
|
181
158
|
|
|
182
|
-
// TLS library version + algorithm catalogues.
|
|
183
159
|
const opensslVer = readOrSpawn(paths.opensslVersionOutput, "openssl", ["version", "-a"], errors);
|
|
184
160
|
const opensslKem = readOrSpawn(paths.opensslKemOutput, "openssl", ["list", "-kem-algorithms"], errors);
|
|
185
161
|
const opensslSig = readOrSpawn(paths.opensslSignatureOutput, "openssl", ["list", "-signature-algorithms"], errors);
|
|
186
162
|
const sshVer = readOrSpawn(paths.sshVersionOutput, "ssh", ["-V"], errors);
|
|
187
163
|
|
|
188
|
-
//
|
|
189
|
-
// can come from a path override (tests stage a synthetic file
|
|
190
|
-
// tree); otherwise read /etc/ssh/sshd_config directly.
|
|
164
|
+
// A path override lets a test stage a synthetic tree in place of /etc/ssh.
|
|
191
165
|
const sshdConfigPath = paths.sshdConfig || "/etc/ssh/sshd_config";
|
|
192
166
|
const sshdConfigDPath = paths.sshdConfigD || "/etc/ssh/sshd_config.d";
|
|
193
167
|
const sshdBase = readFileSafe(sshdConfigPath);
|
|
194
168
|
const sshdEffective = sshdBase ? expandSshdConfig(sshdBase, sshdConfigDPath) : null;
|
|
195
169
|
const sshdParsed = parseSshdEffective(sshdEffective);
|
|
196
170
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// signal_overrides so the runner returns inconclusive rather than
|
|
200
|
-
// asserting a clean posture without evidence.
|
|
171
|
+
// An indicator is flipped only when its source was readable; otherwise it stays
|
|
172
|
+
// out of this object and the runner returns inconclusive.
|
|
201
173
|
const signal_overrides = {};
|
|
202
174
|
|
|
203
175
|
const verSig = compareOpensslVersion(opensslVer);
|
|
@@ -220,8 +192,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
220
192
|
: undefined;
|
|
221
193
|
if (weakSig !== undefined) signal_overrides["weak-mac-or-cipher"] = weakSig;
|
|
222
194
|
|
|
223
|
-
// certificate-store: list count of *.pem / *.crt under the
|
|
224
|
-
// standard trust roots. Path overridable for tests.
|
|
225
195
|
let certStoreSummary;
|
|
226
196
|
const certStoreRoot = paths.certStore || "/etc/ssl/certs";
|
|
227
197
|
try {
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Companion collector for the `hardening` playbook. Linux-only:
|
|
7
|
-
* reads `/proc/sys/kernel/*`, `/proc/cmdline`,
|
|
8
|
-
* `/sys/kernel/security/lockdown`, and `/etc/ssh/sshd_config` to
|
|
9
|
-
* flip deterministic indicators. On non-Linux platforms the
|
|
10
|
-
* precondition fails and the collector emits an empty submission.
|
|
4
|
+
* Companion collector for the `hardening` playbook. Linux-only: off Linux the
|
|
5
|
+
* precondition fails and the submission comes back empty.
|
|
11
6
|
*
|
|
12
7
|
* Interface: see lib/collectors/README.md
|
|
13
8
|
*/
|
|
@@ -30,21 +25,15 @@ function readFileSafe(p, max = 256 * 1024) {
|
|
|
30
25
|
fd = fs.openSync(p, "r");
|
|
31
26
|
const st = fs.fstatSync(fd);
|
|
32
27
|
if (st.size > max) return null;
|
|
33
|
-
// readFileSync(fd) loops
|
|
34
|
-
//
|
|
35
|
-
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
36
|
-
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
28
|
+
// readFileSync(fd) loops to EOF, where one readSync can return short on a
|
|
29
|
+
// network/FUSE fd and NUL-fill the tail; via the fd it is also TOCTOU-free.
|
|
37
30
|
return fs.readFileSync(fd, "utf8");
|
|
38
31
|
} catch { return null; }
|
|
39
32
|
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// Mirrors OpenSSH's parse order: first-match-wins, and the first match
|
|
45
|
-
// can come from a drop-in file when `Include` appears earlier in the
|
|
46
|
-
// base config than the directive it sets. Drop-in files within an
|
|
47
|
-
// Include are processed in lexical order (matching OpenSSH glob).
|
|
35
|
+
// Inlines `Include <glob>` at its textual position: OpenSSH is first-match-wins,
|
|
36
|
+
// so a drop-in included early beats a later line in the base config.
|
|
48
37
|
function expandSshdConfig(baseContent, configDPath) {
|
|
49
38
|
if (!baseContent) return "";
|
|
50
39
|
const out = [];
|
|
@@ -53,13 +42,9 @@ function expandSshdConfig(baseContent, configDPath) {
|
|
|
53
42
|
const m = stripped.match(/^Include\s+(\S+)/i);
|
|
54
43
|
if (!m) { out.push(raw); continue; }
|
|
55
44
|
const glob = m[1];
|
|
56
|
-
//
|
|
57
|
-
// `<dir>/*.conf` form; other shapes fall back to no-op.
|
|
45
|
+
// Only the common `<dir>/*.conf` form resolves; other shapes are no-ops.
|
|
58
46
|
let dir = null;
|
|
59
47
|
if (glob.endsWith("/sshd_config.d/*.conf")) {
|
|
60
|
-
// The canonical sshd-config drop-in directory. Honour the
|
|
61
|
-
// path override (tests / chroot / sshd_config.d outside the
|
|
62
|
-
// default location).
|
|
63
48
|
dir = configDPath;
|
|
64
49
|
} else {
|
|
65
50
|
const dirMatch = glob.match(/^(.*)\/\*\.conf$/);
|
|
@@ -80,9 +65,7 @@ function expandSshdConfig(baseContent, configDPath) {
|
|
|
80
65
|
}
|
|
81
66
|
|
|
82
67
|
function parseSshdEffective(content) {
|
|
83
|
-
//
|
|
84
|
-
// `PasswordAuthentication <value>` lines. sshd_config is parsed
|
|
85
|
-
// first-match-wins for most directives.
|
|
68
|
+
// First occurrence wins, as sshd_config parses these directives.
|
|
86
69
|
if (!content) return { permitRootLogin: null, passwordAuth: null };
|
|
87
70
|
const out = { permitRootLogin: null, passwordAuth: null };
|
|
88
71
|
for (const raw of content.split(/\r?\n/)) {
|
|
@@ -100,10 +83,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
100
83
|
const errors = [];
|
|
101
84
|
const startTime = Date.now();
|
|
102
85
|
const root = path.resolve(cwd);
|
|
103
|
-
//
|
|
104
|
-
// redirect /proc / /sys / /etc reads to a synthetic tempdir
|
|
105
|
-
// mirroring the real layout. Without overrides the collector
|
|
106
|
-
// reads the live host paths.
|
|
86
|
+
// args.paths redirects the /proc, /sys and /etc reads at a synthetic tree.
|
|
107
87
|
const paths = args.paths || {};
|
|
108
88
|
const P = {
|
|
109
89
|
kptrRestrict: paths.kptrRestrict || "/proc/sys/kernel/kptr_restrict",
|
|
@@ -117,9 +97,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
117
97
|
sshdConfigD: paths.sshdConfigD || "/etc/ssh/sshd_config.d",
|
|
118
98
|
kallsyms: paths.kallsyms || "/proc/kallsyms",
|
|
119
99
|
};
|
|
120
|
-
//
|
|
121
|
-
// when running on win32 / darwin. Without it, the platform gate
|
|
122
|
-
// remains the source of truth for whether the collector runs.
|
|
100
|
+
// args.forceLinux exercises the Linux path from win32 / darwin.
|
|
123
101
|
const isLinux = args.forceLinux === true || process.platform === "linux";
|
|
124
102
|
|
|
125
103
|
if (!isLinux) {
|
|
@@ -160,8 +138,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
160
138
|
};
|
|
161
139
|
}
|
|
162
140
|
|
|
163
|
-
//
|
|
164
|
-
// kernel / non-standard build).
|
|
141
|
+
// `null` means the sysctl path is absent (older kernel, non-standard build).
|
|
165
142
|
const kptrRestrict = readSysctl(P.kptrRestrict);
|
|
166
143
|
const unprivUserns = readSysctl(P.unprivUserns);
|
|
167
144
|
const unprivBpf = readSysctl(P.unprivBpf);
|
|
@@ -170,21 +147,12 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
170
147
|
const cmdline = readFileSafe(P.cmdline) || "";
|
|
171
148
|
const lockdown = readSysctl(P.lockdown) || "";
|
|
172
149
|
|
|
173
|
-
// sshd_config: expand Include directives in-place so the
|
|
174
|
-
// first-match-wins parse honours OpenSSH's effective directive
|
|
175
|
-
// order. On Debian/Ubuntu the default sshd_config begins with
|
|
176
|
-
// `Include /etc/ssh/sshd_config.d/*.conf` — drop-in values take
|
|
177
|
-
// precedence over the base file's later lines.
|
|
178
150
|
const sshdBase = readFileSafe(P.sshdConfig);
|
|
179
151
|
const sshdContent = sshdBase ? expandSshdConfig(sshdBase, P.sshdConfigD) : null;
|
|
180
152
|
const sshdParsed = parseSshdEffective(sshdContent);
|
|
181
153
|
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
// sysctls (e.g. masked /proc in a constrained container, kernel
|
|
185
|
-
// built without that knob) leave the indicator unflipped so the
|
|
186
|
-
// runner returns inconclusive rather than forging a "hardened"
|
|
187
|
-
// miss without evidence.
|
|
154
|
+
// A sysctl-derived indicator flips only when the sysctl was readable, so an
|
|
155
|
+
// unreadable knob reports inconclusive rather than a "hardened" miss.
|
|
188
156
|
function fromSysctl(value, hitWhen) {
|
|
189
157
|
if (value == null) return undefined; // unreadable → inconclusive
|
|
190
158
|
return value === hitWhen ? "hit" : "miss";
|
|
@@ -200,9 +168,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
200
168
|
const yamaSig = fromSysctl(yamaPtrace, "0");
|
|
201
169
|
if (yamaSig !== undefined) signal_overrides["yama-ptrace-permissive"] = yamaSig;
|
|
202
170
|
|
|
203
|
-
//
|
|
204
|
-
// couldn't read it at all, those three indicators stay unflipped
|
|
205
|
-
// (inconclusive) rather than asserting an absent string.
|
|
171
|
+
// An unreadable cmdline leaves both unflipped, not asserted against nothing.
|
|
206
172
|
if (cmdline) {
|
|
207
173
|
const kaslrDisabled = /\bnokaslr\b/.test(cmdline) || /\bkaslr=off\b/.test(cmdline);
|
|
208
174
|
const mitigationsOff = /\bmitigations=off\b/.test(cmdline);
|
|
@@ -210,10 +176,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
210
176
|
signal_overrides["mitigations-off"] = mitigationsOff ? "hit" : "miss";
|
|
211
177
|
}
|
|
212
178
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// lockdown file AND /proc/cmdline are unreadable, leave the
|
|
216
|
-
// indicator unflipped.
|
|
179
|
+
// Lockdown is none when the file shows `[none]`, or it is absent and no
|
|
180
|
+
// lockdown= rides the cmdline. Both unreadable leaves the indicator unflipped.
|
|
217
181
|
if (lockdown || cmdline) {
|
|
218
182
|
const lockdownShowsNone = /\[none\]/.test(lockdown);
|
|
219
183
|
const lockdownCmdline = /\blockdown=(?:integrity|confidentiality)\b/.test(cmdline);
|
|
@@ -223,8 +187,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
223
187
|
signal_overrides["kernel-lockdown-none"] = lockdownNoneHit ? "hit" : "miss";
|
|
224
188
|
}
|
|
225
189
|
|
|
226
|
-
//
|
|
227
|
-
// was readable. Missing config (no SSH server) → unflipped.
|
|
190
|
+
// A missing sshd_config (no SSH server) leaves this unflipped.
|
|
228
191
|
let sshdRootHit = false;
|
|
229
192
|
if (sshdContent !== null) {
|
|
230
193
|
sshdRootHit =
|
|
@@ -235,30 +198,9 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
235
198
|
|
|
236
199
|
const kptrHit = kptrSig === "hit";
|
|
237
200
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// honestly downgrades to inconclusive.
|
|
242
|
-
//
|
|
243
|
-
// kptr-restrict-disabled:
|
|
244
|
-
// [0] kdump / perf debug-session runbook — operator judgement
|
|
245
|
-
// [1] /proc/kallsyms zero-leakage cross-check — collector CAN
|
|
246
|
-
// attest (read first line as unprivileged user).
|
|
247
|
-
// yama-ptrace-permissive:
|
|
248
|
-
// [0] MAC enforcement (AppArmor / SELinux) — operator
|
|
249
|
-
// [1] container observability — operator
|
|
250
|
-
// [2] single-tenant dev VM — operator
|
|
251
|
-
// kaslr-disabled-at-boot:
|
|
252
|
-
// [0] kdump runbook — operator
|
|
253
|
-
// [1] dmesg KASLR offsets — collector cannot read dmesg
|
|
254
|
-
// without root on most distros (dmesg-restrict=1).
|
|
255
|
-
// mitigations-off:
|
|
256
|
-
// [0] HPC/benchmark exemption — operator
|
|
257
|
-
// [1] single-tenant — operator
|
|
258
|
-
//
|
|
259
|
-
// For kptr-restrict-disabled the kallsyms cross-check is the only
|
|
260
|
-
// FP-check the collector can attest; we attest index 1 when we
|
|
261
|
-
// observed the kallsyms first line carries non-zero hex.
|
|
201
|
+
// __fp_checks attests only the checks actually performed; the rest stay
|
|
202
|
+
// unsatisfied so the runner downgrades to inconclusive. Index 1 of
|
|
203
|
+
// kptr-restrict-disabled is the /proc/kallsyms leakage cross-check.
|
|
262
204
|
if (kptrHit) {
|
|
263
205
|
let kallsymsLeaks = false;
|
|
264
206
|
try {
|
package/lib/collectors/kernel.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* kernel release string for the kver-in-affected-range indicator.
|
|
9
|
-
*
|
|
10
|
-
* Scope: Linux only. On macOS / Windows the playbook's linux-platform
|
|
11
|
-
* precondition halts at preflight; the collector reports that
|
|
12
|
-
* truthfully so the operator sees the visibility gap without the
|
|
13
|
-
* runner having to re-derive it.
|
|
14
|
-
*
|
|
15
|
-
* Interface: see lib/collectors/README.md
|
|
4
|
+
* Companion collector for the `kernel` playbook: the linux-platform and
|
|
5
|
+
* uname-available preconditions, plus the kernel release, cmdline and sysctl
|
|
6
|
+
* snapshot its indicators read. Off Linux it reports the visibility gap rather
|
|
7
|
+
* than staying silent. Interface: lib/collectors/README.md
|
|
16
8
|
*/
|
|
17
9
|
|
|
18
10
|
const { execFileSync } = require("node:child_process");
|
|
@@ -32,12 +24,8 @@ function runUname(arg) {
|
|
|
32
24
|
function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
33
25
|
const errors = [];
|
|
34
26
|
|
|
35
|
-
// Precondition 1: linux-platform. Use process.platform first
|
|
36
|
-
// (Node-derived, always available); cross-check against uname -s
|
|
37
|
-
// when available.
|
|
38
27
|
const linuxPlatform = process.platform === "linux";
|
|
39
28
|
|
|
40
|
-
// Precondition 2: uname-available. Pure capability check.
|
|
41
29
|
const unameR = runUname("-r");
|
|
42
30
|
const unameAvailable = unameR.ok;
|
|
43
31
|
if (!unameAvailable && linuxPlatform) {
|
|
@@ -47,10 +35,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
47
35
|
});
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// is captured=false with the reason; the runner treats the
|
|
53
|
-
// dependent indicators as inconclusive.
|
|
38
|
+
// captured=false carries the reason, so the runner reads dependent indicators
|
|
39
|
+
// as inconclusive rather than as a miss.
|
|
54
40
|
const artifacts = {};
|
|
55
41
|
if (unameR.ok) {
|
|
56
42
|
artifacts["kernel-release"] = { value: unameR.value, captured: true };
|
|
@@ -64,9 +50,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
64
50
|
};
|
|
65
51
|
}
|
|
66
52
|
|
|
67
|
-
//
|
|
68
|
-
// KASLR / unpriv-userns / unpriv-bpf indicator evaluation. Read
|
|
69
|
-
// from /proc directly so we don't fork another process.
|
|
53
|
+
// kernel-cmdline feeds the KASLR / unpriv-userns / unpriv-bpf indicators.
|
|
70
54
|
if (linuxPlatform) {
|
|
71
55
|
try {
|
|
72
56
|
const fs = require("node:fs");
|
|
@@ -79,8 +63,6 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
79
63
|
reason: `/proc/cmdline read failed: ${e.message}`,
|
|
80
64
|
});
|
|
81
65
|
}
|
|
82
|
-
// sysctl snapshot for kernel.unprivileged_userns_clone +
|
|
83
|
-
// kernel.unprivileged_bpf_disabled when readable.
|
|
84
66
|
try {
|
|
85
67
|
const fs = require("node:fs");
|
|
86
68
|
const sysctls = {};
|
|
@@ -93,8 +75,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
93
75
|
try {
|
|
94
76
|
sysctls[path.basename(p)] = fs.readFileSync(p, "utf8").trim();
|
|
95
77
|
} catch {
|
|
96
|
-
//
|
|
97
|
-
// doesn't exist on this kernel.
|
|
78
|
+
// A missing file means the sysctl does not exist on this kernel.
|
|
98
79
|
}
|
|
99
80
|
}
|
|
100
81
|
if (Object.keys(sysctls).length) {
|
|
@@ -109,10 +90,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
109
90
|
}
|
|
110
91
|
}
|
|
111
92
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// feature is compiled out of the kernel. Best-effort — unreadable on many
|
|
115
|
-
// hardened hosts.
|
|
93
|
+
// The running kernel's build config backs the CONFIG_* false-positive checks:
|
|
94
|
+
// a sysctl is moot when the feature is compiled out. Often unreadable.
|
|
116
95
|
let kernelConfig = null;
|
|
117
96
|
if (linuxPlatform && unameR.ok) {
|
|
118
97
|
try {
|
|
@@ -121,10 +100,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
121
100
|
} catch { /* config not readable — CONFIG_* checks stay unattested */ }
|
|
122
101
|
}
|
|
123
102
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// correlation). But we CAN flip the deterministic indicators that
|
|
127
|
-
// read directly off the sysctl snapshot.
|
|
103
|
+
// kver-in-affected-range needs the CVE affected-version catalog, so the runner
|
|
104
|
+
// correlates that; only straight sysctl reads are decided here.
|
|
128
105
|
const signal_overrides = {};
|
|
129
106
|
const sysctl = artifacts["sysctl-snapshot"];
|
|
130
107
|
if (sysctl && sysctl.captured) {
|
|
@@ -136,29 +113,22 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
136
113
|
const v = parseInt(parsed.randomize_va_space, 10);
|
|
137
114
|
signal_overrides["kaslr-disabled"] = (v < 2) ? "hit" : "miss";
|
|
138
115
|
}
|
|
139
|
-
//
|
|
116
|
+
// unprivileged_userns_clone == 1 means unprivileged userns is enabled.
|
|
140
117
|
if (parsed.unprivileged_userns_clone != null) {
|
|
141
118
|
const v = parseInt(parsed.unprivileged_userns_clone, 10);
|
|
142
119
|
const hit = v === 1;
|
|
143
120
|
signal_overrides["unpriv-userns-enabled"] = hit ? "hit" : "miss";
|
|
144
|
-
// FP[1]:
|
|
145
|
-
// compiled in. Attested when /boot/config confirms it. FP[0]
|
|
146
|
-
// (rootless-runtime exception + LSM enforcement) is operator
|
|
147
|
-
// judgement and stays unattested.
|
|
121
|
+
// Attests FP[1] only: the sysctl is live only when userns is compiled in.
|
|
148
122
|
if (hit && kernelConfig && /^CONFIG_USER_NS=y$/m.test(kernelConfig)) {
|
|
149
123
|
signal_overrides["unpriv-userns-enabled__fp_checks"] = { "1": true };
|
|
150
124
|
}
|
|
151
125
|
}
|
|
152
|
-
//
|
|
153
|
-
// is allowed (risky).
|
|
126
|
+
// unprivileged_bpf_disabled == 0 means unprivileged BPF is allowed.
|
|
154
127
|
if (parsed.unprivileged_bpf_disabled != null) {
|
|
155
128
|
const v = parseInt(parsed.unprivileged_bpf_disabled, 10);
|
|
156
129
|
const hit = v === 0;
|
|
157
130
|
signal_overrides["unpriv-bpf-allowed"] = hit ? "hit" : "miss";
|
|
158
|
-
// FP[0]:
|
|
159
|
-
// moot if BPF is compiled out. Attested when /boot/config confirms
|
|
160
|
-
// both. FP[1] (enforcing LSM bpf() restriction) is operator
|
|
161
|
-
// judgement and stays unattested.
|
|
131
|
+
// Attests FP[0] only: the sysctl is moot if BPF is compiled out.
|
|
162
132
|
if (hit && kernelConfig &&
|
|
163
133
|
/^CONFIG_BPF_SYSCALL=y$/m.test(kernelConfig) &&
|
|
164
134
|
/^CONFIG_BPF_JIT=y$/m.test(kernelConfig)) {
|