dsh-standards 1.2.0 → 1.2.1
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 +2 -2
- package/40-checker/scripts/level-audit.mjs +33 -5
- package/40-checker/src/flow-evidence.mjs +24 -0
- package/40-checker/src/index.mjs +1 -1
- package/40-checker/src/remediation.mjs +14 -9
- package/CHANGELOG.md +3 -1
- package/package.json +1 -2
- package/spec/flow/README.md +4 -0
- package/spec/flow-evidence.schema.json +7 -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
|
|
|
@@ -137,7 +137,7 @@ node bin/dsh-flow-collect.mjs --package ./my-plugin --out flow-evidence.json
|
|
|
137
137
|
- 每个 J 必须至少 1 条 `checks[]` 且全部 `exitCode === 0`、断言成立、至少一条非 `static` 观测检查,才计 pass;`static` preflight 不是 runtime,`warn`/`blocked`/`na` 都不计 8/8。
|
|
138
138
|
- collector 把包复制到临时目录再跑,前后 `treeSha256` 必须一致;子进程 env 走白名单(不含 token/key/secret);不授予 `allowBuilds`,不自动 `allow-version`。
|
|
139
139
|
- 前置缺失(无 dsh、无 `--previous`、宿主兼容门禁拒绝)记 `blocked` + reason,出口码 0;真失败出口码 1,并打印五要素提示(子检查 / 命令 / exitCode / stderr 末 20 行 / 日志绝对路径 + 条款锚)。
|
|
140
|
-
- 本地证据只是 `self-attested`:结构自洽 + 绑定此包 +
|
|
140
|
+
- 本地证据只是 `self-attested`:结构自洽 + 绑定此包 + 未过期,**不证明真跑过**,也不是沙箱、认证或安全审计。`level-audit` 只有见到 `claims.claimLevel=sandbox-attested` 或 `producer.kind=sandbox` 才判 `runtime`;self/ci 证据保留 `passed` 计数但记 `not-run`。
|
|
141
141
|
|
|
142
142
|
## 4. 运行 L1–L3
|
|
143
143
|
|
|
@@ -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,9 @@ function classifyFlow(evidence) {
|
|
|
161
165
|
return {
|
|
162
166
|
state: 'not-run',
|
|
163
167
|
schema: null,
|
|
168
|
+
claimLevel: null,
|
|
169
|
+
producerKind: null,
|
|
170
|
+
sandboxAttested: false,
|
|
164
171
|
deprecated: false,
|
|
165
172
|
invalid: false,
|
|
166
173
|
evidenceHash: null,
|
|
@@ -177,6 +184,9 @@ function classifyFlow(evidence) {
|
|
|
177
184
|
}
|
|
178
185
|
const base = {
|
|
179
186
|
schema: verdict.schema,
|
|
187
|
+
claimLevel: verdict.claimLevel ?? null,
|
|
188
|
+
producerKind: verdict.producerKind ?? null,
|
|
189
|
+
sandboxAttested: verdict.sandboxAttested === true,
|
|
180
190
|
deprecated: verdict.kind === 'v1-deprecated' || verdict.kind === 'legacy',
|
|
181
191
|
invalid: false,
|
|
182
192
|
evidenceHash: null,
|
|
@@ -222,19 +232,33 @@ function classifyFlow(evidence) {
|
|
|
222
232
|
const passed = ids.filter(id => journeys[id].passes)
|
|
223
233
|
const failed = ids.filter(id => journeys[id].status === 'fail' && journeys[id].declared !== 'blocked')
|
|
224
234
|
const blocked = ids.filter(id => journeys[id].declared === 'blocked')
|
|
235
|
+
// Passing journeys are necessary but not sufficient: only sandbox-attested
|
|
236
|
+
// evidence may be reported as runtime. A self-attested/ci-attested document
|
|
237
|
+
// keeps its pass count but never upgrades to runtime. An honest FAIL/BLOCKED
|
|
238
|
+
// stays visible instead of being hidden behind the attestation level.
|
|
239
|
+
const runtimeEligible = base.sandboxAttested
|
|
225
240
|
const state = failed.length > 0
|
|
226
241
|
? 'failed'
|
|
227
|
-
:
|
|
228
|
-
? '
|
|
229
|
-
:
|
|
242
|
+
: blocked.length > 0
|
|
243
|
+
? 'blocked'
|
|
244
|
+
: runtimeEligible && passed.length === ids.length
|
|
245
|
+
? 'runtime'
|
|
246
|
+
: 'not-run'
|
|
230
247
|
const reasons = []
|
|
248
|
+
if (!runtimeEligible && failed.length === 0) {
|
|
249
|
+
reasons.push(`${base.claimLevel ?? 'non-sandbox'} evidence; runtime requires sandbox-attested`)
|
|
250
|
+
}
|
|
231
251
|
if (failed.length > 0) reasons.push(`failed journeys: ${failed.join(', ')}`)
|
|
232
252
|
if (blocked.length > 0) reasons.push(`blocked journeys (never a pass): ${blocked.join(', ')}`)
|
|
233
253
|
if (failed.length === 0 && passed.length !== ids.length) {
|
|
234
254
|
const rest = ids.filter(id => !passed.includes(id) && !blocked.includes(id))
|
|
235
255
|
if (rest.length > 0) reasons.push(`${passed.length}/8 journeys pass; ${rest.length} static/warn/not-run`)
|
|
236
256
|
}
|
|
237
|
-
if (passed.length === ids.length)
|
|
257
|
+
if (passed.length === ids.length) {
|
|
258
|
+
reasons.push(runtimeEligible
|
|
259
|
+
? 'all J1-J8 journey checks pass (exitCode 0, assertions held)'
|
|
260
|
+
: 'all J1-J8 journey checks pass but the evidence is not sandbox-attested; not runtime')
|
|
261
|
+
}
|
|
238
262
|
return {
|
|
239
263
|
...base,
|
|
240
264
|
state,
|
|
@@ -338,7 +362,11 @@ function nextActions(entry, flow, boost) {
|
|
|
338
362
|
if (entry.g4 && entry.g4.hasClient && entry.g4.ok === false) actions.push(`ENTRY: fix G4 — ${entry.g4.reason}`)
|
|
339
363
|
for (const warning of entry.warnings) actions.push(`ENTRY: consider ${warning.id} — ${warning.reason}`)
|
|
340
364
|
if (entry.state === 'recognized') actions.push('ENTRY: run with --host to replace static recognition with a real G0-G3 boot')
|
|
341
|
-
if (flow.state === 'not-run'
|
|
365
|
+
if (flow.state === 'not-run' && flow.claimLevel && flow.sandboxAttested !== true) {
|
|
366
|
+
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)`)
|
|
367
|
+
} else if (flow.state === 'not-run') {
|
|
368
|
+
actions.push('FLOW: record J1-J8 with node 40-checker/bin/dsh-flow-collect.mjs; do not claim FLOW until they run')
|
|
369
|
+
}
|
|
342
370
|
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
371
|
if (flow.state === 'invalid') actions.push('FLOW: flow evidence failed structural validation; fix the listed fields or recollect')
|
|
344
372
|
if (flow.state === 'failed') actions.push(`FLOW: fix failed journeys: ${Object.entries(flow.journeys).filter(([, row]) => row.status === 'fail').map(([id]) => id).join(', ')}`)
|
|
@@ -13,6 +13,12 @@
|
|
|
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.
|
|
16
22
|
*/
|
|
17
23
|
import crypto from 'node:crypto'
|
|
18
24
|
import fs from 'node:fs'
|
|
@@ -163,6 +169,18 @@ export function journeyCountsAsPass(journey) {
|
|
|
163
169
|
return declared === null || declared === 'pass'
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Runtime eligibility. A local collector run is `self-attested`
|
|
174
|
+
* (`ci-attested` with `--ci`): structurally valid, but it does not prove that
|
|
175
|
+
* commands ran, let alone in a sandbox. Only `claims.claimLevel ===
|
|
176
|
+
* 'sandbox-attested'` or the producer-side equivalent `producer.kind ===
|
|
177
|
+
* 'sandbox'` may be reported as FLOW runtime.
|
|
178
|
+
*/
|
|
179
|
+
export function isSandboxAttested(document) {
|
|
180
|
+
if (!isPlainObject(document)) return false
|
|
181
|
+
return document.claims?.claimLevel === 'sandbox-attested' || document.producer?.kind === 'sandbox'
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
function emptyVerdict(kind, schema, errors) {
|
|
167
185
|
return {
|
|
168
186
|
kind,
|
|
@@ -176,6 +194,9 @@ function emptyVerdict(kind, schema, errors) {
|
|
|
176
194
|
host: null,
|
|
177
195
|
generatedAt: null,
|
|
178
196
|
conclusion: null,
|
|
197
|
+
claimLevel: null,
|
|
198
|
+
producerKind: null,
|
|
199
|
+
sandboxAttested: false,
|
|
179
200
|
}
|
|
180
201
|
}
|
|
181
202
|
|
|
@@ -366,6 +387,9 @@ function validateV2(document) {
|
|
|
366
387
|
host: isPlainObject(document.host) ? document.host : null,
|
|
367
388
|
generatedAt: document.generatedAt ?? null,
|
|
368
389
|
conclusion: document.conclusion ?? null,
|
|
390
|
+
claimLevel: isPlainObject(document.claims) ? document.claims.claimLevel ?? null : null,
|
|
391
|
+
producerKind: isPlainObject(document.producer) ? document.producer.kind ?? null : null,
|
|
392
|
+
sandboxAttested: isSandboxAttested(document),
|
|
369
393
|
}
|
|
370
394
|
|
|
371
395
|
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'
|
|
@@ -53,8 +53,13 @@ function uniqueEvidence(...lists) {
|
|
|
53
53
|
*
|
|
54
54
|
* One entry is produced per (check.id, detail.item) pair with FAIL/WARN — or,
|
|
55
55
|
* for a FAIL/WARN check with no live details, one entry keyed by check.clause.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* `gate` is the check's final result (PASS/WARN/FAIL) after report adjustments
|
|
57
|
+
* such as the ENTRY static-claim downgrade; `severity` follows that gate, so a
|
|
58
|
+
* FAIL detail under a check downgraded to WARN yields a WARN advice entry. The
|
|
59
|
+
* detail's own reason stays in `reason` for diagnostics.
|
|
60
|
+
*
|
|
61
|
+
* Duplicate details are merged: evidence is unioned, occurrences counted, and
|
|
62
|
+
* the specific reasons are joined for diagnostics.
|
|
58
63
|
*
|
|
59
64
|
* @param {object} report dsh-conformance/1 report
|
|
60
65
|
* @param {object} rules flat key→rule map (also accepts a { rules } or { items } wrapper)
|
|
@@ -66,15 +71,15 @@ export function joinAdvice(report, rules, docsBase = DOCS_BASE) {
|
|
|
66
71
|
const version = versionOf(rules)
|
|
67
72
|
const merged = new Map()
|
|
68
73
|
|
|
69
|
-
const add = (check, key,
|
|
70
|
-
if (typeof key !== 'string' || key === '' || !LIVE.has(
|
|
74
|
+
const add = (check, key, gate, reason, evidence) => {
|
|
75
|
+
if (typeof key !== 'string' || key === '' || !LIVE.has(gate)) return false
|
|
71
76
|
const id = `${check.id}|${key}`
|
|
72
77
|
const current = merged.get(id)
|
|
73
78
|
if (!current) {
|
|
74
79
|
merged.set(id, {
|
|
75
80
|
check,
|
|
76
81
|
key,
|
|
77
|
-
|
|
82
|
+
gate,
|
|
78
83
|
reasons: typeof reason === 'string' && reason !== '' ? [reason] : [],
|
|
79
84
|
evidence: uniqueEvidence(evidence),
|
|
80
85
|
occurrences: 1,
|
|
@@ -83,7 +88,6 @@ export function joinAdvice(report, rules, docsBase = DOCS_BASE) {
|
|
|
83
88
|
}
|
|
84
89
|
current.occurrences += 1
|
|
85
90
|
if (typeof reason === 'string' && reason !== '' && !current.reasons.includes(reason)) current.reasons.push(reason)
|
|
86
|
-
if (SEVERITY_RANK[severity] > SEVERITY_RANK[current.severity]) current.severity = severity
|
|
87
91
|
current.evidence = uniqueEvidence(current.evidence, evidence)
|
|
88
92
|
return true
|
|
89
93
|
}
|
|
@@ -92,7 +96,7 @@ export function joinAdvice(report, rules, docsBase = DOCS_BASE) {
|
|
|
92
96
|
const liveDetails = (Array.isArray(check.details) ? check.details : [])
|
|
93
97
|
.filter(detail => detail && LIVE.has(detail.result))
|
|
94
98
|
let added = 0
|
|
95
|
-
for (const detail of liveDetails) if (add(check, detail.item,
|
|
99
|
+
for (const detail of liveDetails) if (add(check, detail.item, check.result, detail.reason, detail.evidence)) added += 1
|
|
96
100
|
// A runtime FAIL (DSC-04 / DSC-14) has no details; its clause is the only
|
|
97
101
|
// joinable key. Only use the fallback when no joinable detail was produced,
|
|
98
102
|
// so a specific detail always wins over the check-level summary.
|
|
@@ -106,7 +110,8 @@ export function joinAdvice(report, rules, docsBase = DOCS_BASE) {
|
|
|
106
110
|
const rule = table[entry.key] ?? null
|
|
107
111
|
const item = {
|
|
108
112
|
key: entry.key,
|
|
109
|
-
|
|
113
|
+
gate: entry.gate,
|
|
114
|
+
severity: entry.gate,
|
|
110
115
|
title: rule?.title ?? entry.key,
|
|
111
116
|
why: rule?.why ?? null,
|
|
112
117
|
fix: rule?.fix ?? null,
|
|
@@ -124,7 +129,7 @@ export function joinAdvice(report, rules, docsBase = DOCS_BASE) {
|
|
|
124
129
|
|
|
125
130
|
// FAIL before WARN, then key/source for deterministic output.
|
|
126
131
|
advice.sort((a, b) => (
|
|
127
|
-
(SEVERITY_RANK[b.
|
|
132
|
+
(SEVERITY_RANK[b.gate] ?? 0) - (SEVERITY_RANK[a.gate] ?? 0)
|
|
128
133
|
|| a.key.localeCompare(b.key)
|
|
129
134
|
|| a.source.localeCompare(b.source)
|
|
130
135
|
))
|
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
##
|
|
1
|
+
## 1.2.1 — 2026-11-06
|
|
2
2
|
|
|
3
|
+
- **V2 核验修复**:`level-audit` 不再把结构自洽的 self-attested/ci-attested `dsh-flow-evidence/2` 判成 FLOW `runtime`。只有 `claims.claimLevel=sandbox-attested` 或 `producer.kind=sandbox` 才允许 `runtime`;self/ci 的 `passed` 计数保留但 state 记 `not-run`,reason 为 `<claimLevel> evidence; runtime requires sandbox-attested`。`spec/flow-evidence.schema.json` 的 `claims`/`producer.kind` 补充了该判定说明。`advice[]` 新增 `gate`(该 check 的最终结果 PASS/WARN/FAIL),`severity` 跟随 `gate`:ENTRY static claim 把 DSC-02 降为 WARN 时,C-2.1 的 FAIL detail 仍保留在 `reason`,但 advice severity 为 WARN。测试 150 → 152。
|
|
3
4
|
- **FLOW 证据链**:新增 `spec/flow-evidence.schema.json`(`dsh-flow-evidence/2`)与 `40-checker/src/flow-evidence.mjs`。`level-audit --flow-evidence` 只接受 v2;v1(`dsh-flow-journey/1`、`dsh-flow-runtime/1`)与裸 `{Jx:{status}}` 标 `deprecated`,不计 pass。每个 J 须有 `checks[]` 且全部 `exitCode === 0`、断言成立、至少一条非 `static` 观测检查才计 pass(`warn`/`blocked`/`na`/`static` 不计 8/8)。v2 的 `chain.evidenceHash` 由 audit 重算,改动一个字节即 `invalid`。
|
|
4
5
|
- **collector**:新增 `40-checker/bin/dsh-flow-collect.mjs`(根入口 `bin/dsh-flow-collect.mjs`,并支持 `dsh-levels flow collect`)。只读副本 + 前后 `treeSha256` 一致、`npm pack --ignore-scripts` 的 integrity/tarballSha256、`git rev-parse` commit、临时 HOME/DSH_HOME、失败五要素提示(子检查/命令/exitCode/stderr 末 20 行/日志绝对路径 + 条款锚);前置缺失记 `blocked` 且 exit 0,真失败 exit 1。
|
|
5
6
|
- **安全**:新增 `40-checker/src/sandbox-env.mjs` 白名单 env。`src/cli.mjs` 的 runtime-child/bundle-smoke 子进程、`bundle-smoke.mjs`/`runtime-child.mjs` 进程内 boot、`m2-runner.mjs`、`journey-runtime.mjs`、`journey-config-observe.mjs`、`flow-sample-ci.mjs`、`host-farm.mjs`、`entry18-web.mjs` 不再把 `process.env` 透传给被检插件/宿主 boot。
|
|
6
7
|
- 测试 137 → 149(新增 `40-checker/test/flow-evidence.test.mjs`:v1 不再 pass、v2 伪造 checks/exitCode 不 pass、篡改链路 invalid、blocked 不判 fail、collector 产物可被 audit 读取、失败五要素提示、env 白名单)。
|
|
8
|
+
- 发布包不再包含 `40-checker/test`:该目录依赖仓库内 fixtures,装到用户环境后无法自测,`npm test` 会在 tarball 内失败。`npm test` 152/152。
|
|
7
9
|
|
|
8
10
|
## 1.2.0 — 2026-11-06
|
|
9
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-standards",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "DSH plugin standards and conformance checks: ENTRY/FLOW checks plus BOOST guidance. Non-official; no certification or ranking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "40-checker/src/index.mjs",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"40-checker/scripts",
|
|
21
21
|
"40-checker/samples",
|
|
22
22
|
"40-checker/fixtures",
|
|
23
|
-
"40-checker/test",
|
|
24
23
|
"40-checker/package.json",
|
|
25
24
|
"40-checker/README.md",
|
|
26
25
|
"spec",
|
package/spec/flow/README.md
CHANGED
|
@@ -27,3 +27,7 @@ uninstall, diagnostics, client/server states.
|
|
|
27
27
|
- `claims.claimLevel` is `self-attested` for a local collector run,
|
|
28
28
|
`ci-attested` with `--ci`. Neither is `verified`, a certification, or a
|
|
29
29
|
security audit; only a sandbox run may be described as runtime-attested.
|
|
30
|
+
- `level-audit.mjs` reports FLOW `runtime` only when `claims.claimLevel` is
|
|
31
|
+
`sandbox-attested` or `producer.kind` is `sandbox`. A `self-attested`/
|
|
32
|
+
`ci-attested` document keeps its `passed` count but is reported `not-run`;
|
|
33
|
+
structural validation cannot prove where the commands ran.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/PerryLink/dsh-standards/spec/flow-evidence.schema.json",
|
|
4
4
|
"title": "DSH FLOW evidence chain",
|
|
5
|
-
"description": "One collected FLOW result (J1-J8) bound to a package, a host, a timestamp, and a chain hash. Structural validation only: it does not prove the commands really ran. v1 (dsh-flow-journey/1, dsh-flow-runtime/1) and bare {Jx:{status}} documents are deprecated and never count as a pass. `level-audit.mjs --flow-evidence` accepts this document only.",
|
|
5
|
+
"description": "One collected FLOW result (J1-J8) bound to a package, a host, a timestamp, and a chain hash. Structural validation only: it does not prove the commands really ran. v1 (dsh-flow-journey/1, dsh-flow-runtime/1) and bare {Jx:{status}} documents are deprecated and never count as a pass. `level-audit.mjs --flow-evidence` accepts this document only, and reports `runtime` only when `claims.claimLevel` is `sandbox-attested` or `producer.kind` is `sandbox`; self-attested/ci-attested keeps its pass count but stays not-run.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"producer": {
|
|
31
31
|
"type": "object",
|
|
32
|
+
"description": "Who collected the evidence. `kind: sandbox` is the producer-side equivalent of `claims.claimLevel: sandbox-attested` and is one of the two ways a document may be reported as FLOW runtime.",
|
|
32
33
|
"additionalProperties": false,
|
|
33
34
|
"required": ["command", "at"],
|
|
34
35
|
"properties": {
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"format": "date-time"
|
|
43
44
|
},
|
|
44
45
|
"kind": {
|
|
45
|
-
"enum": ["local", "ci", "sandbox"]
|
|
46
|
+
"enum": ["local", "ci", "sandbox"],
|
|
47
|
+
"description": "`local`/`ci` are structural evidence only; `sandbox` may count toward FLOW runtime."
|
|
46
48
|
},
|
|
47
49
|
"runner": { "type": "string" },
|
|
48
50
|
"actor": { "type": "string" },
|
|
@@ -116,11 +118,13 @@
|
|
|
116
118
|
},
|
|
117
119
|
"claims": {
|
|
118
120
|
"type": "object",
|
|
121
|
+
"description": "Attestation level of this evidence. Passing journeys are not runtime by themselves: `level-audit` may report `runtime` only when `claimLevel` is `sandbox-attested` (or `producer.kind` is `sandbox`). `self-attested` (local collector) and `ci-attested` (`--ci`) keep their `passed` count but are reported `not-run`, because structural validation cannot prove the commands ran, let alone in a sandbox.",
|
|
119
122
|
"additionalProperties": false,
|
|
120
123
|
"required": ["claimLevel"],
|
|
121
124
|
"properties": {
|
|
122
125
|
"claimLevel": {
|
|
123
|
-
"enum": ["self-attested", "ci-attested", "sandbox-attested"]
|
|
126
|
+
"enum": ["self-attested", "ci-attested", "sandbox-attested"],
|
|
127
|
+
"description": "`sandbox-attested` is the only claim level that can yield FLOW runtime; `self-attested` and `ci-attested` are never runtime."
|
|
124
128
|
},
|
|
125
129
|
"notClaimed": {
|
|
126
130
|
"type": "array",
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { validateAttestation } from '../scripts/attestation-check.mjs'
|
|
7
|
-
|
|
8
|
-
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const ROOT = path.resolve(HERE, '..', '..')
|
|
10
|
-
const base = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'publish/attestations/DSH-ATT-2026-001.json'), 'utf8'))
|
|
11
|
-
|
|
12
|
-
test('attestation: the checked-in example passes', () => {
|
|
13
|
-
const result = validateAttestation(base())
|
|
14
|
-
assert.deepEqual(result.errors, [])
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('attestation: a level field is forbidden', () => {
|
|
18
|
-
const cert = base()
|
|
19
|
-
cert.level = 'entry'
|
|
20
|
-
const result = validateAttestation(cert)
|
|
21
|
-
assert.ok(result.errors.some((e) => /level field/.test(e)))
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('attestation: selfAssessed cannot be verified', () => {
|
|
25
|
-
const cert = base()
|
|
26
|
-
cert.claimLevel = 'verified'
|
|
27
|
-
const result = validateAttestation(cert)
|
|
28
|
-
assert.ok(result.errors.some((e) => /cannot claim verified/.test(e)))
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('attestation: an expired active attestation fails', () => {
|
|
32
|
-
const cert = base()
|
|
33
|
-
cert.status = 'active'
|
|
34
|
-
cert.issuedAt = '2024-01-01'
|
|
35
|
-
cert.expiresAt = '2025-01-01'
|
|
36
|
-
const result = validateAttestation(cert)
|
|
37
|
-
assert.ok(result.errors.some((e) => /cannot be active/.test(e)))
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('attestation: excluded scope is required', () => {
|
|
41
|
-
const cert = base()
|
|
42
|
-
cert.scope.excluded = []
|
|
43
|
-
const result = validateAttestation(cert)
|
|
44
|
-
assert.ok(result.errors.some((e) => /scope\.excluded/.test(e)))
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('attestation: evidence needs a URL or hash', () => {
|
|
48
|
-
const cert = base()
|
|
49
|
-
cert.evidence[0] = { id: 'A', result: 'PASS' }
|
|
50
|
-
const result = validateAttestation(cert)
|
|
51
|
-
assert.ok(result.errors.some((e) => /needs a url or sha256/.test(e)))
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('attestation: disclaimer must deny official/certification', () => {
|
|
55
|
-
const cert = base()
|
|
56
|
-
cert.disclaimer = 'A great plugin, approved.'
|
|
57
|
-
const result = validateAttestation(cert)
|
|
58
|
-
assert.ok(result.errors.some((e) => /non-official/.test(e)))
|
|
59
|
-
})
|