dsh-standards 0.0.3 → 1.0.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 +251 -0
- package/40-checker/bin/dsh-levels.mjs +15 -0
- package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
- package/40-checker/fixtures/bad/index.mjs +12 -0
- package/40-checker/fixtures/bad/package.json +15 -0
- package/40-checker/fixtures/bad-manifest/index.js +1 -0
- package/40-checker/fixtures/bad-manifest/package.json +5 -0
- package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle/package.json +15 -0
- package/40-checker/fixtures/bundle/server.mjs +7 -0
- package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-v0/package.json +21 -0
- package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
- package/40-checker/fixtures/client-missing/client.js +1 -0
- package/40-checker/fixtures/client-missing/index.js +1 -0
- package/40-checker/fixtures/client-missing/package.json +6 -0
- package/40-checker/fixtures/flow-config/index.js +9 -0
- package/40-checker/fixtures/flow-config/package.json +10 -0
- package/40-checker/fixtures/fs-write/index.mjs +7 -0
- package/40-checker/fixtures/fs-write/package.json +10 -0
- package/40-checker/fixtures/good/index.mjs +10 -0
- package/40-checker/fixtures/good/package.json +19 -0
- package/40-checker/fixtures/hang/index.mjs +5 -0
- package/40-checker/fixtures/hang/package.json +10 -0
- package/40-checker/fixtures/known-answer/expected.json +18 -0
- package/40-checker/fixtures/market-index-real.json +18 -0
- package/40-checker/fixtures/market-index.json +18 -0
- package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-patch/package.json +1 -0
- package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
- package/40-checker/fixtures/negative/root-missing/package.json +1 -0
- package/40-checker/fixtures/negative/workspace/package.json +1 -0
- package/40-checker/package.json +22 -0
- package/40-checker/samples/README.md +8 -0
- package/40-checker/samples/entry18.json +139 -0
- package/40-checker/samples/frame.json +485 -0
- package/40-checker/scripts/apply-overlay.mjs +59 -0
- package/40-checker/scripts/attestation-check.mjs +151 -0
- package/40-checker/scripts/bundle-smoke.mjs +126 -0
- package/40-checker/scripts/claim-check.mjs +145 -0
- package/40-checker/scripts/clause-map.mjs +94 -0
- package/40-checker/scripts/collect-fixtures.mjs +44 -0
- package/40-checker/scripts/dashboard.mjs +85 -0
- package/40-checker/scripts/dco-check.mjs +147 -0
- package/40-checker/scripts/dependabot-check.mjs +40 -0
- package/40-checker/scripts/doc-commands-check.mjs +92 -0
- package/40-checker/scripts/entry-scan.mjs +190 -0
- package/40-checker/scripts/entry-validate.mjs +291 -0
- package/40-checker/scripts/entry18-attribution.mjs +66 -0
- package/40-checker/scripts/entry18-ci.mjs +685 -0
- package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
- package/40-checker/scripts/entry18-web.mjs +307 -0
- package/40-checker/scripts/evidence-chain.mjs +100 -0
- package/40-checker/scripts/flow-index.mjs +82 -0
- package/40-checker/scripts/flow-sample-ci.mjs +529 -0
- package/40-checker/scripts/governance-check.mjs +210 -0
- package/40-checker/scripts/host-farm.mjs +165 -0
- package/40-checker/scripts/journey-config-observe.mjs +109 -0
- package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
- package/40-checker/scripts/journey-runtime.mjs +178 -0
- package/40-checker/scripts/journey.mjs +76 -0
- package/40-checker/scripts/label-drift.mjs +74 -0
- package/40-checker/scripts/label-sync.mjs +87 -0
- package/40-checker/scripts/label-verify.mjs +90 -0
- package/40-checker/scripts/landlock-exec.c +101 -0
- package/40-checker/scripts/level-audit.mjs +336 -0
- package/40-checker/scripts/m1-sidecar.mjs +126 -0
- package/40-checker/scripts/m2-runner.mjs +213 -0
- package/40-checker/scripts/round-robin.mjs +88 -0
- package/40-checker/scripts/sample-frame.mjs +72 -0
- package/40-checker/scripts/sample20-ci.mjs +25 -0
- package/40-checker/scripts/sample60-ci.mjs +25 -0
- package/40-checker/scripts/stale-check.mjs +59 -0
- package/40-checker/scripts/surveillance.mjs +88 -0
- package/40-checker/scripts/web-probe.mjs +504 -0
- package/40-checker/src/cli.mjs +300 -0
- package/40-checker/src/client-shape.mjs +37 -0
- package/40-checker/src/declarations.mjs +242 -0
- package/40-checker/src/index.mjs +8 -0
- package/40-checker/src/install-spec.mjs +67 -0
- package/40-checker/src/label.mjs +130 -0
- package/40-checker/src/manifest.mjs +381 -0
- package/40-checker/src/permissions.mjs +60 -0
- package/40-checker/src/report.mjs +204 -0
- package/40-checker/src/result.mjs +60 -0
- package/40-checker/src/runtime.mjs +299 -0
- package/40-checker/src/semver.mjs +194 -0
- package/40-checker/src/util.mjs +148 -0
- package/40-checker/test/attestation.test.mjs +59 -0
- package/40-checker/test/checks.test.mjs +172 -0
- package/40-checker/test/claim.test.mjs +28 -0
- package/40-checker/test/client-shape.test.mjs +35 -0
- package/40-checker/test/dco.test.mjs +51 -0
- package/40-checker/test/entry-validate.test.mjs +172 -0
- package/40-checker/test/governance.test.mjs +76 -0
- package/40-checker/test/label.test.mjs +97 -0
- package/40-checker/test/level-audit.test.mjs +99 -0
- package/40-checker/test/levels.test.mjs +31 -0
- package/40-checker/test/ops.test.mjs +117 -0
- package/40-checker/test/permissions.test.mjs +40 -0
- package/40-checker/test/semver.test.mjs +32 -0
- package/40-checker/test/verify.test.mjs +57 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +28 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-CC-BY +23 -0
- package/NOTICE +14 -0
- package/NOTICE.md +11 -0
- package/README.md +146 -3
- package/bin/dsh-levels.mjs +4 -0
- package/bin/dsh-standards.mjs +19 -0
- package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
- package/docs/DEVELOPER-JOURNEY.md +40 -0
- package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
- package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
- package/docs/NAMING-FALLBACK.md +11 -0
- package/docs/fork-policy.md +12 -0
- package/docs/full-test-plan-2026-11-02.md +207 -0
- package/docs/incident.md +37 -0
- package/docs/onboarding.md +27 -0
- package/docs/privacy.md +12 -0
- package/docs/release.md +16 -0
- package/docs/scope-security.md +62 -0
- package/docs/status-taxonomy.md +54 -0
- package/docs/sunset.md +44 -0
- package/docs/translating.md +12 -0
- package/package.json +58 -12
- package/spec/README.md +27 -0
- package/spec/boost/README.md +8 -0
- package/spec/clauses.json +253 -0
- package/spec/entry/README.md +9 -0
- package/spec/flow/README.md +8 -0
- package/spec/levels.json +371 -0
- package/spec/levels.schema.json +45 -0
- package/spec/reference-suite.json +35 -0
- package/spec/schemas/attestation.schema.json +284 -0
- package/spec/schemas/evidence.schema.json +331 -0
- package/spec/schemas/report.schema.json +64 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* level-audit.mjs — one-command self-check against the three DSH adoption levels.
|
|
4
|
+
*
|
|
5
|
+
* What it does:
|
|
6
|
+
* ENTRY runs the existing ENTRY checker (static by default; real G0-G3 boot
|
|
7
|
+
* when --host is supplied) and maps its conclusion to an evidence state.
|
|
8
|
+
* FLOW reads an optional J1-J8 evidence file; with no file it reports the
|
|
9
|
+
* checklist as `not-run` and never invents a pass.
|
|
10
|
+
* BOOST reads an optional LEV evidence file; with no file it reports
|
|
11
|
+
* `not-measured`. It does not run benchmarks for you.
|
|
12
|
+
*
|
|
13
|
+
* The machine source of truth is `spec/levels.json`; this tool only reads it.
|
|
14
|
+
* Zero dependencies: it shells out to the checker already in this repo.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node 40-checker/scripts/level-audit.mjs <plugin-dir>
|
|
18
|
+
* node 40-checker/scripts/level-audit.mjs <bundle-dir> --host <app-boot/lib/index.js> \
|
|
19
|
+
* --base <dsh-base/cordis.patch.yml> --bare <farm> [--timeout 150000]
|
|
20
|
+
* node 40-checker/scripts/level-audit.mjs <dir> --flow-evidence flow.json \
|
|
21
|
+
* --boost-evidence boost.json --format json --out audit.json
|
|
22
|
+
*
|
|
23
|
+
* Exit codes: 0 ENTRY recognized/runtime; 2 ENTRY failed; 3 audit could not run.
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import { spawnSync } from 'node:child_process'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
|
|
30
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
const STD = path.resolve(HERE, '..', '..')
|
|
32
|
+
const LEVELS_PATH = path.join(STD, 'spec', 'levels.json')
|
|
33
|
+
const CHECKER = path.join(STD, '40-checker', 'bin', 'dsh-plugin-verify.mjs')
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
console.log(`level-audit — DSH-ENTRY / FLOW / BOOST self-check
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
node 40-checker/scripts/level-audit.mjs <plugin-dir> [options]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--host <file> app-boot lib/index.js; enables real ENTRY G0-G3
|
|
43
|
+
--base <file> dsh-base cordis.patch.yml
|
|
44
|
+
--bare <dir> bare-module farm for the bundle smoke
|
|
45
|
+
--no-runtime force static ENTRY even when --host is given
|
|
46
|
+
--flow-evidence <file> JSON: { "J1": { "status": "pass|fail|not-run", "evidence": "..." }, ... }
|
|
47
|
+
--boost-evidence <file> JSON: { "LEV-3": { "status": "measured|not-measured|failed", "evidenceCard": { ... } }, ... }
|
|
48
|
+
measured requires a valid evidence card; a status-only row is downgraded to not-measured
|
|
49
|
+
--timeout <ms> checker timeout, default 150000
|
|
50
|
+
--format text|json default text
|
|
51
|
+
--out <file> write the JSON result
|
|
52
|
+
--help show this help
|
|
53
|
+
|
|
54
|
+
The tool never upgrades a skip to a pass. FLOW and BOOST without evidence files
|
|
55
|
+
are reported as not-run / not-measured.`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const options = {
|
|
60
|
+
dir: null, host: null, base: null, bare: null, runtime: true,
|
|
61
|
+
flowEvidence: null, boostEvidence: null, timeout: 150_000,
|
|
62
|
+
format: 'text', out: null,
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
65
|
+
const arg = argv[i]
|
|
66
|
+
const next = () => {
|
|
67
|
+
const value = argv[i + 1]
|
|
68
|
+
if (value === undefined) throw new Error(`${arg} needs a value`)
|
|
69
|
+
i += 1
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
if (arg === '--host') options.host = next()
|
|
73
|
+
else if (arg === '--base') options.base = next()
|
|
74
|
+
else if (arg === '--bare') options.bare = next()
|
|
75
|
+
else if (arg === '--no-runtime') options.runtime = false
|
|
76
|
+
else if (arg === '--flow-evidence') options.flowEvidence = next()
|
|
77
|
+
else if (arg === '--boost-evidence') options.boostEvidence = next()
|
|
78
|
+
else if (arg === '--timeout') options.timeout = Number(next())
|
|
79
|
+
else if (arg === '--format') options.format = next()
|
|
80
|
+
else if (arg === '--out') options.out = next()
|
|
81
|
+
else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
|
|
82
|
+
else if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`)
|
|
83
|
+
else if (options.dir === null) options.dir = arg
|
|
84
|
+
else throw new Error(`unexpected argument: ${arg}`)
|
|
85
|
+
}
|
|
86
|
+
if (!options.dir) throw new Error('a plugin directory is required')
|
|
87
|
+
if (!['text', 'json'].includes(options.format)) throw new Error('--format must be text or json')
|
|
88
|
+
if (!Number.isFinite(options.timeout) || options.timeout < 1000) throw new Error('--timeout must be >= 1000')
|
|
89
|
+
options.dir = path.resolve(options.dir)
|
|
90
|
+
if (!fs.existsSync(path.join(options.dir, 'package.json'))) throw new Error(`not a package directory: ${options.dir}`)
|
|
91
|
+
return options
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readJson(file, label) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new Error(`${label} is not valid JSON: ${error.message}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runChecker(options) {
|
|
103
|
+
const args = [CHECKER, options.dir, '--scope', 'entry', '--format', 'json']
|
|
104
|
+
const canRun = options.runtime && options.host !== null
|
|
105
|
+
args.push('--entry-claim', canRun ? 'runtime' : 'static')
|
|
106
|
+
if (canRun) args.push('--host', options.host)
|
|
107
|
+
if (options.base) args.push('--base', options.base)
|
|
108
|
+
if (options.bare) args.push('--bare', options.bare)
|
|
109
|
+
const run = spawnSync(process.execPath, args, {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
timeout: options.timeout,
|
|
112
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
113
|
+
})
|
|
114
|
+
let report = null
|
|
115
|
+
try {
|
|
116
|
+
report = JSON.parse(run.stdout)
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error(`checker did not emit JSON (exit ${run.status}): ${(run.stderr || '').slice(-500)}`)
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
invoked: canRun ? 'runtime' : 'static',
|
|
122
|
+
exit: run.status,
|
|
123
|
+
report,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function checkById(report, id) {
|
|
128
|
+
return (report.checks ?? []).find(check => check.id === id)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function classifyEntry(report, invoked) {
|
|
132
|
+
const conclusion = report.conclusion ?? {}
|
|
133
|
+
const status = conclusion.status
|
|
134
|
+
const failed = (report.checks ?? []).filter(check => check.result === 'FAIL')
|
|
135
|
+
const warnings = (report.checks ?? []).filter(check => check.result === 'WARN')
|
|
136
|
+
const dsc14 = checkById(report, 'DSC-14')
|
|
137
|
+
let state
|
|
138
|
+
if (status === 'fail' || failed.length > 0) state = 'failed'
|
|
139
|
+
else if (status === 'pass' || status === 'warn') state = invoked === 'runtime' && dsc14?.result === 'PASS' ? 'runtime' : 'recognized'
|
|
140
|
+
else state = 'not-run'
|
|
141
|
+
return {
|
|
142
|
+
state,
|
|
143
|
+
claim: invoked,
|
|
144
|
+
checkerStatus: status ?? 'unknown',
|
|
145
|
+
checkerLevel: conclusion.level ?? null,
|
|
146
|
+
failed: failed.map(check => ({ id: check.id, reason: check.reason })),
|
|
147
|
+
warnings: warnings.map(check => ({ id: check.id, reason: check.reason })),
|
|
148
|
+
runtimeEvidence: dsc14 ? { result: dsc14.result, reason: dsc14.reason } : null,
|
|
149
|
+
g4: report.g4 ?? null,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function classifyFlow(evidence) {
|
|
154
|
+
const ids = ['J1', 'J2', 'J3', 'J4', 'J5', 'J6', 'J7', 'J8']
|
|
155
|
+
if (!evidence) {
|
|
156
|
+
return {
|
|
157
|
+
state: 'not-run',
|
|
158
|
+
reason: 'no --flow-evidence file; run the J1-J8 journeys and record pass/fail/not-run',
|
|
159
|
+
journeys: Object.fromEntries(ids.map(id => [id, { status: 'not-run' }])),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const journeys = {}
|
|
163
|
+
for (const id of ids) {
|
|
164
|
+
const row = evidence[id] ?? evidence[id.toLowerCase()]
|
|
165
|
+
const status = row?.status
|
|
166
|
+
journeys[id] = {
|
|
167
|
+
status: ['pass', 'fail', 'not-run', 'manual'].includes(status) ? status : 'not-run',
|
|
168
|
+
evidence: row?.evidence ?? null,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const failed = ids.filter(id => journeys[id].status === 'fail')
|
|
172
|
+
const passed = ids.filter(id => journeys[id].status === 'pass')
|
|
173
|
+
return {
|
|
174
|
+
state: failed.length > 0 ? 'failed' : passed.length === ids.length ? 'runtime' : 'not-run',
|
|
175
|
+
reason: failed.length > 0
|
|
176
|
+
? `failed journeys: ${failed.join(', ')}`
|
|
177
|
+
: passed.length === ids.length
|
|
178
|
+
? 'all J1-J8 have pass evidence'
|
|
179
|
+
: `${passed.length}/8 journeys have pass evidence`,
|
|
180
|
+
journeys,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const BOOST_CARD_FIELDS = [
|
|
185
|
+
'subject', 'host', 'method', 'scope', 'command', 'result', 'negativeControl',
|
|
186
|
+
'generatedAt', 'expiresAt', 'recordHash', 'claimLevel', 'issuer', 'selfAssessed',
|
|
187
|
+
]
|
|
188
|
+
const BOOST_CLAIM_LEVELS = new Set(['self-reported', 'self-assessed', 'host-reviewed', 'independent', 'reference'])
|
|
189
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
|
190
|
+
const HASH_RE = /^sha256:[0-9a-f]{64}$/
|
|
191
|
+
|
|
192
|
+
function validateEvidenceCard(card) {
|
|
193
|
+
if (!card || typeof card !== 'object' || Array.isArray(card)) {
|
|
194
|
+
return { valid: false, missing: ['evidenceCard'], reason: 'missing evidence card' }
|
|
195
|
+
}
|
|
196
|
+
const missing = []
|
|
197
|
+
for (const field of BOOST_CARD_FIELDS) {
|
|
198
|
+
if (card[field] === undefined || card[field] === null || card[field] === '') missing.push(field)
|
|
199
|
+
}
|
|
200
|
+
if (!DATE_RE.test(String(card.generatedAt ?? ''))) missing.push('generatedAt(date)')
|
|
201
|
+
if (!DATE_RE.test(String(card.expiresAt ?? ''))) missing.push('expiresAt(date)')
|
|
202
|
+
if (!HASH_RE.test(String(card.recordHash ?? ''))) missing.push('recordHash(sha256)')
|
|
203
|
+
if (!BOOST_CLAIM_LEVELS.has(String(card.claimLevel ?? ''))) missing.push('claimLevel')
|
|
204
|
+
if (typeof card.selfAssessed !== 'boolean') missing.push('selfAssessed(boolean)')
|
|
205
|
+
if (missing.length > 0) {
|
|
206
|
+
return { valid: false, missing, reason: `evidence card missing/invalid: ${missing.join(', ')}` }
|
|
207
|
+
}
|
|
208
|
+
const generated = Date.parse(`${card.generatedAt}T00:00:00Z`)
|
|
209
|
+
const expires = Date.parse(`${card.expiresAt}T00:00:00Z`)
|
|
210
|
+
if (!Number.isFinite(generated) || !Number.isFinite(expires) || expires < generated) {
|
|
211
|
+
return { valid: false, missing: ['validity'], reason: 'evidence card has invalid generatedAt/expiresAt' }
|
|
212
|
+
}
|
|
213
|
+
return { valid: true, missing: [], reason: null }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function classifyBoost(evidence) {
|
|
217
|
+
const ids = ['LEV-1', 'LEV-2', 'LEV-3', 'LEV-4', 'LEV-5', 'LEV-6', 'LEV-7', 'LEV-8']
|
|
218
|
+
if (!evidence) {
|
|
219
|
+
return {
|
|
220
|
+
state: 'not-measured',
|
|
221
|
+
reason: 'no --boost-evidence file; benchmarks/evidence cards are not run by this tool',
|
|
222
|
+
practices: Object.fromEntries(ids.map(id => [id, { status: 'not-measured' }])),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const practices = {}
|
|
226
|
+
for (const id of ids) {
|
|
227
|
+
const row = evidence[id] ?? evidence[id.toLowerCase()]
|
|
228
|
+
const declared = row?.status
|
|
229
|
+
const card = row?.evidenceCard ?? (row?.evidence && typeof row.evidence === 'object' ? row.evidence : null)
|
|
230
|
+
const cardCheck = validateEvidenceCard(card)
|
|
231
|
+
let status = 'not-measured'
|
|
232
|
+
if (declared === 'failed') status = 'failed'
|
|
233
|
+
else if (declared === 'measured' && cardCheck.valid) status = 'measured'
|
|
234
|
+
practices[id] = {
|
|
235
|
+
status,
|
|
236
|
+
declared: ['measured', 'not-measured', 'failed'].includes(declared) ? declared : 'not-measured',
|
|
237
|
+
evidence: typeof row?.evidence === 'string' ? row.evidence : null,
|
|
238
|
+
evidenceCard: card,
|
|
239
|
+
card: cardCheck,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const failed = ids.filter(id => practices[id].status === 'failed')
|
|
243
|
+
const measured = ids.filter(id => practices[id].status === 'measured')
|
|
244
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
245
|
+
const stale = measured.filter(id => String(practices[id].evidenceCard.expiresAt) < today)
|
|
246
|
+
const invalid = ids.filter(id => practices[id].declared === 'measured' && !practices[id].card.valid)
|
|
247
|
+
let state = 'not-measured'
|
|
248
|
+
if (failed.length > 0) state = 'failed'
|
|
249
|
+
else if (stale.length > 0) state = 'stale'
|
|
250
|
+
else if (measured.length === ids.length) state = 'measured'
|
|
251
|
+
const reasons = []
|
|
252
|
+
if (failed.length > 0) reasons.push(`failed practices: ${failed.join(', ')}`)
|
|
253
|
+
if (stale.length > 0) reasons.push(`stale evidence cards: ${stale.join(', ')}`)
|
|
254
|
+
if (invalid.length > 0) reasons.push(`measured without a valid evidence card: ${invalid.join(', ')}`)
|
|
255
|
+
if (failed.length === 0 && stale.length === 0 && measured.length !== ids.length) {
|
|
256
|
+
reasons.push(`${measured.length}/8 practices have valid measured evidence cards`)
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
state,
|
|
260
|
+
reason: reasons.join('; '),
|
|
261
|
+
practices,
|
|
262
|
+
validCards: measured.length,
|
|
263
|
+
invalidCards: invalid.length,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function nextActions(entry, flow, boost) {
|
|
268
|
+
const actions = []
|
|
269
|
+
for (const failure of entry.failed) actions.push(`ENTRY: fix ${failure.id} — ${failure.reason}`)
|
|
270
|
+
if (entry.g4 && entry.g4.hasClient && entry.g4.ok === false) actions.push(`ENTRY: fix G4 — ${entry.g4.reason}`)
|
|
271
|
+
for (const warning of entry.warnings) actions.push(`ENTRY: consider ${warning.id} — ${warning.reason}`)
|
|
272
|
+
if (entry.state === 'recognized') actions.push('ENTRY: run with --host to replace static recognition with a real G0-G3 boot')
|
|
273
|
+
if (flow.state === 'not-run') actions.push('FLOW: record J1-J8 in a flow-evidence JSON; do not claim FLOW until they run')
|
|
274
|
+
if (boost.state === 'not-measured') actions.push('BOOST: attach LEV evidence cards (subject/host/method/scope/command/result/negativeControl/generatedAt/expiresAt/recordHash/claimLevel/issuer/selfAssessed); a status alone is not measured')
|
|
275
|
+
if (boost.state === 'stale') actions.push('BOOST: refresh stale evidence cards before claiming measured')
|
|
276
|
+
return actions
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function render(result) {
|
|
280
|
+
const lines = [
|
|
281
|
+
`DSH level audit — ${result.target.dir}`,
|
|
282
|
+
` ENTRY : ${result.levels.entry.state.padEnd(12)} (${result.levels.entry.checkerLevel ?? 'no level'})`,
|
|
283
|
+
` FLOW : ${result.levels.flow.state.padEnd(12)} ${result.levels.flow.reason}`,
|
|
284
|
+
` BOOST : ${result.levels.boost.state.padEnd(12)} ${result.levels.boost.reason}`,
|
|
285
|
+
]
|
|
286
|
+
if (result.nextActions.length > 0) {
|
|
287
|
+
lines.push('Next:')
|
|
288
|
+
for (const action of result.nextActions) lines.push(` - ${action}`)
|
|
289
|
+
}
|
|
290
|
+
lines.push('Claims: not official, not a certification, not a security audit; static is not runtime.')
|
|
291
|
+
return lines.join('\n')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function main() {
|
|
295
|
+
const options = parseArgs(process.argv.slice(2))
|
|
296
|
+
const levels = JSON.parse(fs.readFileSync(LEVELS_PATH, 'utf8'))
|
|
297
|
+
const flowEvidence = options.flowEvidence ? readJson(options.flowEvidence, '--flow-evidence') : null
|
|
298
|
+
const boostEvidence = options.boostEvidence ? readJson(options.boostEvidence, '--boost-evidence') : null
|
|
299
|
+
|
|
300
|
+
const checker = runChecker(options)
|
|
301
|
+
const entry = classifyEntry(checker.report, checker.invoked)
|
|
302
|
+
const flow = classifyFlow(flowEvidence)
|
|
303
|
+
const boost = classifyBoost(boostEvidence)
|
|
304
|
+
|
|
305
|
+
const result = {
|
|
306
|
+
schema: 'dsh-level-audit/1',
|
|
307
|
+
generatedAt: new Date().toISOString(),
|
|
308
|
+
target: { dir: options.dir },
|
|
309
|
+
levelsSpec: { schema: levels.schema, version: levels.version, status: levels.status },
|
|
310
|
+
levels: { entry, flow, boost },
|
|
311
|
+
nextActions: nextActions(entry, flow, boost),
|
|
312
|
+
notClaimed: ['official standard', 'certification', 'security audit', 'ranking', 'production baseline'],
|
|
313
|
+
checker: {
|
|
314
|
+
invoked: checker.invoked,
|
|
315
|
+
exit: checker.exit,
|
|
316
|
+
schema: checker.report.schema,
|
|
317
|
+
anchor: checker.report.anchor,
|
|
318
|
+
coverage: checker.report.coverage,
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const output = options.format === 'json' ? JSON.stringify(result, null, 2) : render(result)
|
|
323
|
+
if (options.out) {
|
|
324
|
+
fs.mkdirSync(path.dirname(path.resolve(options.out)), { recursive: true })
|
|
325
|
+
fs.writeFileSync(options.out, `${output}\n`)
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(`${output}\n`)
|
|
328
|
+
process.exit(entry.state === 'failed' ? 2 : entry.state === 'not-run' ? 3 : 0)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
main()
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error(`level-audit: ${error.message}`)
|
|
335
|
+
process.exit(3)
|
|
336
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate a levels.json sidecar for a format-1 market index.
|
|
4
|
+
*
|
|
5
|
+
* Tier A (index-only): every entry gets index-field validation
|
|
6
|
+
* (exact SemVer, npm/git source pinned, engines.dsh presence).
|
|
7
|
+
* No package is downloaded or executed; level is null.
|
|
8
|
+
* Tier B (static-only): optional pilot JSON files from
|
|
9
|
+
* `dsh-plugin-verify --static-only` are merged in by name@version.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/m1-sidecar.mjs \
|
|
13
|
+
* --index /tmp/dsh-catalog/index.json \
|
|
14
|
+
* --pilot results/m1 \
|
|
15
|
+
* --out results/m1/levels.json
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
const EXACT = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
21
|
+
const COMMIT = /^[0-9a-f]{40}$/
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const options = { index: null, out: null, pilot: null, indexUrl: 'https://awesome-dsh-plugin.com/index.json' }
|
|
25
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
+
const arg = argv[i]
|
|
27
|
+
if (arg === '--index') options.index = argv[++i]
|
|
28
|
+
else if (arg === '--out') options.out = argv[++i]
|
|
29
|
+
else if (arg === '--pilot') options.pilot = argv[++i]
|
|
30
|
+
else if (arg === '--index-url') options.indexUrl = argv[++i]
|
|
31
|
+
else if (arg === '--help' || arg === '-h') { console.log('usage: node scripts/m1-sidecar.mjs --index <format-1.json> [--pilot <dir>] --out <levels.json>'); process.exit(0) }
|
|
32
|
+
else throw new Error(`unknown option: ${arg}`)
|
|
33
|
+
}
|
|
34
|
+
if (!options.index) throw new Error('--index is required')
|
|
35
|
+
if (!options.out) throw new Error('--out is required')
|
|
36
|
+
return options
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readPilots(directory) {
|
|
40
|
+
const map = new Map()
|
|
41
|
+
if (!directory || !fs.existsSync(directory)) return map
|
|
42
|
+
for (const entry of fs.readdirSync(directory)) {
|
|
43
|
+
if (!entry.endsWith('.json')) continue
|
|
44
|
+
const file = path.join(directory, entry)
|
|
45
|
+
let report
|
|
46
|
+
try { report = JSON.parse(fs.readFileSync(file, 'utf8')) } catch { continue }
|
|
47
|
+
if (report?.schema !== 'dsh-conformance/1' || !report.subject?.name || !report.subject?.version) continue
|
|
48
|
+
map.set(`${report.subject.name}@${report.subject.version}`, { file, report })
|
|
49
|
+
}
|
|
50
|
+
return map
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pilotLevel(report, file) {
|
|
54
|
+
const status = { pass: 'static-only', warn: 'static-only-warn', fail: 'failed', incomplete: 'inconclusive' }[report.conclusion?.status] ?? 'inconclusive'
|
|
55
|
+
return {
|
|
56
|
+
standard: 'DS-C',
|
|
57
|
+
standardVersion: report.standard?.version ?? '1.0.0',
|
|
58
|
+
profile: 'listing',
|
|
59
|
+
status,
|
|
60
|
+
coverage: report.coverage,
|
|
61
|
+
anchor: report.anchor,
|
|
62
|
+
checkedAt: new Date().toISOString().slice(0, 10),
|
|
63
|
+
expiresAt: new Date(Date.now() + 90 * 86400_000).toISOString().slice(0, 10),
|
|
64
|
+
evidenceUrl: `file://${path.resolve(file)}`,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function main() {
|
|
69
|
+
const options = parseArgs(process.argv.slice(2))
|
|
70
|
+
const index = JSON.parse(fs.readFileSync(options.index, 'utf8'))
|
|
71
|
+
if (index.format !== 1 || !Array.isArray(index.plugins)) throw new Error('not a format-1 index')
|
|
72
|
+
const pilots = readPilots(options.pilot)
|
|
73
|
+
const levels = {}
|
|
74
|
+
const summary = { total: 0, indexOnly: 0, staticOnly: 0, staticOnlyWarn: 0, failed: 0, inconclusive: 0, withEnginesDsh: 0, npm: 0, git: 0 }
|
|
75
|
+
for (const plugin of [...index.plugins].sort((a, b) => String(a.name).localeCompare(String(b.name)))) {
|
|
76
|
+
const versions = Array.isArray(plugin.versions) ? [...plugin.versions].sort((a, b) => String(a.version).localeCompare(String(b.version))) : []
|
|
77
|
+
for (const version of versions) {
|
|
78
|
+
summary.total += 1
|
|
79
|
+
const source = version.source ?? {}
|
|
80
|
+
const sourcePinned = source.type === 'npm' || (source.type === 'git' && COMMIT.test(source.commit ?? ''))
|
|
81
|
+
const enginesDsh = typeof version.engines?.dsh === 'string' && version.engines.dsh !== ''
|
|
82
|
+
if (enginesDsh) summary.withEnginesDsh += 1
|
|
83
|
+
if (source.type === 'npm') summary.npm += 1
|
|
84
|
+
if (source.type === 'git') summary.git += 1
|
|
85
|
+
const key = `${plugin.name}@${version.version}`
|
|
86
|
+
const pilot = pilots.get(key)
|
|
87
|
+
const base = {
|
|
88
|
+
plugin: plugin.name,
|
|
89
|
+
version: version.version,
|
|
90
|
+
status: 'index-only',
|
|
91
|
+
index: {
|
|
92
|
+
versionExact: EXACT.test(version.version ?? ''),
|
|
93
|
+
sourceType: source.type ?? null,
|
|
94
|
+
sourcePinned,
|
|
95
|
+
enginesDsh,
|
|
96
|
+
hasUpstream: plugin.upstream !== undefined,
|
|
97
|
+
},
|
|
98
|
+
reason: 'package tarball not fetched; only index fields were checked',
|
|
99
|
+
anchor: { indexUrl: options.indexUrl, parser: 'm1-sidecar/1', checkedAt: new Date().toISOString().slice(0, 10) },
|
|
100
|
+
}
|
|
101
|
+
if (pilot) {
|
|
102
|
+
const level = pilotLevel(pilot.report, pilot.file)
|
|
103
|
+
levels[key] = { ...base, status: level.status, reason: '', level }
|
|
104
|
+
if (level.status === 'static-only-warn') summary.staticOnlyWarn += 1
|
|
105
|
+
else if (level.status === 'failed') summary.failed += 1
|
|
106
|
+
else if (level.status === 'inconclusive') summary.inconclusive += 1
|
|
107
|
+
else summary.staticOnly += 1
|
|
108
|
+
} else {
|
|
109
|
+
levels[key] = base
|
|
110
|
+
summary.indexOnly += 1
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const output = {
|
|
115
|
+
schema: 'dsh-levels/1',
|
|
116
|
+
indexUrl: options.indexUrl,
|
|
117
|
+
generatedAt: new Date().toISOString(),
|
|
118
|
+
summary,
|
|
119
|
+
levels,
|
|
120
|
+
}
|
|
121
|
+
fs.mkdirSync(path.dirname(path.resolve(options.out)), { recursive: true })
|
|
122
|
+
fs.writeFileSync(options.out, JSON.stringify(output, null, 2))
|
|
123
|
+
console.log(JSON.stringify({ out: path.resolve(options.out), summary }, null, 2))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try { main() } catch (error) { console.error(`m1-sidecar: ${error.message}`); process.exitCode = 3 }
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* M2 runner (MVP): execute one package's L1-L3 in an isolated child process.
|
|
4
|
+
*
|
|
5
|
+
* This is NOT a security sandbox. It isolates the checker from the developer's
|
|
6
|
+
* real DSH_HOME/profile and kills the process tree on timeout; it does not
|
|
7
|
+
* restrict the child's filesystem or network. The standard's M2 contract
|
|
8
|
+
* (50 §5) still needs a container/image for untrusted packages.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/m2-runner.mjs \
|
|
12
|
+
* --package /tmp/dsh-host/plugins/ci-diet \
|
|
13
|
+
* --runtime <dsh-checkout>/vendor/cordis/lib/index.js \
|
|
14
|
+
* --runtime-version 0.2.2-alpha.2 \
|
|
15
|
+
* --out results/m2/ci-diet.json
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import os from 'node:os'
|
|
19
|
+
import path from 'node:path'
|
|
20
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
21
|
+
|
|
22
|
+
function parse(argv) {
|
|
23
|
+
const options = {
|
|
24
|
+
package: null, runtime: null, runtimeVersion: '0.2.2-alpha.2', cordisVersion: '4.0.6-alpha.1',
|
|
25
|
+
timeoutMs: 60_000, out: null, keep: false, json: false, modules: null, backend: 'process', image: null,
|
|
26
|
+
}
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const arg = argv[i]
|
|
29
|
+
if (arg === '--package') options.package = argv[++i]
|
|
30
|
+
else if (arg === '--runtime') options.runtime = argv[++i]
|
|
31
|
+
else if (arg === '--runtime-version') options.runtimeVersion = argv[++i]
|
|
32
|
+
else if (arg === '--cordis-version') options.cordisVersion = argv[++i]
|
|
33
|
+
else if (arg === '--timeout') options.timeoutMs = Number(argv[++i])
|
|
34
|
+
else if (arg === '--out') options.out = argv[++i]
|
|
35
|
+
else if (arg === '--keep') options.keep = true
|
|
36
|
+
else if (arg === '--modules') options.modules = argv[++i]
|
|
37
|
+
else if (arg === '--backend') options.backend = argv[++i]
|
|
38
|
+
else if (arg === '--image') options.image = argv[++i]
|
|
39
|
+
else if (arg === '--json') options.json = true
|
|
40
|
+
else if (arg === '--help' || arg === '-h') { console.log('usage: node scripts/m2-runner.mjs --package <dir> --runtime <cordis-lib> [--backend process|unshare|landlock|unshare-landlock] [--image ref@sha256:<64hex>] [--out level.json]'); process.exit(0) }
|
|
41
|
+
else throw new Error(`unknown option: ${arg}`)
|
|
42
|
+
}
|
|
43
|
+
if (!options.package) throw new Error('--package is required')
|
|
44
|
+
if (!options.runtime) throw new Error('--runtime is required')
|
|
45
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 1000) throw new Error('--timeout must be >= 1000 ms')
|
|
46
|
+
if (!['process', 'unshare', 'landlock', 'unshare-landlock'].includes(options.backend)) throw new Error('--backend must be process, unshare, landlock, or unshare-landlock')
|
|
47
|
+
if (options.image && !/^[^@\s]+@sha256:[0-9a-f]{64}$/.test(options.image)) throw new Error('--image must be ref@sha256:<64 hex>')
|
|
48
|
+
return options
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function killTree(child, signal = 'SIGKILL') {
|
|
52
|
+
if (!child.pid) return
|
|
53
|
+
if (process.platform === 'win32') {
|
|
54
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
try { process.kill(-child.pid, signal) } catch { try { child.kill(signal) } catch { /* gone */ } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runChecker(command, args, cwd, env, timeoutMs) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const child = spawn(command, args, { cwd, env, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
63
|
+
let stdout = ''
|
|
64
|
+
let stderr = ''
|
|
65
|
+
let killed = false
|
|
66
|
+
const timer = setTimeout(() => { killed = true; killTree(child) }, timeoutMs)
|
|
67
|
+
child.stdout.on('data', chunk => { stdout += chunk })
|
|
68
|
+
child.stderr.on('data', chunk => { stderr += chunk })
|
|
69
|
+
child.on('error', error => { clearTimeout(timer); resolve({ code: null, stdout, stderr: `${stderr}${error.message}`, killed }) })
|
|
70
|
+
child.on('close', code => { clearTimeout(timer); resolve({ code, stdout, stderr, killed }) })
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function linkModules(pkg, modules) {
|
|
75
|
+
if (!modules) return 0
|
|
76
|
+
const source = path.resolve(modules)
|
|
77
|
+
if (!fs.existsSync(source)) throw new Error(`--modules not found: ${source}`)
|
|
78
|
+
const target = path.join(pkg, 'node_modules')
|
|
79
|
+
fs.mkdirSync(target, { recursive: true })
|
|
80
|
+
let linked = 0
|
|
81
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
82
|
+
const from = path.join(source, entry.name)
|
|
83
|
+
const to = path.join(target, entry.name)
|
|
84
|
+
if (entry.isDirectory() && entry.name.startsWith('@')) {
|
|
85
|
+
fs.mkdirSync(to, { recursive: true })
|
|
86
|
+
for (const child of fs.readdirSync(from, { withFileTypes: true })) {
|
|
87
|
+
const dest = path.join(to, child.name)
|
|
88
|
+
if (fs.existsSync(dest)) continue
|
|
89
|
+
try { fs.symlinkSync(path.join(from, child.name), dest, process.platform === 'win32' ? 'junction' : 'dir'); linked += 1 } catch { /* skip */ }
|
|
90
|
+
}
|
|
91
|
+
} else if (!fs.existsSync(to)) {
|
|
92
|
+
try { fs.symlinkSync(from, to, process.platform === 'win32' ? 'junction' : 'dir'); linked += 1 } catch { /* skip */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return linked
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ensureLandlock() {
|
|
99
|
+
const source = path.resolve(path.dirname(new URL(import.meta.url).pathname), 'landlock-exec.c')
|
|
100
|
+
const binary = path.join(os.tmpdir(), 'dsh-landlock-exec')
|
|
101
|
+
try {
|
|
102
|
+
const sourceStat = fs.statSync(source)
|
|
103
|
+
const binaryStat = fs.statSync(binary, { throwIfNoEntry: false })
|
|
104
|
+
if (binaryStat && binaryStat.mtimeMs >= sourceStat.mtimeMs) return binary
|
|
105
|
+
} catch { /* compile below */ }
|
|
106
|
+
const result = spawnSync('cc', ['-O2', source, '-o', binary], { stdio: 'inherit' })
|
|
107
|
+
if (result.error || result.status !== 0) throw new Error('cannot build landlock-exec (need a C compiler)')
|
|
108
|
+
return binary
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mapStatus(report) {
|
|
112
|
+
const status = report?.conclusion?.status
|
|
113
|
+
if (status === 'pass') return 'verified'
|
|
114
|
+
if (status === 'warn') return 'verified-warn'
|
|
115
|
+
if (status === 'fail') return 'failed'
|
|
116
|
+
return 'inconclusive'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const options = parse(process.argv.slice(2))
|
|
121
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-m2-'))
|
|
122
|
+
const home = path.join(root, 'home')
|
|
123
|
+
const work = path.join(root, 'work')
|
|
124
|
+
const pkg = path.join(root, 'package')
|
|
125
|
+
fs.mkdirSync(home, { recursive: true })
|
|
126
|
+
fs.mkdirSync(work, { recursive: true })
|
|
127
|
+
fs.cpSync(path.resolve(options.package), pkg, { recursive: true, dereference: false, filter: source => !source.includes(`${path.sep}.git${path.sep}`) })
|
|
128
|
+
const linkedModules = linkModules(pkg, options.modules)
|
|
129
|
+
const reportPath = path.join(work, 'report.json')
|
|
130
|
+
const cli = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'bin', 'dsh-plugin-verify.mjs')
|
|
131
|
+
const args = [cli, pkg, '--format', 'json', '--treat-as-plugin', '--runtime', path.resolve(options.runtime), '--runtime-version', options.runtimeVersion, '--cordis-version', options.cordisVersion, '--out', reportPath]
|
|
132
|
+
const env = {
|
|
133
|
+
...process.env,
|
|
134
|
+
DSH_HOME: home,
|
|
135
|
+
HOME: home,
|
|
136
|
+
USERPROFILE: home,
|
|
137
|
+
TMPDIR: path.join(root, 'tmp'),
|
|
138
|
+
TEMP: path.join(root, 'tmp'),
|
|
139
|
+
TMP: path.join(root, 'tmp'),
|
|
140
|
+
}
|
|
141
|
+
fs.mkdirSync(env.TMPDIR, { recursive: true })
|
|
142
|
+
const started = Date.now()
|
|
143
|
+
const needsLandlock = options.backend === 'landlock' || options.backend === 'unshare-landlock'
|
|
144
|
+
const needsUnshare = options.backend === 'unshare' || options.backend === 'unshare-landlock'
|
|
145
|
+
const landlock = needsLandlock ? ensureLandlock() : null
|
|
146
|
+
const command = needsUnshare ? 'unshare' : needsLandlock ? landlock : process.execPath
|
|
147
|
+
const commandArgs = []
|
|
148
|
+
if (needsUnshare) {
|
|
149
|
+
commandArgs.push('-Urn', '--fork', '--pid', '--mount-proc')
|
|
150
|
+
if (needsLandlock) commandArgs.push(landlock)
|
|
151
|
+
}
|
|
152
|
+
if (needsLandlock) commandArgs.push('--rw', home, '--rw', work, '--rw', env.TMPDIR, '--')
|
|
153
|
+
commandArgs.push(process.execPath, ...args)
|
|
154
|
+
const result = await runChecker(command, commandArgs, work, env, options.timeoutMs)
|
|
155
|
+
const elapsedMs = Date.now() - started
|
|
156
|
+
let report = null
|
|
157
|
+
try { report = JSON.parse(fs.readFileSync(reportPath, 'utf8')) } catch { /* report missing */ }
|
|
158
|
+
const imageUnverified = Boolean(options.image)
|
|
159
|
+
const status = result.killed || imageUnverified ? 'inconclusive' : mapStatus(report)
|
|
160
|
+
const level = {
|
|
161
|
+
schema: 'dsh-m2-level/1',
|
|
162
|
+
status,
|
|
163
|
+
standard: report?.standard ?? { id: 'DS-C', version: '1.0.0' },
|
|
164
|
+
subject: report?.subject ?? { name: null, version: null, path: path.resolve(options.package) },
|
|
165
|
+
anchor: report?.anchor ?? null,
|
|
166
|
+
coverage: report?.coverage ?? null,
|
|
167
|
+
conclusion: report?.conclusion ?? null,
|
|
168
|
+
m1: { coverage: report?.coverage ?? null, conclusion: report?.conclusion ?? null },
|
|
169
|
+
reason: imageUnverified ? 'image metadata is unverified; M2 cannot assert verified' : undefined,
|
|
170
|
+
sandbox: {
|
|
171
|
+
runner: 'm2-runner/1',
|
|
172
|
+
node: process.version,
|
|
173
|
+
timeoutMs: options.timeoutMs,
|
|
174
|
+
elapsedMs,
|
|
175
|
+
killed: result.killed,
|
|
176
|
+
exitCode: result.code,
|
|
177
|
+
isolatedHome: true,
|
|
178
|
+
backend: options.backend,
|
|
179
|
+
networkNamespace: needsUnshare,
|
|
180
|
+
fsWriteRestricted: needsLandlock,
|
|
181
|
+
securityBoundary: false,
|
|
182
|
+
containment: options.backend === 'unshare-landlock'
|
|
183
|
+
? 'landlock-fs+network-namespace+process'
|
|
184
|
+
: options.backend === 'landlock'
|
|
185
|
+
? 'landlock-fs+process'
|
|
186
|
+
: options.backend === 'unshare'
|
|
187
|
+
? 'network-namespace+process'
|
|
188
|
+
: 'none',
|
|
189
|
+
linkedModules,
|
|
190
|
+
image: options.image ? { reference: options.image.split('@')[0], digest: `sha256:${options.image.split('@sha256:')[1]}`, verified: false, assertion: 'unverified-metadata', reason: 'OCI/Docker backend not enabled; metadata only; cannot yield verified' } : null,
|
|
191
|
+
note: needsLandlock
|
|
192
|
+
? 'Landlock restricts writes to HOME/TMP/cwd; read is allowed everywhere. Network isolation only with unshare-landlock.'
|
|
193
|
+
: needsUnshare
|
|
194
|
+
? 'user+network namespace + process isolation; no filesystem write restriction'
|
|
195
|
+
: 'process isolation only; no filesystem/network restriction',
|
|
196
|
+
},
|
|
197
|
+
checks: report?.checks ?? [],
|
|
198
|
+
stderr: result.stderr.slice(0, 4000),
|
|
199
|
+
ranAt: new Date().toISOString(),
|
|
200
|
+
}
|
|
201
|
+
if (!options.keep) fs.rmSync(root, { recursive: true, force: true })
|
|
202
|
+
const output = JSON.stringify(level, null, 2)
|
|
203
|
+
if (options.out) {
|
|
204
|
+
const out = path.resolve(options.out)
|
|
205
|
+
fs.mkdirSync(path.dirname(out), { recursive: true })
|
|
206
|
+
fs.writeFileSync(out, output)
|
|
207
|
+
}
|
|
208
|
+
if (options.json || !options.out) process.stdout.write(`${output}\n`)
|
|
209
|
+
const exit = status === 'verified' || status === 'verified-warn' ? 0 : status === 'failed' ? 1 : 2
|
|
210
|
+
process.exitCode = exit
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch(error => { console.error(`m2-runner: ${error.message}`); process.exitCode = 3 })
|