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.
@@ -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` 兜底)。`--no-advice` 可关闭;它不改变任何 check 的结果或 exit code。
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
- : passed.length === ids.length
228
- ? 'runtime'
229
- : blocked.length > 0 ? 'blocked' : 'not-run'
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) reasons.push('all J1-J8 journey checks pass (exitCode 0, assertions held)')
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') actions.push('FLOW: record J1-J8 with node 40-checker/bin/dsh-flow-collect.mjs; do not claim FLOW until they 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) {
@@ -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
- * Duplicate details are merged: severity takes the worst value, evidence is
57
- * unioned, and the specific reasons are joined for diagnostics.
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, severity, reason, evidence) => {
70
- if (typeof key !== 'string' || key === '' || !LIVE.has(severity)) return false
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
- severity,
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, detail.result, detail.reason, detail.evidence)) added += 1
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
- severity: entry.severity,
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.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0)
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.0",
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",
@@ -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
- })