dsh-standards 1.2.0 → 1.2.2
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/40-checker/README.md +8 -3
- package/40-checker/bin/dsh-levels.mjs +7 -1
- package/40-checker/scripts/flow-collect.mjs +140 -21
- package/40-checker/scripts/level-audit.mjs +43 -5
- package/40-checker/scripts/run-tests.mjs +21 -0
- package/40-checker/src/flow-evidence.mjs +107 -2
- package/40-checker/src/index.mjs +1 -1
- package/40-checker/src/remediation.mjs +14 -9
- package/40-checker/src/sandbox-run.mjs +403 -0
- package/CHANGELOG.md +8 -1
- package/README.en.md +347 -0
- package/README.es.md +347 -0
- package/README.hi.md +347 -0
- package/README.md +295 -82
- package/README.pt.md +347 -0
- package/docs/assets/dsh-standards-flow-dark.svg +38 -0
- package/docs/assets/dsh-standards-flow-light.svg +38 -0
- package/package.json +2 -3
- package/spec/flow/README.md +21 -4
- package/spec/flow-evidence.schema.json +73 -3
- package/40-checker/test/attestation.test.mjs +0 -59
- package/40-checker/test/checks.test.mjs +0 -414
- package/40-checker/test/claim.test.mjs +0 -28
- package/40-checker/test/client-shape.test.mjs +0 -35
- package/40-checker/test/dco.test.mjs +0 -51
- package/40-checker/test/entry-validate.test.mjs +0 -172
- package/40-checker/test/flow-evidence.test.mjs +0 -295
- package/40-checker/test/governance.test.mjs +0 -76
- package/40-checker/test/label.test.mjs +0 -97
- package/40-checker/test/level-audit.test.mjs +0 -99
- package/40-checker/test/levels.test.mjs +0 -31
- package/40-checker/test/ops.test.mjs +0 -117
- package/40-checker/test/permissions.test.mjs +0 -40
- package/40-checker/test/remediation.test.mjs +0 -300
- package/40-checker/test/semver.test.mjs +0 -32
- package/40-checker/test/verify.test.mjs +0 -57
package/40-checker/README.md
CHANGED
|
@@ -19,7 +19,7 @@ node bin/dsh-plugin-verify.mjs /path/to/plugin --format json --out report.json
|
|
|
19
19
|
node bin/dsh-plugin-verify.mjs /path/to/plugin --format sarif --out report.sarif
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
`buildReport()` 与 `--format json` 默认带**加性** `advice[]`(来自 `spec/remediation.json` 的 68 条规则;FAIL/WARN detail 优先,无 detail 的运行时 FAIL 用 `check.clause`
|
|
22
|
+
`buildReport()` 与 `--format json` 默认带**加性** `advice[]`(来自 `spec/remediation.json` 的 68 条规则;FAIL/WARN detail 优先,无 detail 的运行时 FAIL 用 `check.clause` 兜底)。每条 advice 的 `gate` 是该 check 的最终结果(PASS/WARN/FAIL),`severity` 跟随 `gate`:被降级为 WARN 的 check 即使 detail 仍是 FAIL,也只给 WARN,detail 原因保留在 `reason`。`--no-advice` 可关闭;它不改变任何 check 的结果或 exit code。
|
|
23
23
|
|
|
24
24
|
## 2. 检查项
|
|
25
25
|
|
|
@@ -131,13 +131,18 @@ node scripts/level-audit.mjs ./my-bundle \
|
|
|
131
131
|
```bash
|
|
132
132
|
node bin/dsh-flow-collect.mjs --package ./my-plugin --out flow-evidence.json
|
|
133
133
|
# 等价:node bin/dsh-levels.mjs flow collect --package ./my-plugin --out flow-evidence.json
|
|
134
|
+
# 隔离运行(unshare 无网 + prlimit + 进程组超时杀树 + 白名单 env):
|
|
135
|
+
node bin/dsh-flow-collect.mjs --package ./my-plugin --sandbox --out flow-evidence.json
|
|
136
|
+
# 等价:node bin/dsh-levels.mjs flow sandbox --package ./my-plugin --out flow-evidence.json
|
|
134
137
|
```
|
|
135
138
|
|
|
136
139
|
- 只接受 `dsh-flow-evidence/2`(`spec/flow-evidence.schema.json`)。`level-audit --flow-evidence` 对 v1(`dsh-flow-journey/1`、`dsh-flow-runtime/1`)和裸 `{Jx:{status}}` 标 `deprecated`,不计 pass。
|
|
137
140
|
- 每个 J 必须至少 1 条 `checks[]` 且全部 `exitCode === 0`、断言成立、至少一条非 `static` 观测检查,才计 pass;`static` preflight 不是 runtime,`warn`/`blocked`/`na` 都不计 8/8。
|
|
138
|
-
- collector 把包复制到临时目录再跑,前后 `treeSha256`
|
|
141
|
+
- collector 把包复制到临时目录再跑,前后 `treeSha256` 必须一致;`treeStable:false` 或 `treeSha256After` 不同直接判证据无效(invalid),不是 warn。子进程 env 走白名单(不含 token/key/secret);不授予 `allowBuilds`,不自动 `allow-version`。
|
|
142
|
+
- `--sandbox` 在临时 HOME/DSH_HOME 下跑 `dsh plugin add` + boot:先试 `unshare -n`,不行再试 `unshare -Urn`;用 `prlimit` 设 `nproc`/`as` 并从 `/proc/self/limits` 回读实际生效项;超时 SIGKILL 整个进程组(Windows `taskkill /T /F`)。硬断言 `GH_TOKEN`/`NPM_TOKEN`/`GITHUB_TOKEN` 不在子进程 env,发现即中止。
|
|
143
|
+
- 只有**所有**隔离项都生效才写 `producer.kind=sandbox` / `claims.claimLevel=sandbox-attested`;任何一项缺失(`unshare-missing`、`prlimit-missing`、`env-token-present`、`home-not-isolated`)写 `sandbox-degraded` + `sandbox.degraded[]`,仍是 self-attested,`level-audit` 永不判 runtime。`--sandbox-no-network` / `--sandbox-no-limits` 用于强制验证降级路径。
|
|
139
144
|
- 前置缺失(无 dsh、无 `--previous`、宿主兼容门禁拒绝)记 `blocked` + reason,出口码 0;真失败出口码 1,并打印五要素提示(子检查 / 命令 / exitCode / stderr 末 20 行 / 日志绝对路径 + 条款锚)。
|
|
140
|
-
-
|
|
145
|
+
- 本地证据默认 `self-attested`:结构自洽 + 绑定此包 + 未过期,**不证明真跑过**。即使 `--sandbox` 通过,它也只是 containment(无 seccomp/cgroup/OCI 镜像/签名),不是认证或安全审计。`level-audit` 只有见到 `claims.claimLevel=sandbox-attested` 或 `producer.kind=sandbox` 才判 `runtime`;self/ci/degraded 证据保留 `passed` 计数但记 `not-run`。
|
|
141
146
|
|
|
142
147
|
## 4. 运行 L1–L3
|
|
143
148
|
|
|
@@ -16,8 +16,14 @@ let target = AUDIT
|
|
|
16
16
|
if (args[0] === 'flow' && args[1] === 'collect') {
|
|
17
17
|
args.splice(0, 2)
|
|
18
18
|
target = FLOW_COLLECT
|
|
19
|
+
} else if (args[0] === 'flow' && args[1] === 'sandbox') {
|
|
20
|
+
// Convenience alias for `flow collect --sandbox`.
|
|
21
|
+
args.splice(0, 2)
|
|
22
|
+
args.unshift('--sandbox')
|
|
23
|
+
target = FLOW_COLLECT
|
|
19
24
|
} else if (args[0] === 'flow') {
|
|
20
|
-
console.error('usage: dsh-levels flow collect --package <dir> [--out <file>] [--static] [--previous <dir>]')
|
|
25
|
+
console.error('usage: dsh-levels flow collect --package <dir> [--out <file>] [--static|--sandbox] [--previous <dir>]')
|
|
26
|
+
console.error(' dsh-levels flow sandbox --package <dir> [--out <file>] (= flow collect --sandbox)')
|
|
21
27
|
process.exit(2)
|
|
22
28
|
} else if (args[0] === 'audit') {
|
|
23
29
|
args.shift()
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
* 2. copy the package to a temp dir and run every journey against that copy
|
|
10
10
|
* only; the original directory is never written;
|
|
11
11
|
* 3. static preflight (`journey.mjs`) for J1-J8, then the runtime probe
|
|
12
|
-
* (`journey-runtime.mjs`
|
|
13
|
-
* an allowlisted child env;
|
|
14
|
-
*
|
|
12
|
+
* (`journey-runtime.mjs`, which runs `dsh plugin add` and boots the tree)
|
|
13
|
+
* with a temp HOME/DSH_HOME and an allowlisted child env; with
|
|
14
|
+
* `--sandbox` the runtime probe runs behind `unshare` (network),
|
|
15
|
+
* `prlimit` (nproc/as), and a process-group timeout that kills the whole
|
|
16
|
+
* tree. Any missing isolation item makes `producer.kind`
|
|
17
|
+
* `sandbox-degraded`, which is never runtime;
|
|
18
|
+
* 4. re-hash the original tree and record `subject.treeStable`; a changed
|
|
19
|
+
* tree invalidates the evidence;
|
|
15
20
|
* 5. write the JSON, per-check logs, the chain hash, and SHA256SUMS.
|
|
16
21
|
*
|
|
17
22
|
* Missing prerequisites (no dsh, no --previous, host compatibility gate
|
|
@@ -22,7 +27,8 @@
|
|
|
22
27
|
* Usage:
|
|
23
28
|
* node 40-checker/bin/dsh-flow-collect.mjs --package <dir> [--out flow-evidence.json]
|
|
24
29
|
* [--logs <dir>] [--previous <dir>] [--dsh <bin>] [--timeout <ms>]
|
|
25
|
-
* [--static] [--ci] [--
|
|
30
|
+
* [--static] [--ci] [--sandbox [--sandbox-no-network] [--sandbox-no-limits]
|
|
31
|
+
* [--sandbox-nproc <n>] [--sandbox-as <bytes>]] [--json]
|
|
26
32
|
*/
|
|
27
33
|
import fs from 'node:fs'
|
|
28
34
|
import os from 'node:os'
|
|
@@ -34,6 +40,9 @@ import {
|
|
|
34
40
|
sha256File, sha256Value, treeSha256, validateFlowEvidence,
|
|
35
41
|
} from '../src/flow-evidence.mjs'
|
|
36
42
|
import { sanitizedEnv } from '../src/sandbox-env.mjs'
|
|
43
|
+
import {
|
|
44
|
+
assertNoCredentialEnv, buildSandboxPlan, runInProcessTree, sandboxArgv,
|
|
45
|
+
} from '../src/sandbox-run.mjs'
|
|
37
46
|
|
|
38
47
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
39
48
|
const STD = path.resolve(HERE, '..', '..')
|
|
@@ -68,6 +77,14 @@ Options:
|
|
|
68
77
|
--timeout <ms> per-command budget (default: 120000)
|
|
69
78
|
--static skip the runtime probe (J1/J2/J4/J6 become static/blocked)
|
|
70
79
|
--ci mark producer.kind=ci, claims.claimLevel=ci-attested
|
|
80
|
+
--sandbox run the runtime probe under unshare/prlimit/process-group
|
|
81
|
+
timeout with an allowlisted env; producer.kind=sandbox only
|
|
82
|
+
when every isolation item is effective (otherwise
|
|
83
|
+
sandbox-degraded, never runtime)
|
|
84
|
+
--sandbox-no-network force the degraded path (records unshare-missing)
|
|
85
|
+
--sandbox-no-limits force the degraded path (records prlimit-missing)
|
|
86
|
+
--sandbox-nproc <n> RLIMIT_NPROC (default: current task count + headroom)
|
|
87
|
+
--sandbox-as <bytes> RLIMIT_AS (default: 4 GiB)
|
|
71
88
|
--no-assert-unchanged skip the before/after tree hash comparison (not recommended)
|
|
72
89
|
--json print the evidence JSON instead of the summary
|
|
73
90
|
--help show this help
|
|
@@ -82,6 +99,8 @@ function parse(argv) {
|
|
|
82
99
|
package: null, out: path.resolve('flow-evidence.json'), logs: null, previous: null,
|
|
83
100
|
dsh: process.env.DSH_BIN || null, timeout: 120_000, static: false, ci: false,
|
|
84
101
|
assertUnchanged: true, json: false,
|
|
102
|
+
sandbox: false, sandboxNoNetwork: false, sandboxNoLimits: false,
|
|
103
|
+
sandboxNproc: null, sandboxAs: 4 * 1024 ** 3,
|
|
85
104
|
}
|
|
86
105
|
for (let i = 0; i < argv.length; i += 1) {
|
|
87
106
|
const next = () => {
|
|
@@ -99,6 +118,11 @@ function parse(argv) {
|
|
|
99
118
|
else if (arg === '--timeout') out.timeout = Number(next())
|
|
100
119
|
else if (arg === '--static' || arg === '--no-runtime') out.static = true
|
|
101
120
|
else if (arg === '--ci') out.ci = true
|
|
121
|
+
else if (arg === '--sandbox') out.sandbox = true
|
|
122
|
+
else if (arg === '--sandbox-no-network' || arg === '--no-sandbox-network') out.sandboxNoNetwork = true
|
|
123
|
+
else if (arg === '--sandbox-no-limits' || arg === '--no-sandbox-limits') out.sandboxNoLimits = true
|
|
124
|
+
else if (arg === '--sandbox-nproc') out.sandboxNproc = Number(next())
|
|
125
|
+
else if (arg === '--sandbox-as') out.sandboxAs = Number(next())
|
|
102
126
|
else if (arg === '--no-assert-unchanged') out.assertUnchanged = false
|
|
103
127
|
else if (arg === '--json') out.json = true
|
|
104
128
|
else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
|
|
@@ -106,6 +130,10 @@ function parse(argv) {
|
|
|
106
130
|
}
|
|
107
131
|
if (!out.package) throw new Error('--package is required')
|
|
108
132
|
if (!Number.isFinite(out.timeout) || out.timeout < 1000) throw new Error('--timeout must be >= 1000')
|
|
133
|
+
if (out.sandboxNproc !== null && (!Number.isFinite(out.sandboxNproc) || out.sandboxNproc < 2)) throw new Error('--sandbox-nproc must be >= 2')
|
|
134
|
+
if (!Number.isFinite(out.sandboxAs) || out.sandboxAs < 64 * 1024 * 1024) throw new Error('--sandbox-as must be >= 67108864')
|
|
135
|
+
if (out.sandbox && out.static) throw new Error('--sandbox cannot be combined with --static')
|
|
136
|
+
if (out.sandbox && out.ci) throw new Error('--sandbox cannot be combined with --ci (a sandboxed run is sandbox-attested)')
|
|
109
137
|
return out
|
|
110
138
|
}
|
|
111
139
|
|
|
@@ -144,7 +172,7 @@ function writeLog(logDir, name, text) {
|
|
|
144
172
|
return { file, sha256: sha256Value(text ?? '') }
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
function main() {
|
|
175
|
+
async function main() {
|
|
148
176
|
const options = parse(process.argv.slice(2))
|
|
149
177
|
const pkg = path.resolve(options.package)
|
|
150
178
|
if (!fs.existsSync(path.join(pkg, 'package.json'))) throw new Error(`not a package directory: ${pkg}`)
|
|
@@ -162,12 +190,25 @@ function main() {
|
|
|
162
190
|
fs.mkdirSync(tmpHome, { recursive: true })
|
|
163
191
|
fs.mkdirSync(tmpCache, { recursive: true })
|
|
164
192
|
const childEnv = () => sanitizedEnv({
|
|
165
|
-
HOME: tmpHome, USERPROFILE: tmpHome, TMPDIR: path.join(tmpRoot, 'tmp'), TEMP: path.join(tmpRoot, 'tmp'), TMP: path.join(tmpRoot, 'tmp'),
|
|
193
|
+
HOME: tmpHome, DSH_HOME: tmpHome, USERPROFILE: tmpHome, TMPDIR: path.join(tmpRoot, 'tmp'), TEMP: path.join(tmpRoot, 'tmp'), TMP: path.join(tmpRoot, 'tmp'),
|
|
166
194
|
npm_config_cache: tmpCache, npm_config_audit: 'false', npm_config_fund: 'false', npm_config_update_notifier: 'false',
|
|
167
195
|
})
|
|
196
|
+
// The sandbox is planned before any plugin code runs. `assertNoCredentialEnv`
|
|
197
|
+
// is a hard gate: a surviving token aborts collection instead of booting with it.
|
|
198
|
+
let sandboxPlan = null
|
|
168
199
|
|
|
169
200
|
const warnings = []
|
|
170
201
|
try {
|
|
202
|
+
if (options.sandbox) {
|
|
203
|
+
assertNoCredentialEnv(childEnv())
|
|
204
|
+
sandboxPlan = buildSandboxPlan({
|
|
205
|
+
env: childEnv(),
|
|
206
|
+
nproc: options.sandboxNproc,
|
|
207
|
+
asBytes: options.sandboxAs,
|
|
208
|
+
force: { network: options.sandboxNoNetwork, limits: options.sandboxNoLimits },
|
|
209
|
+
})
|
|
210
|
+
sandboxPlan.env = assertNoCredentialEnv(childEnv())
|
|
211
|
+
}
|
|
171
212
|
// 0. Never let our own output/log dirs count as a tree change.
|
|
172
213
|
const treeExclude = []
|
|
173
214
|
for (const candidate of [outFile, logDir]) {
|
|
@@ -245,13 +286,59 @@ function main() {
|
|
|
245
286
|
if (options.static) runtimeSkipped = 'runtime collection disabled (--static)'
|
|
246
287
|
else if (dshVersion === null) runtimeSkipped = `dsh executable not usable: ${dshBin} (exit ${versionRun.exitCode})`
|
|
247
288
|
let runtimeVerdict = null
|
|
289
|
+
let sandboxExecution = null
|
|
248
290
|
if (runtimeSkipped === null) {
|
|
249
291
|
const args = [JOURNEY_RUNTIME, '--package', copy, '--dsh', dshBin, '--json', '--log-dir', logDir, '--timeout', String(options.timeout)]
|
|
250
292
|
if (previousCopy) args.push('--previous', previousCopy)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
293
|
+
let runtimeRun
|
|
294
|
+
if (sandboxPlan) {
|
|
295
|
+
// journey-runtime spawns `dsh plugin add` and the boot as its own
|
|
296
|
+
// children; they inherit this process group and network namespace.
|
|
297
|
+
const argv = sandboxArgv(sandboxPlan, process.execPath, args)
|
|
298
|
+
runtimeRun = await runInProcessTree({
|
|
299
|
+
command: argv[0],
|
|
300
|
+
args: argv.slice(1),
|
|
301
|
+
cwd: copy,
|
|
302
|
+
env: childEnv(),
|
|
303
|
+
timeoutMs: options.timeout * 2 + 60_000,
|
|
304
|
+
})
|
|
305
|
+
sandboxExecution = {
|
|
306
|
+
command: argv,
|
|
307
|
+
exitCode: runtimeRun.exitCode,
|
|
308
|
+
timedOut: runtimeRun.timedOut,
|
|
309
|
+
killed: runtimeRun.killed,
|
|
310
|
+
spawnError: runtimeRun.spawnError ?? null,
|
|
311
|
+
durationMs: runtimeRun.durationMs,
|
|
312
|
+
pid: runtimeRun.pid ?? null,
|
|
313
|
+
processTree: {
|
|
314
|
+
mechanism: runtimeRun.treeKill?.mechanism ?? sandboxPlan.processTreeKill.mechanism,
|
|
315
|
+
signalled: runtimeRun.treeKill?.signalled ?? null,
|
|
316
|
+
cleared: runtimeRun.treeKill?.cleared ?? null,
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
if (runtimeRun.spawnError) {
|
|
320
|
+
// The probe passed but the sandboxed spawn itself failed: do not keep
|
|
321
|
+
// calling this run `sandbox`.
|
|
322
|
+
sandboxPlan.degraded = [...new Set([...sandboxPlan.degraded, 'sandbox-execution-failed'])]
|
|
323
|
+
sandboxPlan.mode = 'sandbox-degraded'
|
|
324
|
+
}
|
|
325
|
+
if (runtimeRun.timedOut) {
|
|
326
|
+
runtimeSkipped = `sandboxed runtime probe timed out after ${options.timeout * 2 + 60_000} ms; whole process tree killed (${sandboxExecution.processTree.mechanism}, cleared=${sandboxExecution.processTree.cleared})`
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
runtimeRun = run(process.execPath, args, { env: childEnv(), timeout: options.timeout * 2 + 60_000 })
|
|
330
|
+
}
|
|
331
|
+
const stdoutText = runtimeRun.stdoutText ?? runtimeRun.stdout ?? ''
|
|
332
|
+
const stderrText = runtimeRun.stderrText ?? runtimeRun.stderr ?? ''
|
|
333
|
+
writeLog(logDir, 'runtime-journey.json', stdoutText)
|
|
334
|
+
if (stderrText) writeLog(logDir, 'runtime-journey.err.log', stderrText)
|
|
335
|
+
if (runtimeSkipped === null) {
|
|
336
|
+
try {
|
|
337
|
+
runtimeVerdict = JSON.parse(stdoutText)
|
|
338
|
+
} catch {
|
|
339
|
+
runtimeSkipped = `runtime probe produced no JSON (exit ${runtimeRun.exitCode}): ${tailLines(stderrText, 5)}`
|
|
340
|
+
}
|
|
341
|
+
}
|
|
255
342
|
if (runtimeVerdict) {
|
|
256
343
|
for (const check of runtimeVerdict.checks ?? []) {
|
|
257
344
|
const mapped = toEvidenceCheck(check, logDir, toEvidencePath)
|
|
@@ -267,6 +354,9 @@ function main() {
|
|
|
267
354
|
}
|
|
268
355
|
}
|
|
269
356
|
}
|
|
357
|
+
if (sandboxPlan) {
|
|
358
|
+
writeLog(logDir, 'sandbox-plan.json', JSON.stringify({ ...sandboxPlan, execution: sandboxExecution }, null, 2))
|
|
359
|
+
}
|
|
270
360
|
if (runtimeSkipped !== null) warnings.push(`runtime probe skipped: ${runtimeSkipped}`)
|
|
271
361
|
|
|
272
362
|
// 6. Journey statuses + reasons (never upgrade a skip to a pass).
|
|
@@ -342,18 +432,40 @@ function main() {
|
|
|
342
432
|
? 'flow-fail'
|
|
343
433
|
: statuses.includes('blocked') ? 'flow-blocked' : statuses.every(status => status === 'pass') ? 'flow-pass' : 'flow-warn'
|
|
344
434
|
const producerCommand = [process.execPath, path.relative(STD, fileURLToPath(import.meta.url)).split(path.sep).join('/'), ...process.argv.slice(2)]
|
|
435
|
+
const sandboxExecuted = Boolean(sandboxExecution)
|
|
436
|
+
const sandboxKind = sandboxPlan && sandboxExecuted
|
|
437
|
+
? (sandboxPlan.degraded.length === 0 ? 'sandbox' : 'sandbox-degraded')
|
|
438
|
+
: null
|
|
439
|
+
const producerKind = sandboxKind ?? (options.ci ? 'ci' : 'local')
|
|
440
|
+
const notClaimed = [...NOT_CLAIMED]
|
|
441
|
+
if (sandboxKind === 'sandbox') {
|
|
442
|
+
const localIndex = notClaimed.findIndex(item => item.startsWith('sandbox confinement'))
|
|
443
|
+
if (localIndex >= 0) notClaimed.splice(localIndex, 1)
|
|
444
|
+
notClaimed.push('OCI/container image, seccomp, and cgroup memory/pids limits (not implemented)', 'signature (not produced)')
|
|
445
|
+
} else if (sandboxKind === 'sandbox-degraded') {
|
|
446
|
+
notClaimed.push(`sandbox confinement (degraded: ${sandboxPlan.degraded.join(', ')})`)
|
|
447
|
+
}
|
|
448
|
+
const claimLevel = sandboxKind === 'sandbox' ? 'sandbox-attested' : options.ci ? 'ci-attested' : 'self-attested'
|
|
449
|
+
const method = sandboxKind === 'sandbox'
|
|
450
|
+
? 'sandboxed collector: allowlisted env (no tokens), temporary HOME/DSH_HOME, network+user namespace, prlimit nproc/as, process-group SIGKILL timeout'
|
|
451
|
+
: sandboxKind === 'sandbox-degraded'
|
|
452
|
+
? `sandboxed collector DEGRADED (${sandboxPlan.degraded.join(', ')}): allowlisted env, temporary HOME/DSH_HOME, process-group SIGKILL timeout; every missing isolation item is recorded`
|
|
453
|
+
: 'local-collector: read-only copy, allowlisted child env, no allowBuilds, no exemption'
|
|
454
|
+
const sandbox = sandboxPlan ? { ...sandboxPlan, executed: sandboxExecuted, execution: sandboxExecution } : null
|
|
345
455
|
const document = finalizeEvidence({
|
|
346
456
|
schema: FLOW_EVIDENCE_SCHEMA,
|
|
347
457
|
generatedAt,
|
|
348
|
-
producer: {
|
|
458
|
+
producer: {
|
|
459
|
+
command: producerCommand,
|
|
460
|
+
at: generatedAt,
|
|
461
|
+
kind: producerKind,
|
|
462
|
+
runner: sandboxPlan ? 'flow-collect/2-sandbox' : 'flow-collect/1',
|
|
463
|
+
},
|
|
464
|
+
...(sandbox ? { sandbox } : {}),
|
|
349
465
|
subject,
|
|
350
466
|
host,
|
|
351
467
|
journeys,
|
|
352
|
-
claims: {
|
|
353
|
-
claimLevel: options.ci ? 'ci-attested' : 'self-attested',
|
|
354
|
-
notClaimed: NOT_CLAIMED,
|
|
355
|
-
method: 'local-collector: read-only copy, allowlisted child env, no allowBuilds, no exemption',
|
|
356
|
-
},
|
|
468
|
+
claims: { claimLevel, notClaimed, method },
|
|
357
469
|
conclusion,
|
|
358
470
|
})
|
|
359
471
|
const verdict = validateFlowEvidence(document)
|
|
@@ -385,8 +497,17 @@ function main() {
|
|
|
385
497
|
` totals : ${JSON.stringify(counts)}`,
|
|
386
498
|
` conclusion: ${conclusion}`,
|
|
387
499
|
` chain : ${document.chain.evidenceHash}`,
|
|
500
|
+
...(sandboxPlan ? [
|
|
501
|
+
` sandbox : ${sandboxPlan.mode}${sandboxPlan.degraded.length > 0 ? ` degraded=[${sandboxPlan.degraded.join(', ')}]` : ''}`,
|
|
502
|
+
` env=${sandboxPlan.env.policy} tokens-absent=${sandboxPlan.env.absent} network=${sandboxPlan.network.isolated ? sandboxPlan.network.mechanism : 'off'} limits=${sandboxPlan.limits.available ? sandboxPlan.limits.applied.join('+') : 'off'} home=${sandboxPlan.home.isolated ? 'temp' : 'real'}`,
|
|
503
|
+
` execution=${sandboxExecuted ? `exit=${sandboxExecution.exitCode} timedOut=${sandboxExecution.timedOut} killed=${sandboxExecution.killed} treeCleared=${sandboxExecution.processTree.cleared}` : 'not-run'}`,
|
|
504
|
+
] : []),
|
|
388
505
|
` logs : ${logDir} (+ ${path.join(outDir, 'SHA256SUMS')})`,
|
|
389
|
-
|
|
506
|
+
sandboxKind === 'sandbox'
|
|
507
|
+
? 'Claims: sandbox-attested (containment only; no OCI image, seccomp, cgroup, signature); not a certification or security audit.'
|
|
508
|
+
: sandboxKind === 'sandbox-degraded'
|
|
509
|
+
? `Claims: self-attested; sandbox DEGRADED (${sandboxPlan.degraded.join(', ')}). Degraded evidence is never FLOW runtime.`
|
|
510
|
+
: 'Claims: self-attested structure + binding only; no certification, no security audit, not a sandbox.',
|
|
390
511
|
]
|
|
391
512
|
if (warnings.length > 0) {
|
|
392
513
|
lines.push('Warnings:')
|
|
@@ -486,9 +607,7 @@ function collectHints(journeyId, checks, hints, outDir, logDir, toEvidencePath)
|
|
|
486
607
|
}
|
|
487
608
|
}
|
|
488
609
|
|
|
489
|
-
|
|
490
|
-
main()
|
|
491
|
-
} catch (error) {
|
|
610
|
+
main().catch(error => {
|
|
492
611
|
console.error(`flow-collect: ${error.message}`)
|
|
493
612
|
process.exitCode = 3
|
|
494
|
-
}
|
|
613
|
+
})
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* FLOW reads an optional `dsh-flow-evidence/2` file; with no file it reports
|
|
9
9
|
* the checklist as `not-run` and never invents a pass. v1 schemas and
|
|
10
10
|
* bare {Jx:{status}} rows are `deprecated` and never count as a pass.
|
|
11
|
+
* Only sandbox-attested evidence (claims.claimLevel =
|
|
12
|
+
* 'sandbox-attested' or producer.kind = 'sandbox') may become
|
|
13
|
+
* `runtime`; self-attested/ci-attested keeps its pass count but stays
|
|
14
|
+
* `not-run`.
|
|
11
15
|
* BOOST reads an optional LEV evidence file; with no file it reports
|
|
12
16
|
* `not-measured`. It does not run benchmarks for you.
|
|
13
17
|
*
|
|
@@ -161,6 +165,11 @@ function classifyFlow(evidence) {
|
|
|
161
165
|
return {
|
|
162
166
|
state: 'not-run',
|
|
163
167
|
schema: null,
|
|
168
|
+
claimLevel: null,
|
|
169
|
+
producerKind: null,
|
|
170
|
+
sandboxMode: null,
|
|
171
|
+
sandboxDegraded: [],
|
|
172
|
+
sandboxAttested: false,
|
|
164
173
|
deprecated: false,
|
|
165
174
|
invalid: false,
|
|
166
175
|
evidenceHash: null,
|
|
@@ -177,6 +186,11 @@ function classifyFlow(evidence) {
|
|
|
177
186
|
}
|
|
178
187
|
const base = {
|
|
179
188
|
schema: verdict.schema,
|
|
189
|
+
claimLevel: verdict.claimLevel ?? null,
|
|
190
|
+
producerKind: verdict.producerKind ?? null,
|
|
191
|
+
sandboxMode: verdict.sandbox?.mode ?? null,
|
|
192
|
+
sandboxDegraded: verdict.sandbox?.degraded ?? [],
|
|
193
|
+
sandboxAttested: verdict.sandboxAttested === true,
|
|
180
194
|
deprecated: verdict.kind === 'v1-deprecated' || verdict.kind === 'legacy',
|
|
181
195
|
invalid: false,
|
|
182
196
|
evidenceHash: null,
|
|
@@ -222,19 +236,37 @@ function classifyFlow(evidence) {
|
|
|
222
236
|
const passed = ids.filter(id => journeys[id].passes)
|
|
223
237
|
const failed = ids.filter(id => journeys[id].status === 'fail' && journeys[id].declared !== 'blocked')
|
|
224
238
|
const blocked = ids.filter(id => journeys[id].declared === 'blocked')
|
|
239
|
+
// Passing journeys are necessary but not sufficient: only sandbox-attested
|
|
240
|
+
// evidence may be reported as runtime. A self-attested/ci-attested document
|
|
241
|
+
// keeps its pass count but never upgrades to runtime. An honest FAIL/BLOCKED
|
|
242
|
+
// stays visible instead of being hidden behind the attestation level.
|
|
243
|
+
const runtimeEligible = base.sandboxAttested
|
|
225
244
|
const state = failed.length > 0
|
|
226
245
|
? 'failed'
|
|
227
|
-
:
|
|
228
|
-
? '
|
|
229
|
-
:
|
|
246
|
+
: blocked.length > 0
|
|
247
|
+
? 'blocked'
|
|
248
|
+
: runtimeEligible && passed.length === ids.length
|
|
249
|
+
? 'runtime'
|
|
250
|
+
: 'not-run'
|
|
230
251
|
const reasons = []
|
|
252
|
+
const degraded = Array.isArray(base.sandboxDegraded) ? base.sandboxDegraded : []
|
|
253
|
+
if (degraded.length > 0) {
|
|
254
|
+
reasons.push(`sandbox-degraded (${degraded.join(', ')}); runtime requires every isolation item effective`)
|
|
255
|
+
}
|
|
256
|
+
if (!runtimeEligible && failed.length === 0 && degraded.length === 0) {
|
|
257
|
+
reasons.push(`${base.claimLevel ?? 'non-sandbox'} evidence; runtime requires sandbox-attested`)
|
|
258
|
+
}
|
|
231
259
|
if (failed.length > 0) reasons.push(`failed journeys: ${failed.join(', ')}`)
|
|
232
260
|
if (blocked.length > 0) reasons.push(`blocked journeys (never a pass): ${blocked.join(', ')}`)
|
|
233
261
|
if (failed.length === 0 && passed.length !== ids.length) {
|
|
234
262
|
const rest = ids.filter(id => !passed.includes(id) && !blocked.includes(id))
|
|
235
263
|
if (rest.length > 0) reasons.push(`${passed.length}/8 journeys pass; ${rest.length} static/warn/not-run`)
|
|
236
264
|
}
|
|
237
|
-
if (passed.length === ids.length)
|
|
265
|
+
if (passed.length === ids.length) {
|
|
266
|
+
reasons.push(runtimeEligible
|
|
267
|
+
? 'all J1-J8 journey checks pass (exitCode 0, assertions held)'
|
|
268
|
+
: 'all J1-J8 journey checks pass but the evidence is not sandbox-attested; not runtime')
|
|
269
|
+
}
|
|
238
270
|
return {
|
|
239
271
|
...base,
|
|
240
272
|
state,
|
|
@@ -338,7 +370,13 @@ function nextActions(entry, flow, boost) {
|
|
|
338
370
|
if (entry.g4 && entry.g4.hasClient && entry.g4.ok === false) actions.push(`ENTRY: fix G4 — ${entry.g4.reason}`)
|
|
339
371
|
for (const warning of entry.warnings) actions.push(`ENTRY: consider ${warning.id} — ${warning.reason}`)
|
|
340
372
|
if (entry.state === 'recognized') actions.push('ENTRY: run with --host to replace static recognition with a real G0-G3 boot')
|
|
341
|
-
if (flow.
|
|
373
|
+
if (flow.sandboxDegraded?.length > 0) {
|
|
374
|
+
actions.push(`FLOW: sandbox degraded (${flow.sandboxDegraded.join(', ')}); re-run where unshare and prlimit are effective; degraded evidence is never runtime`)
|
|
375
|
+
} else if (flow.state === 'not-run' && flow.claimLevel && flow.sandboxAttested !== true) {
|
|
376
|
+
actions.push(`FLOW: ${flow.claimLevel} evidence keeps ${flow.passed}/8 journey passes but is not runtime; re-run under a sandbox and set claims.claimLevel=sandbox-attested (or producer.kind=sandbox)`)
|
|
377
|
+
} else if (flow.state === 'not-run') {
|
|
378
|
+
actions.push('FLOW: record J1-J8 with node 40-checker/bin/dsh-flow-collect.mjs; do not claim FLOW until they run')
|
|
379
|
+
}
|
|
342
380
|
if (flow.state === 'deprecated') actions.push('FLOW: v1/bare evidence is deprecated; collect a dsh-flow-evidence/2 file (spec/flow-evidence.schema.json)')
|
|
343
381
|
if (flow.state === 'invalid') actions.push('FLOW: flow evidence failed structural validation; fix the listed fields or recollect')
|
|
344
382
|
if (flow.state === 'failed') actions.push(`FLOW: fix failed journeys: ${Object.entries(flow.journeys).filter(([, row]) => row.status === 'fail').map(([id]) => id).join(', ')}`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Runs the checker tests when the dev-only test tree is present. The npm package
|
|
2
|
+
// does not ship 40-checker/test, so an installed copy must not fail `npm test`
|
|
3
|
+
// on a missing directory.
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const testDir = join(here, '..', 'test')
|
|
11
|
+
if (!existsSync(testDir)) {
|
|
12
|
+
console.log('checker tests are not shipped in the npm package; nothing to run')
|
|
13
|
+
process.exit(0)
|
|
14
|
+
}
|
|
15
|
+
const files = readdirSync(testDir).filter((f) => f.endsWith('.test.mjs')).map((f) => join(testDir, f))
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
console.log('no test files found; nothing to run')
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
const r = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit' })
|
|
21
|
+
process.exit(r.status ?? 1)
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
* status alone): at least one check, every check has exitCode 0, at least one
|
|
14
14
|
* observed (non-static) check, and every observed check's assertions hold.
|
|
15
15
|
* A journey declared `warn`/`blocked`/`na`/`static`/`not-run` never counts.
|
|
16
|
+
*
|
|
17
|
+
* Runtime rule: passing journeys are necessary but not sufficient. Only
|
|
18
|
+
* sandbox-attested evidence (`claims.claimLevel === 'sandbox-attested'` or
|
|
19
|
+
* `producer.kind === 'sandbox'`) may be reported as `flow.state = 'runtime'`.
|
|
20
|
+
* `self-attested`/`ci-attested` documents keep their `passed` count but stay
|
|
21
|
+
* `not-run`, because structural validation cannot prove where commands ran.
|
|
22
|
+
* A `sandbox-degraded` producer (any isolation item missing) is never runtime,
|
|
23
|
+
* and a document whose `subject.treeStable` is false is structurally invalid:
|
|
24
|
+
* a collector that changed the inspected tree has no trustworthy evidence.
|
|
16
25
|
*/
|
|
17
26
|
import crypto from 'node:crypto'
|
|
18
27
|
import fs from 'node:fs'
|
|
@@ -163,6 +172,23 @@ export function journeyCountsAsPass(journey) {
|
|
|
163
172
|
return declared === null || declared === 'pass'
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Runtime eligibility. A local collector run is `self-attested`
|
|
177
|
+
* (`ci-attested` with `--ci`): structurally valid, but it does not prove that
|
|
178
|
+
* commands ran, let alone in a sandbox. Only `claims.claimLevel ===
|
|
179
|
+
* 'sandbox-attested'` or the producer-side equivalent `producer.kind ===
|
|
180
|
+
* 'sandbox'` may be reported as FLOW runtime.
|
|
181
|
+
*/
|
|
182
|
+
export function isSandboxAttested(document) {
|
|
183
|
+
if (!isPlainObject(document)) return false
|
|
184
|
+
// Degradation is disqualifying even if a claim was left at sandbox-attested:
|
|
185
|
+
// the producer admits an isolation item was missing.
|
|
186
|
+
if (document.producer?.kind === 'sandbox-degraded') return false
|
|
187
|
+
if (document.sandbox?.mode === 'sandbox-degraded') return false
|
|
188
|
+
if (Array.isArray(document.sandbox?.degraded) && document.sandbox.degraded.length > 0) return false
|
|
189
|
+
return document.claims?.claimLevel === 'sandbox-attested' || document.producer?.kind === 'sandbox'
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
function emptyVerdict(kind, schema, errors) {
|
|
167
193
|
return {
|
|
168
194
|
kind,
|
|
@@ -176,6 +202,9 @@ function emptyVerdict(kind, schema, errors) {
|
|
|
176
202
|
host: null,
|
|
177
203
|
generatedAt: null,
|
|
178
204
|
conclusion: null,
|
|
205
|
+
claimLevel: null,
|
|
206
|
+
producerKind: null,
|
|
207
|
+
sandboxAttested: false,
|
|
179
208
|
}
|
|
180
209
|
}
|
|
181
210
|
|
|
@@ -247,8 +276,8 @@ function validateV2(document) {
|
|
|
247
276
|
if (!Array.isArray(command) || command.length === 0 || command.some(item => typeof item !== 'string')) {
|
|
248
277
|
errors.push('producer.command must be a non-empty array of strings')
|
|
249
278
|
}
|
|
250
|
-
if ('kind' in document.producer && document.producer.kind !== null && !['local', 'ci', 'sandbox'].includes(document.producer.kind)) {
|
|
251
|
-
errors.push('producer.kind must be local, ci, or sandbox')
|
|
279
|
+
if ('kind' in document.producer && document.producer.kind !== null && !['local', 'ci', 'sandbox', 'sandbox-degraded'].includes(document.producer.kind)) {
|
|
280
|
+
errors.push('producer.kind must be local, ci, sandbox, or sandbox-degraded')
|
|
252
281
|
}
|
|
253
282
|
for (const key of ['runner', 'actor', 'runUrl']) {
|
|
254
283
|
if (key in document.producer && document.producer[key] !== null) requireString(document.producer[key], `producer.${key}`)
|
|
@@ -271,6 +300,12 @@ function validateV2(document) {
|
|
|
271
300
|
}
|
|
272
301
|
}
|
|
273
302
|
if ('source' in subject) requireString(subject.source, 'subject.source')
|
|
303
|
+
if (subject.treeStable === false) {
|
|
304
|
+
errors.push('subject.treeStable is false: the package tree changed during collection')
|
|
305
|
+
}
|
|
306
|
+
if ('treeSha256After' in subject && subject.treeSha256After && subject.treeSha256After !== subject.treeSha256 && subject.treeStable !== false) {
|
|
307
|
+
errors.push('subject.treeSha256After differs from subject.treeSha256: the package tree changed during collection')
|
|
308
|
+
}
|
|
274
309
|
}
|
|
275
310
|
|
|
276
311
|
if (requireObject(document.host, 'host')) {
|
|
@@ -323,6 +358,8 @@ function validateV2(document) {
|
|
|
323
358
|
if ('method' in claims) requireString(claims.method, 'claims.method')
|
|
324
359
|
}
|
|
325
360
|
|
|
361
|
+
validateSandbox(document, errors)
|
|
362
|
+
|
|
326
363
|
if (requireObject(document.chain, 'chain')) {
|
|
327
364
|
requireHash(document.chain.evidenceHash, 'chain.evidenceHash')
|
|
328
365
|
for (const key of Object.keys(document.chain)) if (key !== 'evidenceHash') errors.push(`chain.${key} is not allowed`)
|
|
@@ -366,6 +403,74 @@ function validateV2(document) {
|
|
|
366
403
|
host: isPlainObject(document.host) ? document.host : null,
|
|
367
404
|
generatedAt: document.generatedAt ?? null,
|
|
368
405
|
conclusion: document.conclusion ?? null,
|
|
406
|
+
claimLevel: isPlainObject(document.claims) ? document.claims.claimLevel ?? null : null,
|
|
407
|
+
producerKind: isPlainObject(document.producer) ? document.producer.kind ?? null : null,
|
|
408
|
+
sandbox: isPlainObject(document.sandbox)
|
|
409
|
+
? {
|
|
410
|
+
mode: document.sandbox.mode ?? null,
|
|
411
|
+
degraded: Array.isArray(document.sandbox.degraded) ? [...document.sandbox.degraded] : [],
|
|
412
|
+
}
|
|
413
|
+
: null,
|
|
414
|
+
sandboxAttested: isSandboxAttested(document),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function validateSandbox(doc, errors) {
|
|
418
|
+
const kind = isPlainObject(doc.producer) ? doc.producer.kind ?? null : null
|
|
419
|
+
const declaredSandbox = 'sandbox' in doc
|
|
420
|
+
if (!declaredSandbox && kind !== 'sandbox' && kind !== 'sandbox-degraded') return
|
|
421
|
+
if (!declaredSandbox) {
|
|
422
|
+
errors.push('sandbox is required when producer.kind is sandbox or sandbox-degraded')
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
const sandbox = doc.sandbox
|
|
426
|
+
if (!isPlainObject(sandbox)) {
|
|
427
|
+
errors.push('sandbox must be an object')
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
if (!['sandbox', 'sandbox-degraded'].includes(sandbox.mode)) {
|
|
431
|
+
errors.push('sandbox.mode must be sandbox or sandbox-degraded')
|
|
432
|
+
}
|
|
433
|
+
const degraded = Array.isArray(sandbox.degraded) ? sandbox.degraded : null
|
|
434
|
+
if (!degraded || degraded.some(item => typeof item !== 'string' || item.length === 0)) {
|
|
435
|
+
errors.push('sandbox.degraded must be an array of non-empty strings')
|
|
436
|
+
}
|
|
437
|
+
const list = degraded ?? []
|
|
438
|
+
if (sandbox.mode === 'sandbox' && list.length > 0) errors.push('sandbox.mode is sandbox but sandbox.degraded is not empty')
|
|
439
|
+
if (sandbox.mode === 'sandbox-degraded' && list.length === 0) errors.push('sandbox.mode is sandbox-degraded but sandbox.degraded is empty')
|
|
440
|
+
if ('env' in sandbox) {
|
|
441
|
+
if (!isPlainObject(sandbox.env)) errors.push('sandbox.env must be an object')
|
|
442
|
+
else if (typeof sandbox.env.absent !== 'boolean') errors.push('sandbox.env.absent must be a boolean')
|
|
443
|
+
}
|
|
444
|
+
if ('network' in sandbox && (!isPlainObject(sandbox.network) || typeof sandbox.network.isolated !== 'boolean')) {
|
|
445
|
+
errors.push('sandbox.network.isolated must be a boolean')
|
|
446
|
+
}
|
|
447
|
+
if ('limits' in sandbox) {
|
|
448
|
+
if (!isPlainObject(sandbox.limits)) errors.push('sandbox.limits must be an object')
|
|
449
|
+
else {
|
|
450
|
+
if ('available' in sandbox.limits && typeof sandbox.limits.available !== 'boolean') errors.push('sandbox.limits.available must be a boolean')
|
|
451
|
+
if ('applied' in sandbox.limits && (!Array.isArray(sandbox.limits.applied) || sandbox.limits.applied.some(item => typeof item !== 'string'))) {
|
|
452
|
+
errors.push('sandbox.limits.applied must be an array of strings')
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if ('home' in sandbox && (!isPlainObject(sandbox.home) || typeof sandbox.home.isolated !== 'boolean')) {
|
|
457
|
+
errors.push('sandbox.home.isolated must be a boolean')
|
|
458
|
+
}
|
|
459
|
+
if ('executed' in sandbox && typeof sandbox.executed !== 'boolean') errors.push('sandbox.executed must be a boolean')
|
|
460
|
+
if (kind === 'sandbox') {
|
|
461
|
+
if (sandbox.mode !== 'sandbox') errors.push('producer.kind=sandbox requires sandbox.mode=sandbox')
|
|
462
|
+
if (list.length > 0) errors.push('producer.kind=sandbox requires an empty sandbox.degraded list')
|
|
463
|
+
if (sandbox.executed !== true) errors.push('producer.kind=sandbox requires sandbox.executed=true')
|
|
464
|
+
if (sandbox.env?.absent !== true) errors.push('producer.kind=sandbox requires sandbox.env.absent=true')
|
|
465
|
+
if (sandbox.network?.isolated !== true) errors.push('producer.kind=sandbox requires sandbox.network.isolated=true')
|
|
466
|
+
if (sandbox.limits?.available !== true) errors.push('producer.kind=sandbox requires sandbox.limits.available=true')
|
|
467
|
+
if (sandbox.home?.isolated !== true) errors.push('producer.kind=sandbox requires sandbox.home.isolated=true')
|
|
468
|
+
}
|
|
469
|
+
if (kind === 'sandbox-degraded') {
|
|
470
|
+
if (sandbox.mode !== 'sandbox-degraded') errors.push('producer.kind=sandbox-degraded requires sandbox.mode=sandbox-degraded')
|
|
471
|
+
if (list.length === 0) errors.push('producer.kind=sandbox-degraded requires at least one sandbox.degraded reason')
|
|
472
|
+
if (doc.claims?.claimLevel === 'sandbox-attested') errors.push('producer.kind=sandbox-degraded cannot claim sandbox-attested')
|
|
473
|
+
}
|
|
369
474
|
}
|
|
370
475
|
|
|
371
476
|
function validateCheck(check, label) {
|
package/40-checker/src/index.mjs
CHANGED
|
@@ -7,5 +7,5 @@ export { buildReport, formatText, formatSarif, writeReport } from './report.mjs'
|
|
|
7
7
|
export { joinAdvice, DOCS_BASE } from './remediation.mjs'
|
|
8
8
|
export { satisfies, validRange, parseVersion, compareVersions } from './semver.mjs'
|
|
9
9
|
export { parseInstallSpec } from './install-spec.mjs'
|
|
10
|
-
export { FLOW_IDS, FLOW_EVIDENCE_SCHEMA, evidenceHash, finalizeEvidence, journeyAggregate, treeSha256, validateFlowEvidence } from './flow-evidence.mjs'
|
|
10
|
+
export { FLOW_IDS, FLOW_EVIDENCE_SCHEMA, evidenceHash, finalizeEvidence, journeyAggregate, isSandboxAttested, treeSha256, validateFlowEvidence } from './flow-evidence.mjs'
|
|
11
11
|
export { sanitizedEnv, scrubProcessEnv, isSecretEnvName, isAllowedEnvName, envPolicy } from './sandbox-env.mjs'
|