dsh-standards 1.4.1 → 1.6.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/fixtures/security/sec15-clean/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-clean/package.json +19 -0
- package/40-checker/fixtures/security/sec15-missing-ref/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-missing-ref/package.json +22 -0
- package/40-checker/fixtures/security/sec15-node/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-node/package.json +22 -0
- package/40-checker/fixtures/security/sec15-node/scripts/build.mjs +1 -0
- package/40-checker/fixtures/security/sec15-pipe/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-pipe/package.json +22 -0
- package/40-checker/fixtures/security/sec16-client/index.mjs +3 -0
- package/40-checker/fixtures/security/sec16-client/lib/client.js +3 -0
- package/40-checker/fixtures/security/sec16-client/package.json +21 -0
- package/40-checker/fixtures/security/sec16-declared/index.mjs +3 -0
- package/40-checker/fixtures/security/sec16-declared/package.json +19 -0
- package/40-checker/fixtures/security/sec16-eval/index.mjs +3 -0
- package/40-checker/fixtures/security/sec16-eval/package.json +19 -0
- package/40-checker/fixtures/security/sec16-require/index.mjs +4 -0
- package/40-checker/fixtures/security/sec16-require/package.json +19 -0
- package/40-checker/fixtures/security/sec17-env/index.mjs +5 -0
- package/40-checker/fixtures/security/sec17-env/package.json +19 -0
- package/40-checker/fixtures/security/sec17-seam/index.mjs +3 -0
- package/40-checker/fixtures/security/sec17-seam/package.json +19 -0
- package/40-checker/fixtures/security/sec17-ssh/index.mjs +6 -0
- package/40-checker/fixtures/security/sec17-ssh/package.json +19 -0
- package/40-checker/fixtures/security/sec18-bad/index.mjs +3 -0
- package/40-checker/fixtures/security/sec18-bad/package.json +21 -0
- package/40-checker/fixtures/security/sec18-good/index.mjs +3 -0
- package/40-checker/fixtures/security/sec18-good/package.json +26 -0
- package/40-checker/fixtures/security/sec18-undeclared/index.mjs +3 -0
- package/40-checker/fixtures/security/sec18-undeclared/package.json +26 -0
- package/40-checker/fixtures/security/sec19-blob/index.mjs +2 -0
- package/40-checker/fixtures/security/sec19-blob/package.json +19 -0
- package/40-checker/fixtures/security/sec19-clean/index.mjs +2 -0
- package/40-checker/fixtures/security/sec19-clean/package.json +19 -0
- package/40-checker/fixtures/security/sec19-wasm/blob.wasm +0 -0
- package/40-checker/fixtures/security/sec19-wasm/index.mjs +3 -0
- package/40-checker/fixtures/security/sec19-wasm/package.json +19 -0
- package/40-checker/fixtures/security/sec20-unwrapped/index.mjs +4 -0
- package/40-checker/fixtures/security/sec20-unwrapped/package.json +23 -0
- package/40-checker/fixtures/security/sec20-wrapped/index.mjs +3 -0
- package/40-checker/fixtures/security/sec20-wrapped/package.json +23 -0
- package/40-checker/fixtures/security/sec21-known/index.mjs +3 -0
- package/40-checker/fixtures/security/sec21-known/package.json +23 -0
- package/40-checker/fixtures/security/sec21-typosquat/index.mjs +3 -0
- package/40-checker/fixtures/security/sec21-typosquat/package.json +22 -0
- package/40-checker/fixtures/security/sec22-attested/index.mjs +3 -0
- package/40-checker/fixtures/security/sec22-attested/package.json +22 -0
- package/40-checker/fixtures/security/sec22-none/index.mjs +3 -0
- package/40-checker/fixtures/security/sec22-none/package.json +19 -0
- package/40-checker/scripts/sign-evidence.mjs +169 -0
- package/40-checker/src/cli.mjs +402 -73
- package/40-checker/src/declarations.mjs +10 -10
- package/40-checker/src/flow-evidence.mjs +21 -0
- package/40-checker/src/index.mjs +2 -1
- package/40-checker/src/report.mjs +116 -4
- 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/security.mjs +1065 -0
- package/40-checker/src/signature.mjs +249 -0
- package/40-checker/src/util.mjs +53 -0
- package/CHANGELOG.md +34 -0
- package/docs/SIGNING.md +137 -0
- package/docs/coverage.md +39 -6
- package/package.json +1 -1
- package/spec/coverage-contract.md +33 -11
- package/spec/dsc-clauses.json +410 -9
- package/spec/remediation.json +196 -0
- package/spec/schemas/conformance.schema.json +80 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 signatures for FLOW evidence (`dsh-flow-evidence/2`).
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies: `node:crypto` only. A signature is a detached Ed25519
|
|
5
|
+
* signature over the canonical JSON encoding of the document with the
|
|
6
|
+
* `signature` field removed. The chain hash is computed over the body (minus
|
|
7
|
+
* `chain` and `signature`); the signed payload includes `chain`, so the
|
|
8
|
+
* signature also binds the declared chain hash. Signing never changes
|
|
9
|
+
* `chain.evidenceHash`, because `signature` is excluded from both payloads.
|
|
10
|
+
*
|
|
11
|
+
* Signature record:
|
|
12
|
+
*
|
|
13
|
+
* signature: {
|
|
14
|
+
* alg: "ed25519",
|
|
15
|
+
* keyId: "<first 16 hex of sha256(SPKI public key PEM)>",
|
|
16
|
+
* publicKey: "<SPKI PEM>",
|
|
17
|
+
* sig: "<base64 detached signature>",
|
|
18
|
+
* at: "<ISO-8601 timestamp>"
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* `keyId` is derived from the exact SPKI PEM string that is embedded as
|
|
22
|
+
* `publicKey` (line endings included), so a verifier can recompute it from the
|
|
23
|
+
* signature record. This is an integrity/origin mechanism only: an embedded
|
|
24
|
+
* public key is self-asserted, so a valid signature does not make the signer
|
|
25
|
+
* trustworthy.
|
|
26
|
+
*/
|
|
27
|
+
import crypto from 'node:crypto'
|
|
28
|
+
|
|
29
|
+
export const SIGNATURE_ALG = 'ed25519'
|
|
30
|
+
export const SIGNATURE_SCHEMA = 'dsh-evidence-signature/1'
|
|
31
|
+
export const KEY_ID_LENGTH = 16
|
|
32
|
+
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** JSON with recursively sorted object keys; array order is preserved. */
|
|
38
|
+
export function canonicalJson(value) {
|
|
39
|
+
const sort = (node) => {
|
|
40
|
+
if (Array.isArray(node)) return node.map(sort)
|
|
41
|
+
if (isPlainObject(node)) {
|
|
42
|
+
const out = {}
|
|
43
|
+
for (const key of Object.keys(node).sort()) out[key] = sort(node[key])
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
return node
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(sort(value))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The exact bytes an evidence signature covers: canonical JSON of every field
|
|
53
|
+
* except `signature`. Unlike `evidenceHash()` (which also removes `chain`),
|
|
54
|
+
* this includes `chain`, so a signature binds the declared chain hash as well.
|
|
55
|
+
*/
|
|
56
|
+
export function evidenceSigningPayload(document) {
|
|
57
|
+
if (!isPlainObject(document)) throw new TypeError('evidence document must be a JSON object')
|
|
58
|
+
const copy = { ...document }
|
|
59
|
+
delete copy.signature
|
|
60
|
+
return Buffer.from(canonicalJson(copy), 'utf8')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function asPublicKey(key) {
|
|
64
|
+
if (key instanceof crypto.KeyObject) {
|
|
65
|
+
if (key.type === 'public') return key
|
|
66
|
+
return crypto.createPublicKey(key)
|
|
67
|
+
}
|
|
68
|
+
return crypto.createPublicKey(key)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function asPrivateKey(key) {
|
|
72
|
+
if (key instanceof crypto.KeyObject) {
|
|
73
|
+
if (key.type === 'private') return key
|
|
74
|
+
return crypto.createPrivateKey(key)
|
|
75
|
+
}
|
|
76
|
+
return crypto.createPrivateKey(key)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function publicKeyPem(key) {
|
|
80
|
+
return asPublicKey(key).export({ type: 'spki', format: 'pem' }).toString()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** keyId = first 16 hex of sha256(SPKI public key PEM text). */
|
|
84
|
+
export function keyIdFromPublicKey(key) {
|
|
85
|
+
let pem
|
|
86
|
+
if (typeof key === 'string' && key.includes('BEGIN PUBLIC KEY')) {
|
|
87
|
+
pem = key
|
|
88
|
+
} else if (Buffer.isBuffer(key) && key.toString('utf8').includes('BEGIN PUBLIC KEY')) {
|
|
89
|
+
pem = key.toString('utf8')
|
|
90
|
+
} else {
|
|
91
|
+
pem = publicKeyPem(key)
|
|
92
|
+
}
|
|
93
|
+
return crypto.createHash('sha256').update(pem).digest('hex').slice(0, KEY_ID_LENGTH)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function generateEd25519KeyPair() {
|
|
97
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519')
|
|
98
|
+
return {
|
|
99
|
+
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
100
|
+
publicKeyPem: publicKeyPem(publicKey),
|
|
101
|
+
keyId: keyIdFromPublicKey(publicKey),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function loadPrivateKey(pem) {
|
|
106
|
+
return crypto.createPrivateKey(pem)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function loadPublicKey(pem) {
|
|
110
|
+
return crypto.createPublicKey(pem)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return a copy of `document` carrying an Ed25519 signature. Any previous
|
|
115
|
+
* signature is replaced (it is not part of the signed payload). `at` defaults
|
|
116
|
+
* to now; pass an explicit ISO timestamp for reproducible evidence.
|
|
117
|
+
*/
|
|
118
|
+
export function signEvidence(document, privateKey, { at = new Date().toISOString(), keyId = null } = {}) {
|
|
119
|
+
if (!isPlainObject(document)) throw new TypeError('evidence document must be a JSON object')
|
|
120
|
+
const privateObject = asPrivateKey(privateKey)
|
|
121
|
+
const publicObject = crypto.createPublicKey(privateObject)
|
|
122
|
+
const pem = publicKeyPem(publicObject)
|
|
123
|
+
const computed = keyIdFromPublicKey(pem)
|
|
124
|
+
const id = keyId ?? computed
|
|
125
|
+
if (typeof id !== 'string' || !/^[0-9a-f]{16}$/.test(id)) {
|
|
126
|
+
throw new Error('signature.keyId must be 16 lowercase hex characters')
|
|
127
|
+
}
|
|
128
|
+
if (id !== computed) {
|
|
129
|
+
throw new Error(`signature.keyId does not match the private key (expected ${computed})`)
|
|
130
|
+
}
|
|
131
|
+
if (typeof at !== 'string' || at.length === 0) {
|
|
132
|
+
throw new Error('signature.at must be a non-empty ISO-8601 string')
|
|
133
|
+
}
|
|
134
|
+
const sig = crypto.sign(null, evidenceSigningPayload(document), privateObject).toString('base64')
|
|
135
|
+
return {
|
|
136
|
+
...document,
|
|
137
|
+
signature: {
|
|
138
|
+
alg: SIGNATURE_ALG,
|
|
139
|
+
keyId: id,
|
|
140
|
+
publicKey: pem,
|
|
141
|
+
sig,
|
|
142
|
+
at,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function invalid(reason, extra = {}) {
|
|
148
|
+
return { status: 'invalid', reason, ...extra }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Verify a signed evidence document.
|
|
153
|
+
*
|
|
154
|
+
* - `missing`: no `signature` (or `signature: null`), or no public key is
|
|
155
|
+
* available to check it (embedded key absent and no pinned key supplied).
|
|
156
|
+
* - `invalid`: malformed signature, key mismatch, or a payload that does not
|
|
157
|
+
* verify.
|
|
158
|
+
* - `valid`: the signature verifies against the embedded key (or `publicKey`
|
|
159
|
+
* when pinned).
|
|
160
|
+
*
|
|
161
|
+
* `publicKey` pins the trust anchor: pass a PEM/KeyObject out-of-band and the
|
|
162
|
+
* embedded key is only used to check `keyId` consistency. Without a pin, the
|
|
163
|
+
* embedded key is self-asserted — valid means "these bytes were signed by the
|
|
164
|
+
* holder of this key", not "the signer is trusted".
|
|
165
|
+
*/
|
|
166
|
+
export function verifyEvidenceSignature(document, { publicKey = null } = {}) {
|
|
167
|
+
if (!isPlainObject(document)) return invalid('evidence document must be a JSON object')
|
|
168
|
+
const signature = document.signature
|
|
169
|
+
if (signature === undefined || signature === null) {
|
|
170
|
+
return { status: 'missing', reason: 'document has no signature' }
|
|
171
|
+
}
|
|
172
|
+
if (!isPlainObject(signature)) return invalid('signature must be an object')
|
|
173
|
+
if (signature.alg !== SIGNATURE_ALG) return invalid(`signature.alg must be ${SIGNATURE_ALG}`)
|
|
174
|
+
if (typeof signature.sig !== 'string' || signature.sig.length === 0) {
|
|
175
|
+
return invalid('signature.sig must be a non-empty base64 string')
|
|
176
|
+
}
|
|
177
|
+
if (typeof signature.keyId !== 'string' || !/^[0-9a-f]{16}$/.test(signature.keyId)) {
|
|
178
|
+
return invalid('signature.keyId must be 16 lowercase hex characters')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let embedded = null
|
|
182
|
+
if (signature.publicKey !== undefined && signature.publicKey !== null) {
|
|
183
|
+
if (typeof signature.publicKey !== 'string' || signature.publicKey.trim() === '') {
|
|
184
|
+
return invalid('signature.publicKey must be a non-empty PEM string when present')
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
embedded = asPublicKey(signature.publicKey)
|
|
188
|
+
} catch {
|
|
189
|
+
return invalid('signature.publicKey is not a valid public key')
|
|
190
|
+
}
|
|
191
|
+
const embeddedId = keyIdFromPublicKey(signature.publicKey)
|
|
192
|
+
if (embeddedId !== signature.keyId) {
|
|
193
|
+
return invalid('signature.keyId does not match sha256(signature.publicKey)[:16]', {
|
|
194
|
+
keyId: embeddedId,
|
|
195
|
+
embeddedKeyId: embeddedId,
|
|
196
|
+
keyIdMatch: false,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let key = null
|
|
202
|
+
let source = null
|
|
203
|
+
if (publicKey !== null && publicKey !== undefined) {
|
|
204
|
+
try {
|
|
205
|
+
key = asPublicKey(publicKey)
|
|
206
|
+
} catch {
|
|
207
|
+
return invalid('pinned public key is not a valid public key')
|
|
208
|
+
}
|
|
209
|
+
source = 'pinned'
|
|
210
|
+
const pinnedId = keyIdFromPublicKey(key)
|
|
211
|
+
if (pinnedId !== signature.keyId) {
|
|
212
|
+
return invalid('signature.keyId does not match the pinned public key', {
|
|
213
|
+
keyId: pinnedId,
|
|
214
|
+
embeddedKeyId: embedded ? keyIdFromPublicKey(embedded) : null,
|
|
215
|
+
keyIdMatch: false,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
} else if (embedded) {
|
|
219
|
+
key = embedded
|
|
220
|
+
source = 'embedded'
|
|
221
|
+
} else {
|
|
222
|
+
return {
|
|
223
|
+
status: 'missing',
|
|
224
|
+
reason: 'no public key: signature.publicKey is missing and no pinned public key was supplied',
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const derivedKeyId = keyIdFromPublicKey(key)
|
|
229
|
+
let ok = false
|
|
230
|
+
try {
|
|
231
|
+
ok = crypto.verify(null, evidenceSigningPayload(document), key, Buffer.from(signature.sig, 'base64'))
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return invalid(`signature verification failed: ${error.message}`, {
|
|
234
|
+
keyId: derivedKeyId,
|
|
235
|
+
keyIdMatch: true,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
const result = {
|
|
239
|
+
status: ok ? 'valid' : 'invalid',
|
|
240
|
+
alg: SIGNATURE_ALG,
|
|
241
|
+
keyId: derivedKeyId,
|
|
242
|
+
embeddedKeyId: embedded ? keyIdFromPublicKey(embedded) : null,
|
|
243
|
+
keyIdMatch: true,
|
|
244
|
+
publicKeySource: source,
|
|
245
|
+
at: typeof signature.at === 'string' ? signature.at : null,
|
|
246
|
+
}
|
|
247
|
+
if (!ok) result.reason = 'signature does not verify against the public key'
|
|
248
|
+
return result
|
|
249
|
+
}
|
package/40-checker/src/util.mjs
CHANGED
|
@@ -123,6 +123,59 @@ export function listSourceFiles(root, options = {}) {
|
|
|
123
123
|
return scanSourceFiles(root, options).paths
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Inventory every file in a package (not only source extensions) without
|
|
128
|
+
* following symlinks. Used by the security checks for binary/obfuscation and
|
|
129
|
+
* script-reference analysis. Same skip directories as the source walker so
|
|
130
|
+
* node_modules/tests/docs do not dominate the sample.
|
|
131
|
+
* @param {string} root
|
|
132
|
+
* @param {{maxFiles?: number, maxBytes?: number}} [options]
|
|
133
|
+
*/
|
|
134
|
+
export function inventoryFiles(root, options = {}) {
|
|
135
|
+
const maxFiles = options.maxFiles ?? 1000
|
|
136
|
+
const maxBytes = options.maxBytes ?? 20_000_000
|
|
137
|
+
const files = []
|
|
138
|
+
let bytes = 0
|
|
139
|
+
let truncated = false
|
|
140
|
+
const walk = (directory) => {
|
|
141
|
+
if (files.length >= maxFiles || bytes >= maxBytes) {
|
|
142
|
+
truncated = true
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
let entries
|
|
146
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch { return }
|
|
147
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (files.length >= maxFiles || bytes >= maxBytes) {
|
|
150
|
+
truncated = true
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const absolute = path.join(directory, entry.name)
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
if (SKIP_DIRECTORIES.has(entry.name)) continue
|
|
156
|
+
if (entry.name.startsWith('.')) continue
|
|
157
|
+
walk(absolute)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (!entry.isFile()) continue
|
|
161
|
+
let size = 0
|
|
162
|
+
try { size = fs.statSync(absolute).size } catch { continue }
|
|
163
|
+
if (bytes + size > maxBytes) {
|
|
164
|
+
truncated = true
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
bytes += size
|
|
168
|
+
files.push({
|
|
169
|
+
file: absolute,
|
|
170
|
+
relative: posixRelative(root, absolute),
|
|
171
|
+
size,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
walk(root)
|
|
176
|
+
return { files, stats: { files: files.length, bytes, maxFiles, maxBytes, truncated } }
|
|
177
|
+
}
|
|
178
|
+
|
|
126
179
|
/** Read all source files once, returning {file, relative, text} records. */
|
|
127
180
|
export function readSources(root, options) {
|
|
128
181
|
return scanSourceFiles(root, options).paths.map(file => ({
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
## 1.6.0 — 2026-11-07
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- 安全层 DSC-15…22:生命周期脚本、动态执行、敏感文件、外联端点、混淆/二进制、提示词注入线索、供应链、来源/完整性。
|
|
6
|
+
- `--scope security`:22 项检查,DSC-15/16/17/21 为强制;默认 14 项判定不变,security 块内 FAIL 封顶 WARN。
|
|
7
|
+
- 威胁模型与宿主边界(`34` §十):同进程全权限、capabilities 不被宿主读取、sandbox 仅子进程文件写、approval 仅 tool ask、`!!js` 可执行、client 可调 Remote。
|
|
8
|
+
- `SECURITY.md`(漏洞报告)、`spec/dsc-clauses.json` 22 父/113 条、`spec/remediation.json` C-15…22、coverage 口径 22。
|
|
9
|
+
|
|
10
|
+
### Verified
|
|
11
|
+
|
|
12
|
+
- `npm test` 269/269;恶意 fixture(curl|sh、eval、凭据读取、typosquat)在 security scope 下 FAIL;10 个真实包默认 entry 10/10 exit 0。
|
|
13
|
+
- 静态安全 PASS 不代表无恶意;检查器不能证明安全。
|
|
14
|
+
|
|
15
|
+
## 1.5.0 — 2026-11-07
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- 真实运行时检查:`--runtime`/`--host` 自动解析已装 dsh,在临时 HOME/DSH_HOME + env 白名单 + 超时硬杀下跑 DSC-04–09;报告新增 `runtimeChecks`(executed/modes/durationMs/isolation/checks)。
|
|
20
|
+
- Ed25519 证据签名:`sign-evidence.mjs keygen|sign|verify`(node:crypto,零依赖),`docs/SIGNING.md` 说明密钥与信任边界。
|
|
21
|
+
- 沙箱加固:`setpriv --no-new-privs --bounding-set=-all`、`nofile/fsize` rlimit、`unshare -Urm` + 只读 bind,记录 `hardening[]/hardeningDegraded[]`。
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `incomplete` 只由「mandatory 且 not-run」触发;静态 `partial` 不再把 ds-c bundle 判成 incomplete(修复前 dsh-fast 静态 ds-c 为 incomplete/exit 2,修复后 pass/exit 0)。
|
|
26
|
+
- runtime-child 写 fd3 后立即退出,不再被插件 timers 拖到超时导致六项全 not-run。
|
|
27
|
+
- 裸 `--runtime` 对带入口 bundle 直接走 server-entry;无入口纯 patch bundle 仍需 `--host`。
|
|
28
|
+
- `conformance.schema.json` 加 `runtimeChecks`。
|
|
29
|
+
|
|
30
|
+
### Verified
|
|
31
|
+
|
|
32
|
+
- `npm test` 249/249;真实宿主 dsh 0.2.1-rc.5:dsh-fast/mcp-panel 的 DSC-04–09 出 P/NA/P/P/W/P,security-pack 20.9s 完成 G0–G3。
|
|
33
|
+
- Windows 11 + node 24.22.1 实测:check 结果与 WSL 逐字一致;sandbox 记 degraded(unshare/prlimit 缺失),`flow collect --sandbox` 因 `npm.cmd` 无法无 shell spawn 记 not-run。
|
|
34
|
+
|
|
1
35
|
## 1.4.1 — 2026-11-06
|
|
2
36
|
|
|
3
37
|
- 建议的 `docsUrl` 基址跟到 `dsh-plugin-guide@v0.4.6`(FLOW 示例修正版),不再指向 0.4.4。
|
package/docs/SIGNING.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Evidence signing (Ed25519)
|
|
2
|
+
|
|
3
|
+
`dsh-flow-evidence/2` documents can carry a detached Ed25519 signature. The
|
|
4
|
+
implementation uses only `node:crypto` (no dependencies) and lives in
|
|
5
|
+
`40-checker/src/signature.mjs`; `40-checker/scripts/sign-evidence.mjs` is the
|
|
6
|
+
CLI. Signing is optional: it does not change `validateFlowEvidence()`, does not
|
|
7
|
+
change the chain hash, and never upgrades a claim level.
|
|
8
|
+
|
|
9
|
+
## What is signed
|
|
10
|
+
|
|
11
|
+
The payload is canonical JSON (recursively sorted object keys, array order
|
|
12
|
+
preserved) of every field **except `signature`**. This includes `chain`, so a
|
|
13
|
+
signature also binds the declared `chain.evidenceHash`. The chain hash itself
|
|
14
|
+
is computed over the body only (it excludes `chain` and `signature`). A signed
|
|
15
|
+
document keeps its existing `chain.evidenceHash`; `signature` is outside the
|
|
16
|
+
hash, so signing and re-signing do not invalidate the chain.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
signature: {
|
|
20
|
+
alg: "ed25519",
|
|
21
|
+
keyId: "<first 16 hex of sha256(SPKI public key PEM)>",
|
|
22
|
+
publicKey: "<SPKI PEM>",
|
|
23
|
+
sig: "<base64 detached signature>",
|
|
24
|
+
at: "<ISO-8601 timestamp>"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`keyId` is the first 16 hex characters of sha256 over the exact SPKI PEM string
|
|
29
|
+
embedded as `publicKey` (line endings included), so a verifier can recompute it
|
|
30
|
+
from the signature record. `publicKey` is embedded for transport convenience;
|
|
31
|
+
it is **self-asserted**.
|
|
32
|
+
|
|
33
|
+
## Key management
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
# Generate once, outside the repository working tree.
|
|
37
|
+
node 40-checker/scripts/sign-evidence.mjs keygen --out ~/.dsh-evidence-keys
|
|
38
|
+
# ed25519.pem (PKCS#8 private) and ed25519.pub.pem (SPKI public), both mode 0600.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Never commit `ed25519.pem`, put it in logs, or paste it into an issue. Back it
|
|
42
|
+
up in a secret manager; losing it means old signatures can no longer be
|
|
43
|
+
produced (they remain verifiable).
|
|
44
|
+
- `keygen` refuses to overwrite an existing key unless `--force` is passed.
|
|
45
|
+
- Distribute `ed25519.pub.pem` and its `keyId` through a separate, trusted
|
|
46
|
+
channel (release notes, repository file, pinned fingerprint). A public key
|
|
47
|
+
fetched from the same JSON it verifies proves nothing about identity.
|
|
48
|
+
- Rotate by generating a new pair; signatures keep their old `keyId`. Verifiers
|
|
49
|
+
should accept a set of pinned keyIds and record which one matched.
|
|
50
|
+
- The `at` field is signer-defined. It is evidence metadata, not a trusted
|
|
51
|
+
timestamp; a signer can backdate or replay. Add policy (expiry, nonce,
|
|
52
|
+
transparency log) outside this tool if that matters.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
# Sign after the collector has written and finalized the evidence.
|
|
58
|
+
node 40-checker/scripts/sign-evidence.mjs sign flow-evidence.json \
|
|
59
|
+
--key ~/.dsh-evidence-keys/ed25519.pem \
|
|
60
|
+
--out flow-evidence.signed.json
|
|
61
|
+
|
|
62
|
+
# Verify. Prints exactly one of: valid | invalid | missing.
|
|
63
|
+
node 40-checker/scripts/sign-evidence.mjs verify flow-evidence.signed.json
|
|
64
|
+
node 40-checker/scripts/sign-evidence.mjs verify flow-evidence.signed.json \
|
|
65
|
+
--pub ~/.dsh-evidence-keys/ed25519.pub.pem
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Exit codes: `0` valid, `1` invalid or missing, `2` usage/input error.
|
|
69
|
+
`--json` prints the detailed result (`status`, `keyId`, `keyIdMatch`,
|
|
70
|
+
`publicKeySource`, `reason`). `missing` means the document has no signature (or
|
|
71
|
+
`signature: null`), or there is no public key to check it (no embedded
|
|
72
|
+
`publicKey` and no readable `--pub`). `invalid` covers malformed records, key
|
|
73
|
+
mismatch, and a payload that does not verify. When `--pub` is supplied, it is
|
|
74
|
+
the trust anchor: the embedded key is only used to cross-check `keyId`.
|
|
75
|
+
|
|
76
|
+
The same helpers are re-exported from `40-checker/src/flow-evidence.mjs`:
|
|
77
|
+
`attachEvidenceSignature()`, `verifyEvidenceSignature()`,
|
|
78
|
+
`evidenceSigningPayload()`, `keyIdFromPublicKey()`.
|
|
79
|
+
|
|
80
|
+
## CI usage
|
|
81
|
+
|
|
82
|
+
Keep the private key in a CI secret and materialize it as a 0600 file only for
|
|
83
|
+
the signing step; delete it immediately afterwards. Never print it.
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
- name: Sign FLOW evidence
|
|
87
|
+
env:
|
|
88
|
+
EVIDENCE_PRIVATE_KEY: ${{ secrets.EVIDENCE_PRIVATE_KEY }}
|
|
89
|
+
run: |
|
|
90
|
+
umask 077
|
|
91
|
+
printf '%s' "$EVIDENCE_PRIVATE_KEY" > "$RUNNER_TEMP/evidence-key.pem"
|
|
92
|
+
node 40-checker/scripts/sign-evidence.mjs sign flow-evidence.json \
|
|
93
|
+
--key "$RUNNER_TEMP/evidence-key.pem" \
|
|
94
|
+
--out flow-evidence.signed.json
|
|
95
|
+
rm -f "$RUNNER_TEMP/evidence-key.pem"
|
|
96
|
+
|
|
97
|
+
- name: Verify with the pinned public key
|
|
98
|
+
run: |
|
|
99
|
+
node 40-checker/scripts/sign-evidence.mjs verify flow-evidence.signed.json \
|
|
100
|
+
--pub .github/evidence-pub.pem
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For PRs from forks, do not expose the signing secret to code from the fork.
|
|
104
|
+
Sign on a trusted workflow (e.g. `workflow_run` after review, or a maintainer
|
|
105
|
+
run); a signature produced by untrusted code is only as trustworthy as that
|
|
106
|
+
code.
|
|
107
|
+
|
|
108
|
+
## Trust boundary
|
|
109
|
+
|
|
110
|
+
**A valid self-signature is not trust.** It proves that whoever held the
|
|
111
|
+
private key signed these exact bytes. It does **not** prove:
|
|
112
|
+
|
|
113
|
+
- who that holder is (no identity, no CA, no account binding);
|
|
114
|
+
- that the evidence was produced honestly, or that commands ran where claimed;
|
|
115
|
+
- that the subject package is safe, correct, or officially endorsed;
|
|
116
|
+
- freshness: `at` can be backdated and a valid old document can be replayed;
|
|
117
|
+
- that the signer is allowed to speak for a project or marketplace.
|
|
118
|
+
|
|
119
|
+
Consequences:
|
|
120
|
+
|
|
121
|
+
- A verifier must pin the public key or `keyId` out-of-band. Treat the embedded
|
|
122
|
+
`publicKey` as a hint, not as an identity.
|
|
123
|
+
- Compromise of the private key compromises every signature made with it;
|
|
124
|
+
revocation and rotation are policy decisions this tool does not implement.
|
|
125
|
+
- Signature + chain hash give integrity and origin-of-holding, not truth. The
|
|
126
|
+
structural rules of `validateFlowEvidence()` and `level-audit` are unchanged;
|
|
127
|
+
signing cannot turn `self-attested`/`ci-attested` into `sandbox-attested`.
|
|
128
|
+
- A marketplace or adoption decision still needs its own policy: who is
|
|
129
|
+
allowed to sign, which keyIds are accepted, what evidence is required, how
|
|
130
|
+
long it is valid, and how disputes/revocations are handled. This repository
|
|
131
|
+
ships a mechanism, not a trust root and not a certification.
|
|
132
|
+
|
|
133
|
+
For the sandbox side of the evidence, see `40-checker/src/sandbox-run.mjs`:
|
|
134
|
+
`setpriv --no-new-privs --bounding-set=-all`, extra `nofile`/`fsize` rlimits,
|
|
135
|
+
and a read-only bind of the package copy are attempted and recorded. They are
|
|
136
|
+
containment, not seccomp/cgroup/OCI isolation, and they do not make a
|
|
137
|
+
self-signed document trustworthy.
|
package/docs/coverage.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 覆盖说明(
|
|
1
|
+
# 覆盖说明(22 项口径)— `dsh-standards check` 默认查什么
|
|
2
2
|
|
|
3
3
|
`dsh-standards check <package-dir>` 是作者入口。**默认 `--entry-claim static --no-exec`:不执行插件代码**。本文只描述这套默认,不把 `not-run` / `NA` 写成 PASS。
|
|
4
4
|
|
|
@@ -13,13 +13,15 @@ dsh-standards check <package-dir> \
|
|
|
13
13
|
|
|
14
14
|
| 口径 | 数量 | 说明 |
|
|
15
15
|
|---|---|---|
|
|
16
|
-
|
|
|
16
|
+
| 完整口径 | **22** | `DSC-01` … `DSC-22`;`--scope security` 时 `checks[]` 带全 22 项 |
|
|
17
|
+
| 默认 `checks[]` | **14** | `DSC-01` … `DSC-14`;默认 DS-C 判定只看这 14 项 |
|
|
17
18
|
| 默认实际评估 | **6** | `DSC-01`、`DSC-02`、`DSC-10`、`DSC-11`、`DSC-12`、`DSC-13` |
|
|
18
19
|
| 默认未检查 | **8** | `DSC-03`(NA)+ `DSC-04`–`DSC-09`(not-run)+ `DSC-14`(NA) |
|
|
20
|
+
| 默认 security 块 | **8** | `DSC-15`–`DSC-22` 在 `security` 块里跑 advisory;FAIL 封顶 WARN,不改变 DS-C 结论 |
|
|
19
21
|
|
|
20
22
|
`not checked` 桶把 NA / not-run / ERROR 都列出来;**没检查不等于通过**。`DSC-10` 是 advisory,只提供信号,不改变结论。
|
|
21
23
|
|
|
22
|
-
##
|
|
24
|
+
## 22 项逐条
|
|
23
25
|
|
|
24
26
|
| ID | 层 | 默认状态 | 默认为什么 | 怎么开启 |
|
|
25
27
|
|---|---|---|---|---|
|
|
@@ -37,6 +39,33 @@ dsh-standards check <package-dir> \
|
|
|
37
39
|
| `DSC-12` | L0 | **检查** | 凭据 backing file、`listRecords`、seam、commitments/disclosure 形状 | 默认即查 |
|
|
38
40
|
| `DSC-13` | L0 | **检查** | `dsh.labels` / `upstream` / 禁用语标签 | 默认即查 |
|
|
39
41
|
| `DSC-14` | L1 | **NA** | 默认 `entry-claim=static`,不声明运行时证据 | `--scope entry --entry-claim runtime --runtime <...>`(bundle 加 `--host`) |
|
|
42
|
+
| `DSC-15` | L0 | **advisory(security 块)** | 默认 scope 下 FAIL 封顶 WARN | `--scope security`;生命周期脚本,可 FAIL |
|
|
43
|
+
| `DSC-16` | L0 | **advisory(security 块)** | 默认 scope 下 FAIL 封顶 WARN | `--scope security`;动态执行,可 FAIL |
|
|
44
|
+
| `DSC-17` | L0 | **advisory(security 块)** | 默认 scope 下 FAIL 封顶 WARN | `--scope security`;敏感文件,可 FAIL |
|
|
45
|
+
| `DSC-18` | L0 | **advisory(security 块)** | 端点线索,永不 FAIL | `--scope security`;外联端点 |
|
|
46
|
+
| `DSC-19` | L0 | **advisory(security 块)** | 只登记不判恶 | `--scope security`;混淆/二进制 |
|
|
47
|
+
| `DSC-20` | L0 | **advisory(security 块)** | 语义不可判、误报高 | `--scope security`;提示词注入 |
|
|
48
|
+
| `DSC-21` | L0 | **advisory WARN(security 块)** | 名字线索;默认 scope 不 FAIL | `--scope security`;供应链,强制项但只 WARN |
|
|
49
|
+
| `DSC-22` | L0 | **advisory(security 块)** | 只有字段自证 | `--scope security`;来源/完整性,v1 只读字段 |
|
|
50
|
+
|
|
51
|
+
## security scope(C-15~C-22)
|
|
52
|
+
|
|
53
|
+
`--scope security` 是**独立、opt-in** 的安全范围。它把 8 项加入 `checks[]`(14 → 22),但**不改变** `DS-C` 徽章、也不并入 DS-C 的 11/12 条强制。它只降低风险,**不证明无恶意**:
|
|
54
|
+
|
|
55
|
+
| 项 | security scope 判定 | 触发 |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `DSC-15` 生命周期脚本 | **blocking**(可 FAIL) | 安装期 `curl\|sh`、`base64 -d`、`node -e`、`sh -c`、`powershell -enc`、引用缺失脚本、写 `$HOME`/`/etc` |
|
|
58
|
+
| `DSC-16` 动态执行 | **blocking**(可 FAIL) | `eval`/`new Function(非空)`/`vm`/`module._compile`/`process.binding` → FAIL;裸 `child_process` → WARN |
|
|
59
|
+
| `DSC-17` 敏感文件 | **blocking**(可 FAIL) | 直接读 `~/.ssh`、`$DSH_HOME` credentials、`id_rsa`、`.aws`、`.npmrc`、`.netrc` 等;`.env` 只 WARN |
|
|
60
|
+
| `DSC-18` 外联端点 | **advisory**(永不 FAIL) | 非 https、userinfo、公网裸 IP、webhook/paste/ngrok/onion、未披露端点 |
|
|
61
|
+
| `DSC-19` 混淆/二进制 | **advisory** | 二进制 sha256、超长单行、大块 base64、高熵;只登记不判恶 |
|
|
62
|
+
| `DSC-20` 提示词注入 | **advisory** | 外部文本进 systemPrompt、指令样字面量、无 untrusted 包裹;语义不可判 |
|
|
63
|
+
| `DSC-21` 供应链 | **blocking(强制项)/ WARN(发现封顶)** | scope 同形字/近似 scope、近似名、元数据缺失;**永不 FAIL**,但未跑会使 security `incomplete` |
|
|
64
|
+
| `DSC-22` 来源/完整性 | **advisory**(永不 FAIL) | 只读 `dist.integrity` / `dsh.attestation` / `dsh.signature` 字段是否存在;不验签、不比对 digest;无输入即 `not-run` |
|
|
65
|
+
|
|
66
|
+
**默认不改变判定**:不传 `--scope security` 时,这 8 项不进 `checks[]`,只在 `security` 块里以 `scope=advisory` 运行;任何 FAIL 封顶 WARN,`DS-C` 的 11/12 条强制集合与 exit code 不变。security 的 FAIL 不能撤销 `DS-C` PASS,`DS-C` PASS 也不能证明 security 通过。
|
|
67
|
+
|
|
68
|
+
**边界**:C-15/C-16/C-17 可以 FAIL;C-21 是强制项但发现只到 WARN;C-18/C-19/C-20/C-22 是 advisory;其余安全面(实际网络流量、审批绕过、同进程逃逸、client DOM、`!!js` 配置、Remote 调用方身份)**没有机械判据**。任何 PASS 都只是"本次覆盖内未见该类线索",不是"安全/已审计"。
|
|
40
69
|
|
|
41
70
|
## 执行 opt-in
|
|
42
71
|
|
|
@@ -44,7 +73,8 @@ dsh-standards check <package-dir> \
|
|
|
44
73
|
|
|
45
74
|
- `--runtime <path>`:Cordis 运行时的 `lib/index.js`,开启 L1–L3;
|
|
46
75
|
- `--host <file>`:app-boot 入口,开启 bundle 的 G0–G3 冒烟;
|
|
47
|
-
- `--entry-claim runtime`:entry 门声明运行时证据(通常还要配 `--runtime` / `--host
|
|
76
|
+
- `--entry-claim runtime`:entry 门声明运行时证据(通常还要配 `--runtime` / `--host`);
|
|
77
|
+
- `--scope security`:只跑 C-15~C-22 的静态/元数据检查,**仍不执行插件代码**(C-15 只读脚本文本,不跑 `npm install`)。
|
|
48
78
|
|
|
49
79
|
想保留运行时声明但只看静态结果,显式加 `--no-exec`。`--runtime-version` 只是 anchor,**不是**执行开关。
|
|
50
80
|
|
|
@@ -68,8 +98,11 @@ dsh-standards check <package-dir> \
|
|
|
68
98
|
## 复核命令
|
|
69
99
|
|
|
70
100
|
```sh
|
|
71
|
-
# 默认:6/14 评估,8 项 not-run/NA
|
|
72
|
-
dsh-standards check <package-dir> --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.log(r.coverage.checked+"/"+r.coverage.total);console.log(r.anchor);console.log(r.
|
|
101
|
+
# 默认:6/14 评估,8 项 not-run/NA;security 块是 advisory
|
|
102
|
+
dsh-standards check <package-dir> --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.log(r.coverage.checked+"/"+r.coverage.total);console.log(r.anchor);console.log(r.security)})'
|
|
103
|
+
|
|
104
|
+
# 默认 + security scope:checks[] 变 22 项(不执行插件)
|
|
105
|
+
dsh-standards check <package-dir> --scope security --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.log(r.coverage.checked+"/"+r.coverage.total);console.log(r.security)})'
|
|
73
106
|
|
|
74
107
|
# 显式声明宿主版本,仍不执行插件
|
|
75
108
|
dsh-standards check <package-dir> --host-version 0.2.2-rc.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# coverage 契约(`dsh-conformance/1`)
|
|
2
2
|
|
|
3
|
-
> 适用版本:dsh-standards **1.
|
|
3
|
+
> 适用版本:dsh-standards **1.6.0**(计划版本;1.5.0 及更早为 14 项)。`40-checker/src/report.mjs` 的 `buildReport()` 负责计数;`security` 块见 §6。
|
|
4
4
|
> 机器可校验形状见 [`schemas/conformance.schema.json`](schemas/conformance.schema.json);本文只统一字段含义。
|
|
5
5
|
> 结论:`coverage` 是**加性计数**,不参与 verdict;`PASS/WARN/FAIL` 与 `status/level` 由 `conclusion` 和对应 profile 的强制项决定。
|
|
6
6
|
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
| 字段 | 定义 |
|
|
19
19
|
|---|---|
|
|
20
|
-
| `total` | `checks[].length
|
|
21
|
-
| `mandatory` | 当前 `scope`/`profile`/`staticOnly`/`entryClaim` 选出的强制检查 ID 数,不是
|
|
20
|
+
| `total` | `checks[].length`:默认 `ds-c` 为 14(`DSC-01`…`DSC-14`);`--scope security` 为 22(再加 `DSC-15`…`DSC-22`)。 |
|
|
21
|
+
| `mandatory` | 当前 `scope`/`profile`/`staticOnly`/`entryClaim` 选出的强制检查 ID 数,不是 22;默认 package 11、listing 12、`--scope security` 4(`DSC-15/16/17/21`)。 |
|
|
22
22
|
| `checked` | 结果属于 `PASS` / `WARN` / `FAIL` 的检查数,即**实际评估过**的项。 |
|
|
23
23
|
| `pass` | 结果 `PASS` 的数量。 |
|
|
24
24
|
| `warn` | 结果 `WARN` 的数量。 |
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
- `checked = pass + warn + fail`(`advisory <= warn`)。
|
|
43
43
|
- `error <= notRun`;`notRun` 统计的是“没拿到完整结论”的项,`NA` 不算没跑。
|
|
44
|
-
-
|
|
44
|
+
- 默认 `ds-c` 完整跑(14 项、无缺失)与 `--scope security` 完整跑(22 项、无缺失)都满足 `checked + na + notRun = total`;两份报告的 `total` 不同,不能混算通过率。
|
|
45
45
|
- **例外**:若某项 `coverage: "partial"` 但结果仍是 `WARN`/`FAIL`,它会同时出现在 `checked` 与 `notRun` 中——已评估但不完整,不能当成 `full`。有缺失强制项时,`notRun` 会含不在 `checks[]` 里的 ID,此时上述加法可大于 `total`。
|
|
46
46
|
- `metrics = { checked, total, declaredFields }`:`checked`/`total` 与 `coverage` 同源;`declaredFields` 是 capabilities/labels/commitments/disclosure/engines.dsh 五个可选声明面中实际声明的个数(0–5),只作上下文。
|
|
47
47
|
|
|
@@ -78,23 +78,45 @@
|
|
|
78
78
|
- `checked`:真正产出 detail 的前提;`notChecked`:没产出的前提。
|
|
79
79
|
- `checked.length + notChecked.length = required`;`failed` 是 `checked` 中结果为 `FAIL` 的子集。
|
|
80
80
|
- `pass` 仅在 `failed` 为空**且** `notChecked` 为空时为 `true`。没给 `--install-spec`/`--market-index` 时 C-3.x 未查,`pass` 必为 `false`。
|
|
81
|
-
- `entryBlocking` 不替代 `coverage`;它回答“阻断前提齐不齐”,`coverage`
|
|
81
|
+
- `entryBlocking` 不替代 `coverage`;它回答“阻断前提齐不齐”,`coverage` 回答“`checks[]` 各是什么状态”(默认 14 项,security 22 项)。
|
|
82
82
|
|
|
83
|
-
## 5.
|
|
83
|
+
## 5. 示例(`--scope security` 示意,22 项;数字非实测)
|
|
84
84
|
|
|
85
85
|
```json
|
|
86
86
|
{
|
|
87
|
-
"total":
|
|
88
|
-
"pass":
|
|
89
|
-
"na": 1, "notRun": 7, "error": 7, "entryBlocking": null
|
|
87
|
+
"total": 22, "mandatory": 4, "checked": 14,
|
|
88
|
+
"pass": 12, "warn": 1, "advisory": 1, "fail": 1,
|
|
89
|
+
"na": 1, "notRun": 7, "error": 7, "entryBlocking": null,
|
|
90
|
+
"security": {
|
|
91
|
+
"scope": "security", "status": "fail",
|
|
92
|
+
"blocking": ["DSC-17"], "warn": [], "advisory": ["DSC-19"], "notRun": []
|
|
93
|
+
}
|
|
90
94
|
}
|
|
91
95
|
```
|
|
92
96
|
|
|
93
|
-
读法:
|
|
97
|
+
读法:12 PASS + 1 WARN + 1 FAIL = 14 项实际评估;1 项显式 NA;7 项 ERROR/未跑;`checked + na + notRun = 22`;`error (7) <= notRun (7)`。`conclusion.status` 由 `--scope security` 的 4 个强制项(`DSC-15/16/17/21`)决定;`security.status=fail` 只是 security 块的独立结论,不把 `DS-C` 徽章改写成 fail。
|
|
94
98
|
|
|
95
|
-
## 6.
|
|
99
|
+
## 6. security scope(C-15~C-22)
|
|
100
|
+
|
|
101
|
+
`--scope security` 是独立 opt-in scope,不并入 `ds-c` / `entry` / `mvp`。8 个 security 检查**始终**在报告的 `security` 块里运行;是否进入 `checks[]`、是否可 FAIL,取决于 scope。
|
|
102
|
+
|
|
103
|
+
| 项 | 语义 |
|
|
104
|
+
|---|---|
|
|
105
|
+
| 计入 `total` | **默认否**:`checks[]` 仍是 14,`security` 块单独存在、不计入 `coverage`。**security 是**:`checks[]` 22,`coverage.total=22`、`coverage.mandatory=4`。 |
|
|
106
|
+
| 默认 `security.scope` | `"advisory"`。8 项照常评估,但任何 `FAIL` 封顶为 `WARN`,不改 `conclusion`/exit;`security.status` 可为 `pass`/`warn`。 |
|
|
107
|
+
| `security.status=pass` | **security scope 下**:无 FAIL、无强制项结果不在 `PASS`/`WARN`/`FAIL`、无任何 WARN(含 advisory)。默认 advisory 模式只可能 `pass`/`warn`,且 `pass` 不代表覆盖完整。advisory 的 `not-run`(如 C-22 无元数据)不挡 `pass`,但必须留在 `security.notRun[]`;C-21 的传递依赖元数据缺失只记 check 级 `coverage: partial`,不等同 PASS。 |
|
|
108
|
+
| `security.status=fail` | **仅 security scope**:`DSC-15`/`DSC-16`/`DSC-17` 任一 FAIL。`DSC-21` 永不 FAIL;默认模式 FAIL 封顶 WARN,不会出现 fail。 |
|
|
109
|
+
| `security.status=warn` | 无 FAIL,但有 `DSC-21` 或 15/16/17 的 WARN,或 18/19/20/22 的 advisory WARN。 |
|
|
110
|
+
| `security.status=incomplete` | **security scope 下** `DSC-15`/`DSC-16`/`DSC-17`/`DSC-21` 任一结果不在 `PASS`/`WARN`/`FAIL`(即 `NA`/`ERROR`/`not-run`)。 |
|
|
111
|
+
| 与 DS-C 关系 | **独立**:security FAIL 不撤销 DS-C PASS;DS-C PASS 不证明 security 通过;反之亦然。 |
|
|
112
|
+
| 徽章 | **不得**发 security 徽章;市场只透传 `security.status`、`security.checks[]` 与 `security.notRun[]`,不按安全分排序/筛选。 |
|
|
113
|
+
|
|
114
|
+
严重度以 `spec/dsc-clauses.json` 的 `mandatory`/`advisory` 与 31 §2 C-15~C-22 为准:`DSC-15/16/17` 为 blocking(可 FAIL);`DSC-21` 是强制项但发现封顶 WARN(永不 FAIL,未跑才 `incomplete`);`DSC-18/19/20/22` 为 advisory(`advisory:true`,永不改 verdict)。`security` 块只汇总这 8 项,不与顶层 `coverage` 换算成第二套通过率。
|
|
115
|
+
|
|
116
|
+
## 7. 禁止事项
|
|
96
117
|
|
|
97
118
|
- 不得把 `na`、`notRun`、`error` 或 `partial` 写成 PASS。
|
|
98
119
|
- 不得用 `coverage.pass / total` 当通过率或等级;`total` 含未跑项,且 `mandatory` 随 profile 变。
|
|
99
120
|
- 不得把 `advisory` 计入 `fail` 或据其改变 `exitCode`。
|
|
100
121
|
- 检查级 `coverage` 与报告级 `coverage` 同名不同义;引用时必须写全路径(`checks[].coverage` vs `coverage`)。
|
|
122
|
+
- 不得把 `security.status` 写成安全认证/审计结论;security 的 `pass` 只覆盖这 8 项的静态/元数据证据。
|