dsh-standards 1.4.1 → 1.5.0
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/scripts/sign-evidence.mjs +169 -0
- package/40-checker/src/cli.mjs +394 -70
- package/40-checker/src/flow-evidence.mjs +21 -0
- package/40-checker/src/report.mjs +49 -3
- package/40-checker/src/runtime-child.mjs +68 -0
- package/40-checker/src/runtime.mjs +3 -1
- package/40-checker/src/sandbox-run.mjs +406 -35
- package/40-checker/src/signature.mjs +249 -0
- package/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/spec/schemas/conformance.schema.json +5 -0
|
@@ -108,7 +108,7 @@ function allowedNa(check, scope, profile, kind) {
|
|
|
108
108
|
return false
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime', advice = true, declarations = null }) {
|
|
111
|
+
export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime', advice = true, declarations = null, runtimeChecks = null }) {
|
|
112
112
|
const mandatory = mandatoryFor(scope, profile, staticOnly, entryClaim)
|
|
113
113
|
const adjustedChecks = scope === 'entry' ? checks.map(item => adjustEntryCheck(item, entryClaim)) : checks
|
|
114
114
|
const runtimeIds = new Set(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'])
|
|
@@ -123,7 +123,12 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
|
|
|
123
123
|
const missing = mandatory.filter(id => !byId.has(id))
|
|
124
124
|
const failed = selected.filter(item => item.result === 'FAIL')
|
|
125
125
|
const errored = selected.filter(item => item.result === 'ERROR')
|
|
126
|
-
|
|
126
|
+
// A static `partial` PASS is still a PASS for the mandatory set: it is
|
|
127
|
+
// recorded in buckets.partial / coverage.partial, and "what was not run" is
|
|
128
|
+
// its own signal. Only a check that did not run at all makes the report
|
|
129
|
+
// incomplete. Otherwise every bundle would be exit 2 forever, because the
|
|
130
|
+
// static subset can never be full without executing the package.
|
|
131
|
+
const notRun = selected.filter(item => !allowedNa(item, scope, profile, kind) && item.coverage === 'not-run')
|
|
127
132
|
const notRunIds = [...new Set([...missing, ...errored.map(item => item.id), ...notRun.map(item => item.id)])]
|
|
128
133
|
const warnings = selected.filter(item => item.result === 'WARN')
|
|
129
134
|
const pass = selected.filter(item => item.result === 'PASS')
|
|
@@ -134,7 +139,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
|
|
|
134
139
|
const evaluatedAll = effectiveChecks.filter(item => ['PASS', 'WARN', 'FAIL'].includes(item.result))
|
|
135
140
|
const naAll = effectiveChecks.filter(item => item.result === 'NA')
|
|
136
141
|
const errorAll = effectiveChecks.filter(item => item.result === 'ERROR')
|
|
137
|
-
const notRunAll = effectiveChecks.filter(item => item.result !== 'NA' &&
|
|
142
|
+
const notRunAll = effectiveChecks.filter(item => item.result !== 'NA' && item.coverage === 'not-run')
|
|
138
143
|
const notRunAllIds = [...new Set([...notRunAll.map(item => item.id), ...errorAll.map(item => item.id), ...missing])]
|
|
139
144
|
const entryBlockingIds = scope === 'entry' ? (entryClaim === 'static' ? ENTRY_BLOCKING : ENTRY_RUNTIME_BLOCKING) : null
|
|
140
145
|
const entryBlocking = entryBlockingIds ? summarizeBlocking(effectiveChecks, entryBlockingIds) : null
|
|
@@ -251,6 +256,11 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
|
|
|
251
256
|
reason: reasonParts.join('; '),
|
|
252
257
|
},
|
|
253
258
|
}
|
|
259
|
+
// Additive only: describes what actually executed (runtime/host), the
|
|
260
|
+
// elapsed time, and the isolation it used. It is a sibling of `checks` and
|
|
261
|
+
// never changes the verdict. Kept out of the 1.0.0 schema shape and only
|
|
262
|
+
// emitted when a runtime/host run was requested.
|
|
263
|
+
if (runtimeChecks) report.runtimeChecks = runtimeChecks
|
|
254
264
|
// Additive only: `advice` is a sibling of `checks`, never part of a check,
|
|
255
265
|
// and the verdict is computed above without reading it.
|
|
256
266
|
if (advice !== false) {
|
|
@@ -302,6 +312,41 @@ function renderBucket(lines, title, rows, byId) {
|
|
|
302
312
|
}
|
|
303
313
|
}
|
|
304
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Runtime provenance: what actually ran, for how long, and where. A failed
|
|
317
|
+
* run must say why instead of silently falling back to static rows.
|
|
318
|
+
*/
|
|
319
|
+
function renderRuntime(lines, runtime) {
|
|
320
|
+
if (!runtime) return
|
|
321
|
+
if (!runtime.executed) {
|
|
322
|
+
// Do not repeat six not-run rows that the partial/not-checked buckets
|
|
323
|
+
// already explain; one line is enough to say nothing executed.
|
|
324
|
+
lines.push(`Runtime checks (not executed): ${runtime.reason ?? runtime.failure ?? 'no runtime requested'}`)
|
|
325
|
+
lines.push('')
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
const modes = Array.isArray(runtime.modes) && runtime.modes.length > 0 ? runtime.modes.join('+') : 'none'
|
|
329
|
+
const elapsed = Number.isFinite(runtime.durationMs) ? `${runtime.durationMs} ms` : 'n/a'
|
|
330
|
+
const isolation = runtime.isolation?.isolatedHome ? 'temporary DSH_HOME' : 'un-isolated'
|
|
331
|
+
lines.push(`Runtime checks (${modes}, ${elapsed}, ${isolation}):`)
|
|
332
|
+
if (runtime.failure) lines.push(` failure: ${String(runtime.failure).replace(/\s+/g, ' ').slice(0, 200)}`)
|
|
333
|
+
const rows = Array.isArray(runtime.checks) ? runtime.checks : []
|
|
334
|
+
if (rows.length === 0) {
|
|
335
|
+
lines.push(' none')
|
|
336
|
+
} else {
|
|
337
|
+
for (const row of rows) {
|
|
338
|
+
const reason = String(row.reason ?? '').replace(/\s+/g, ' ').slice(0, 140)
|
|
339
|
+
lines.push(` ${(ICON[row.result] ?? row.result).padEnd(4)} ${String(row.id).padEnd(7)} ${String(row.coverage ?? '-').padEnd(8)} ${reason}`)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const entry = runtime.entry
|
|
343
|
+
if (entry) {
|
|
344
|
+
const reason = String(entry.reason ?? '').replace(/\s+/g, ' ').slice(0, 140)
|
|
345
|
+
lines.push(` ${(ICON[entry.result] ?? entry.result).padEnd(4)} ${String(entry.id).padEnd(7)} ${String(entry.coverage ?? '-').padEnd(8)} ${reason}`)
|
|
346
|
+
}
|
|
347
|
+
lines.push('')
|
|
348
|
+
}
|
|
349
|
+
|
|
305
350
|
export function formatText(report) {
|
|
306
351
|
const lines = []
|
|
307
352
|
const { subject, anchor = {}, conclusion, coverage } = report
|
|
@@ -318,6 +363,7 @@ export function formatText(report) {
|
|
|
318
363
|
lines.push('')
|
|
319
364
|
renderBucket(lines, 'Partly checked (static subset)', report.buckets?.partial ?? [], byId)
|
|
320
365
|
lines.push('')
|
|
366
|
+
renderRuntime(lines, report.runtimeChecks)
|
|
321
367
|
renderBucket(lines, 'Not checked (NA / not-run / ERROR)', report.buckets?.notChecked ?? [], byId)
|
|
322
368
|
lines.push('')
|
|
323
369
|
const passed = (report.checks ?? []).filter(item => item.result === 'PASS')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs L1-L3 in a disposable child process so a plugin cannot block or take over
|
|
3
|
+
// the checker. Input: JSON on stdin. Output: one marker line + JSON on fd 3.
|
|
4
|
+
//
|
|
5
|
+
// The child is *expected to be hostile*: a plugin may leave timers, sockets or
|
|
6
|
+
// workers referenced after `dispose()`. Waiting for the process to exit then
|
|
7
|
+
// looks like a hang even though every check finished, so the result is written
|
|
8
|
+
// first and the process is terminated in the write callback. The parent also
|
|
9
|
+
// resolves on the marker line, so a plugin that keeps the event loop alive
|
|
10
|
+
// cannot turn a completed run into "killed after timeout".
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import { runRuntimeChecks } from './runtime.mjs'
|
|
13
|
+
import { readSources } from './util.mjs'
|
|
14
|
+
import { scrubProcessEnv } from './sandbox-env.mjs'
|
|
15
|
+
|
|
16
|
+
export const MARKER = '__DSH_RUNTIME_CHECKS__'
|
|
17
|
+
|
|
18
|
+
const chunks = []
|
|
19
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
20
|
+
const input = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
21
|
+
|
|
22
|
+
// This process imports the inspected package; keep only allowlisted env keys,
|
|
23
|
+
// then apply the caller-supplied disposable HOME/DSH_HOME/TMPDIR. DSH_HOME is
|
|
24
|
+
// deliberately not allowlisted: an inherited real profile must never reach a
|
|
25
|
+
// plugin, while an explicit temp directory from the checker is trusted.
|
|
26
|
+
scrubProcessEnv()
|
|
27
|
+
const overrides = {
|
|
28
|
+
DSH_HOME: input.home,
|
|
29
|
+
HOME: input.home,
|
|
30
|
+
USERPROFILE: input.home,
|
|
31
|
+
TMPDIR: input.tmp,
|
|
32
|
+
TEMP: input.tmp,
|
|
33
|
+
TMP: input.tmp,
|
|
34
|
+
XDG_CONFIG_HOME: input.home ? `${input.home}/.config` : undefined,
|
|
35
|
+
XDG_DATA_HOME: input.home ? `${input.home}/.local/share` : undefined,
|
|
36
|
+
XDG_CACHE_HOME: input.home ? `${input.home}/.cache` : undefined,
|
|
37
|
+
}
|
|
38
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
39
|
+
if (typeof value === 'string' && value !== '') process.env[key] = value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const started = process.hrtime.bigint()
|
|
43
|
+
let payload
|
|
44
|
+
try {
|
|
45
|
+
let sources = []
|
|
46
|
+
try { sources = readSources(input.dir) } catch { sources = [] }
|
|
47
|
+
const checks = await runRuntimeChecks({ ...input, sources })
|
|
48
|
+
payload = { checks, durationMs: Number((process.hrtime.bigint() - started) / 1_000_000n), error: null }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
payload = {
|
|
51
|
+
checks: [],
|
|
52
|
+
durationMs: Number((process.hrtime.bigint() - started) / 1_000_000n),
|
|
53
|
+
error: String(error?.stack ?? error?.message ?? error).slice(0, 2000),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const line = `${MARKER}${JSON.stringify(payload)}\n`
|
|
58
|
+
// The plugin shares stdout, so results go to a dedicated pipe (fd 3) that a
|
|
59
|
+
// plugin printing logs cannot interleave with or truncate.
|
|
60
|
+
const writeResult = (callback) => {
|
|
61
|
+
try { fs.writeSync(3, line) } catch {
|
|
62
|
+
// A direct `node src/runtime-child.mjs` has no fd 3: fall back to stdout.
|
|
63
|
+
process.stdout.write(line, callback)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
callback()
|
|
67
|
+
}
|
|
68
|
+
writeResult(() => process.exit(0))
|
|
@@ -193,7 +193,9 @@ export async function runRuntimeChecks(input) {
|
|
|
193
193
|
const provideResult = declared.length === 0 ? 'NA' : worst(provideDetails.map(item => item.result))
|
|
194
194
|
const provideCheck = check({
|
|
195
195
|
id: 'DSC-05', clause: 'C-5', layer: 'L1', result: provideResult,
|
|
196
|
-
|
|
196
|
+
// NA is a real finding too ("nothing declared"), so keep its reason; an
|
|
197
|
+
// empty reason here used to make DSC-05 look unexplained in the report.
|
|
198
|
+
reason: provideDetails.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
197
199
|
details: provideDetails,
|
|
198
200
|
})
|
|
199
201
|
|