dsh-standards 1.0.3 → 1.2.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/README.md +15 -0
- package/40-checker/bin/dsh-flow-collect.mjs +3 -0
- package/40-checker/bin/dsh-levels.mjs +13 -2
- package/40-checker/fixtures/bundle-patch-only/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-patch-only/package.json +12 -0
- package/40-checker/fixtures/c12-command-arg/index.mjs +7 -0
- package/40-checker/fixtures/c12-command-arg/package.json +10 -0
- package/40-checker/fixtures/c12-comment/index.mjs +3 -0
- package/40-checker/fixtures/c12-comment/package.json +10 -0
- package/40-checker/fixtures/c12-compare/index.mjs +3 -0
- package/40-checker/fixtures/c12-compare/package.json +10 -0
- package/40-checker/fixtures/c12-doc-string/index.mjs +13 -0
- package/40-checker/fixtures/c12-doc-string/package.json +10 -0
- package/40-checker/fixtures/c12-grant/index.mjs +3 -0
- package/40-checker/fixtures/c12-grant/package.json +10 -0
- package/40-checker/fixtures/c12-string/index.mjs +3 -0
- package/40-checker/fixtures/c12-string/package.json +10 -0
- package/40-checker/fixtures/declaration-file/index.d.ts +1 -0
- package/40-checker/fixtures/declaration-file/index.mjs +9 -0
- package/40-checker/fixtures/declaration-file/package.json +10 -0
- package/40-checker/fixtures/import-local-side-effect/index.mjs +11 -0
- package/40-checker/fixtures/import-local-side-effect/package.json +21 -0
- package/40-checker/fixtures/import-local-side-effect/side-effectful.mjs +5 -0
- package/40-checker/fixtures/import-open-readonly/index.mjs +14 -0
- package/40-checker/fixtures/import-open-readonly/package.json +19 -0
- package/40-checker/fixtures/import-package-only/index.mjs +11 -0
- package/40-checker/fixtures/import-package-only/package.json +21 -0
- package/40-checker/fixtures/import-parse-error/index.mjs +5 -0
- package/40-checker/fixtures/import-parse-error/package.json +10 -0
- package/40-checker/fixtures/import-side-effect/index.mjs +14 -0
- package/40-checker/fixtures/import-side-effect/package.json +19 -0
- package/40-checker/fixtures/import-side-effect-nonentry/index.mjs +9 -0
- package/40-checker/fixtures/import-side-effect-nonentry/package.json +19 -0
- package/40-checker/fixtures/import-side-effect-nonentry/selfcheck.mjs +8 -0
- package/40-checker/fixtures/import-void-main/index.mjs +16 -0
- package/40-checker/fixtures/import-void-main/package.json +19 -0
- package/40-checker/fixtures/no-source/package.json +19 -0
- package/40-checker/fixtures/parse-error-nonentry/index.mjs +2 -0
- package/40-checker/fixtures/parse-error-nonentry/package.json +10 -0
- package/40-checker/fixtures/parse-error-nonentry/references/broken.js +1 -0
- package/40-checker/scripts/bundle-smoke.mjs +3 -0
- package/40-checker/scripts/entry18-ci.mjs +4 -3
- package/40-checker/scripts/entry18-web.mjs +4 -1
- package/40-checker/scripts/flow-collect.mjs +494 -0
- package/40-checker/scripts/flow-sample-ci.mjs +4 -4
- package/40-checker/scripts/host-farm.mjs +2 -1
- package/40-checker/scripts/journey-config-observe.mjs +5 -3
- package/40-checker/scripts/journey-runtime.mjs +208 -83
- package/40-checker/scripts/level-audit.mjs +91 -19
- package/40-checker/scripts/m2-runner.mjs +4 -3
- package/40-checker/scripts/runtime-child.mjs +4 -0
- package/40-checker/src/cli.mjs +11 -3
- package/40-checker/src/declarations.mjs +514 -19
- package/40-checker/src/flow-evidence.mjs +431 -0
- package/40-checker/src/index.mjs +3 -0
- package/40-checker/src/manifest.mjs +5 -0
- package/40-checker/src/remediation.mjs +132 -0
- package/40-checker/src/report.mjs +19 -2
- package/40-checker/src/sandbox-env.mjs +78 -0
- package/40-checker/test/checks.test.mjs +243 -1
- package/40-checker/test/flow-evidence.test.mjs +295 -0
- package/40-checker/test/remediation.test.mjs +300 -0
- package/40-checker/vendor/babel/README.md +10 -0
- package/40-checker/vendor/babel/index.cjs +3 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/LICENSE +22 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/README.md +19 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/lib/index.d.ts +41 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/lib/index.js +286 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/package.json +31 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/LICENSE +22 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/README.md +19 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/identifier-browser.js +62 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/identifier.js +60 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/index.d.ts +28 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/index.js +27 -0
- package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/package.json +45 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/CHANGELOG.md +1073 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/LICENSE +19 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/README.md +19 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/bin/babel-parser.js +14 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/lib/index.d.ts +1047 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/lib/index.js +14612 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/package.json +57 -0
- package/40-checker/vendor/babel/node_modules/@babel/parser/typings/babel-parser.d.ts +636 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/LICENSE +22 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/README.md +19 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/lib/index-legacy.d.ts +2765 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/lib/index.d.ts +3668 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/lib/index.js +10758 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/lib/index.js.flow +2618 -0
- package/40-checker/vendor/babel/node_modules/@babel/types/package.json +38 -0
- package/CHANGELOG.md +66 -0
- package/bin/dsh-flow-collect.mjs +4 -0
- package/package.json +2 -1
- package/spec/README.md +2 -0
- package/spec/flow/README.md +23 -2
- package/spec/flow-evidence.schema.json +217 -0
- package/spec/levels.json +2 -0
- package/spec/remediation.json +478 -0
package/40-checker/src/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { runManifestChecks, DEFAULT_RUNTIME_VERSION } from './manifest.mjs'
|
|
|
5
5
|
import { checkCapabilities, checkCredentials, checkImportTimeAdvisory, checkLabels } from './declarations.mjs'
|
|
6
6
|
import { runRuntimeChecks } from './runtime.mjs'
|
|
7
7
|
import { spawn, spawnSync } from 'node:child_process'
|
|
8
|
+
import { sanitizedEnv } from './sandbox-env.mjs'
|
|
8
9
|
import { readSources } from './util.mjs'
|
|
9
10
|
import { buildReport, writeReport } from './report.mjs'
|
|
10
11
|
|
|
@@ -40,6 +41,8 @@ Options:
|
|
|
40
41
|
--web <file> optional web-app cordis.patch.yml for bundle smoke
|
|
41
42
|
--bare <dir> optional bare-module base for bundle smoke (host farm)
|
|
42
43
|
--out <file> write the report to a file (stdout otherwise)
|
|
44
|
+
--advice include remediation advice in the report (default: on)
|
|
45
|
+
--no-advice omit the additive advice[] field
|
|
43
46
|
--help show this help
|
|
44
47
|
--version show the checker version
|
|
45
48
|
|
|
@@ -75,6 +78,7 @@ function parseArgs(argv) {
|
|
|
75
78
|
web: null,
|
|
76
79
|
bare: null,
|
|
77
80
|
out: null,
|
|
81
|
+
advice: true,
|
|
78
82
|
help: false,
|
|
79
83
|
version: false,
|
|
80
84
|
}
|
|
@@ -117,6 +121,8 @@ function parseArgs(argv) {
|
|
|
117
121
|
case '--base': options.base = value(); break
|
|
118
122
|
case '--web': options.web = value(); break
|
|
119
123
|
case '--bare': options.bare = value(); break
|
|
124
|
+
case '--advice': options.advice = inline === undefined ? true : inline !== 'false'; break
|
|
125
|
+
case '--no-advice': options.advice = false; break
|
|
120
126
|
default: throw new Error(`unknown option: ${key}`)
|
|
121
127
|
}
|
|
122
128
|
}
|
|
@@ -152,7 +158,8 @@ function runtimeNotRun(reason) {
|
|
|
152
158
|
function isolatedRuntimeChecks(input) {
|
|
153
159
|
return new Promise((resolve) => {
|
|
154
160
|
const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'runtime-child.mjs')
|
|
155
|
-
|
|
161
|
+
// The child imports the inspected package: never inherit the caller's env.
|
|
162
|
+
const child = spawn(process.execPath, [script], { stdio: ['pipe', 'pipe', 'pipe'], env: sanitizedEnv() })
|
|
156
163
|
let out = ''
|
|
157
164
|
let err = ''
|
|
158
165
|
const timer = setTimeout(() => { try { child.kill('SIGKILL') } catch {} }, input.timeoutMs + 3000)
|
|
@@ -198,7 +205,7 @@ async function entryRunCheck(kind, runtimeChecks, options, dir) {
|
|
|
198
205
|
if (options.base) args.push('--base', options.base)
|
|
199
206
|
if (options.web) args.push('--web', options.web)
|
|
200
207
|
if (options.bare) args.push('--bare', options.bare)
|
|
201
|
-
const run = spawnSync(process.execPath, args, { encoding: 'utf8', timeout: options.timeoutMs + 5000, maxBuffer: 8 * 1024 * 1024 })
|
|
208
|
+
const run = spawnSync(process.execPath, args, { encoding: 'utf8', timeout: options.timeoutMs + 5000, maxBuffer: 8 * 1024 * 1024, env: sanitizedEnv() })
|
|
202
209
|
if (run.error) return { ...base, result: 'ERROR', coverage: 'not-run', reason: `bundle smoke failed to start: ${run.error.message}` }
|
|
203
210
|
let smoke
|
|
204
211
|
try { smoke = JSON.parse(run.stdout) } catch { return { ...base, result: 'ERROR', coverage: 'not-run', reason: `bundle smoke produced no JSON: ${(run.stderr || '').slice(0, 300)}` } }
|
|
@@ -273,7 +280,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
273
280
|
{ id: 'DSC-13', clause: 'C-13', layer: 'L0', result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [], method: 'M', advisory: false },
|
|
274
281
|
]
|
|
275
282
|
const declarationAdjusted = declarationChecks.map(item => (item.result === 'WARN' && ['DSC-11', 'DSC-13'].includes(item.id) ? { ...item, advisory: true } : item))
|
|
276
|
-
const advisoryChecks = [checkImportTimeAdvisory(sources)]
|
|
283
|
+
const advisoryChecks = [checkImportTimeAdvisory(sources, entry?.relative)]
|
|
277
284
|
|
|
278
285
|
if (options.scope === 'entry' && options.entryRuntime === 'server-entry' && kind !== 'plugin') options.treatAsPlugin = true
|
|
279
286
|
let runtimeKind = kind
|
|
@@ -343,6 +350,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
343
350
|
staticOnly: options.staticOnly,
|
|
344
351
|
entryRuntime: options.entryRuntime,
|
|
345
352
|
entryClaim: options.entryClaim,
|
|
353
|
+
advice: options.advice,
|
|
346
354
|
})
|
|
347
355
|
const output = writeReport(report, options.format)
|
|
348
356
|
if (options.out) {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
|
|
4
|
+
const requireVendor = createRequire(import.meta.url)
|
|
5
|
+
const { parse: parseBabel } = requireVendor('../vendor/babel/index.cjs')
|
|
1
6
|
import { isObject, regexHits } from './util.mjs'
|
|
2
7
|
import { check, detail, worst } from './result.mjs'
|
|
3
8
|
import { validateCommitments, validateDisclosure } from './permissions.mjs'
|
|
@@ -47,33 +52,509 @@ function labelList(manifest) {
|
|
|
47
52
|
return { defined: true, list: value, error: null }
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// AST layer (vendored @babel/parser, no runtime dependency)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function parsePlugins(relative) {
|
|
61
|
+
const extension = path.extname(String(relative || '')).toLowerCase()
|
|
62
|
+
const plugins = ['estree', 'decorators-legacy', 'importAttributes']
|
|
63
|
+
if (extension === '.ts' || extension === '.tsx') plugins.push('typescript')
|
|
64
|
+
if (extension !== '.ts' && extension !== '.tsx') plugins.push('jsx')
|
|
65
|
+
if (extension === '.tsx') plugins.push('jsx')
|
|
66
|
+
return plugins
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const SOURCE_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx']
|
|
70
|
+
const FUNCTION_NODES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])
|
|
71
|
+
const TRANSPARENT_NODES = new Set([
|
|
72
|
+
'TemplateLiteral', 'TemplateElement', 'BinaryExpression', 'LogicalExpression',
|
|
73
|
+
'ConditionalExpression', 'ParenthesizedExpression', 'ChainExpression',
|
|
74
|
+
])
|
|
75
|
+
const EQUALITY_OPERATORS = new Set(['==', '===', '!=', '!=='])
|
|
76
|
+
const GRANT_METHODS = new Set(['set', 'resolve'])
|
|
77
|
+
const CONSOLE_METHODS = new Set(['log', 'info', 'warn', 'error', 'debug'])
|
|
78
|
+
const DOC_PROPERTY_KEYS = new Set(['desc', 'description', 'message', 'reason', 'help', 'label', 'title', 'comment'])
|
|
79
|
+
const COMMAND_PROPERTY_KEYS = new Set(['mode', 'sandboxPolicy', 'sandbox_permissions'])
|
|
80
|
+
const SPAWN_ARG_METHODS = new Set(['spawn', 'spawnSync', 'exec', 'execSync', 'execFile', 'execFileSync'])
|
|
81
|
+
const FS_WRITE_METHODS = new Set([
|
|
82
|
+
'writeFileSync', 'appendFileSync', 'mkdirSync', 'rmSync', 'unlinkSync', 'rmdirSync', 'renameSync',
|
|
83
|
+
'copyFileSync', 'chmodSync', 'symlinkSync', 'truncateSync', 'writeSync',
|
|
84
|
+
])
|
|
85
|
+
const C10_SUBPROCESS_METHODS = new Set(['execSync', 'execFileSync', 'spawnSync', 'execFile', 'spawn'])
|
|
86
|
+
|
|
87
|
+
const PARSE_CACHE = new WeakMap()
|
|
88
|
+
|
|
89
|
+
function parseSource(source) {
|
|
90
|
+
const cached = PARSE_CACHE.get(source)
|
|
91
|
+
if (cached) return cached
|
|
92
|
+
const record = { ast: null, error: null }
|
|
93
|
+
try {
|
|
94
|
+
const parsed = parseBabel(source.text, {
|
|
95
|
+
sourceType: 'module',
|
|
96
|
+
allowHashBang: true,
|
|
97
|
+
plugins: parsePlugins(source.relative),
|
|
98
|
+
})
|
|
99
|
+
record.ast = parsed.program ?? parsed
|
|
100
|
+
} catch (error) {
|
|
101
|
+
record.error = {
|
|
102
|
+
message: String(error?.message ?? error),
|
|
103
|
+
line: Number.isInteger(error?.loc?.line) && error.loc.line > 0 ? error.loc.line : 1,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
PARSE_CACHE.set(source, record)
|
|
107
|
+
return record
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeRelative(relative) {
|
|
111
|
+
return String(relative ?? '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Declaration files have no runtime behavior; parsing them for import-time
|
|
115
|
+
// side effects or execution strings only creates noise.
|
|
116
|
+
function isDeclarationFile(relative) {
|
|
117
|
+
return /\.d\.[cm]?ts$/i.test(String(relative ?? ''))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function identifierName(node) {
|
|
121
|
+
return node?.type === 'Identifier' ? node.name : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function memberPropertyName(node) {
|
|
125
|
+
if (!node || node.type !== 'MemberExpression') return null
|
|
126
|
+
if (!node.computed && node.property?.type === 'Identifier') return node.property.name
|
|
127
|
+
if (node.property?.type === 'Literal' && typeof node.property.value === 'string') return node.property.value
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function propertyKeyName(property) {
|
|
132
|
+
if (!property.computed && property.key?.type === 'Identifier') return property.key.name
|
|
133
|
+
if (property.key?.type === 'Literal' && typeof property.key.value === 'string') return property.key.value
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function calledFunctionName(call) {
|
|
138
|
+
if (!call || call.type !== 'CallExpression') return null
|
|
139
|
+
if (call.callee.type === 'Identifier') return call.callee.name
|
|
140
|
+
if (call.callee.type === 'MemberExpression') return memberPropertyName(call.callee)
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function walkAst(root, visit) {
|
|
145
|
+
const ancestors = []
|
|
146
|
+
const walkNode = (node) => {
|
|
147
|
+
if (!node || typeof node.type !== 'string') return
|
|
148
|
+
visit(node, ancestors)
|
|
149
|
+
ancestors.push(node)
|
|
150
|
+
for (const key of Object.keys(node)) {
|
|
151
|
+
if (key === 'loc') continue
|
|
152
|
+
const value = node[key]
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (const child of value) if (child && typeof child.type === 'string') walkNode(child)
|
|
155
|
+
} else if (value && typeof value.type === 'string') {
|
|
156
|
+
walkNode(value)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
ancestors.pop()
|
|
160
|
+
}
|
|
161
|
+
walkNode(root)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Module scope means "runs while the module is evaluated". Function bodies and
|
|
165
|
+
// class method bodies are skipped; top-level if/try/loop bodies are not.
|
|
166
|
+
function walkModuleScope(ast, visit) {
|
|
167
|
+
const ancestors = []
|
|
168
|
+
const walkNode = (node) => {
|
|
169
|
+
if (!node || typeof node.type !== 'string') return
|
|
170
|
+
if (FUNCTION_NODES.has(node.type) || node.type === 'MethodDefinition') return
|
|
171
|
+
visit(node, ancestors)
|
|
172
|
+
ancestors.push(node)
|
|
173
|
+
for (const key of Object.keys(node)) {
|
|
174
|
+
if (key === 'loc') continue
|
|
175
|
+
const value = node[key]
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
for (const child of value) if (child && typeof child.type === 'string') walkNode(child)
|
|
178
|
+
} else if (value && typeof value.type === 'string') {
|
|
179
|
+
walkNode(value)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
ancestors.pop()
|
|
183
|
+
}
|
|
184
|
+
for (const statement of ast.body) walkNode(statement)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function callArgumentContext(ancestors, node, predicate) {
|
|
188
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
189
|
+
const ancestor = ancestors[index]
|
|
190
|
+
if (ancestor.type !== 'CallExpression' || !predicate(ancestor)) continue
|
|
191
|
+
const child = index + 1 < ancestors.length ? ancestors[index + 1] : node
|
|
192
|
+
if (ancestor.arguments.includes(child)) return ancestor
|
|
193
|
+
}
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isEqualityOperand(ancestors) {
|
|
198
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
199
|
+
const ancestor = ancestors[index]
|
|
200
|
+
if (ancestor.type === 'BinaryExpression' && EQUALITY_OPERATORS.has(ancestor.operator)) return true
|
|
201
|
+
if (!TRANSPARENT_NODES.has(ancestor.type)) return false
|
|
202
|
+
}
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isGrantCallArgument(ancestors, node) {
|
|
207
|
+
return Boolean(callArgumentContext(ancestors, node, (call) => (
|
|
208
|
+
call.callee.type === 'MemberExpression' && GRANT_METHODS.has(memberPropertyName(call.callee))
|
|
209
|
+
)))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isConsoleArgument(ancestors, node) {
|
|
213
|
+
return Boolean(callArgumentContext(ancestors, node, (call) => (
|
|
214
|
+
call.callee.type === 'MemberExpression'
|
|
215
|
+
&& identifierName(call.callee.object) === 'console'
|
|
216
|
+
&& CONSOLE_METHODS.has(memberPropertyName(call.callee))
|
|
217
|
+
)))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isSpawnArrayElement(ancestors, node) {
|
|
221
|
+
for (let index = 0; index < ancestors.length; index += 1) {
|
|
222
|
+
if (ancestors[index].type !== 'ArrayExpression') continue
|
|
223
|
+
for (let outer = index + 1; outer < ancestors.length; outer += 1) {
|
|
224
|
+
const call = ancestors[outer]
|
|
225
|
+
if (call.type !== 'CallExpression') continue
|
|
226
|
+
const child = outer + 1 < ancestors.length ? ancestors[outer + 1] : node
|
|
227
|
+
if (call.arguments.includes(child) && SPAWN_ARG_METHODS.has(calledFunctionName(call))) return true
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isSwitchCaseValue(ancestors, node) {
|
|
234
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
235
|
+
const ancestor = ancestors[index]
|
|
236
|
+
if (ancestor.type === 'SwitchCase') {
|
|
237
|
+
const child = index + 1 < ancestors.length ? ancestors[index + 1] : node
|
|
238
|
+
return ancestor.test === child
|
|
239
|
+
}
|
|
240
|
+
if (!TRANSPARENT_NODES.has(ancestor.type)) return false
|
|
241
|
+
}
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function propertyContext(ancestors, node) {
|
|
246
|
+
let child = node
|
|
247
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
248
|
+
const ancestor = ancestors[index]
|
|
249
|
+
if (ancestor.type === 'Property') {
|
|
250
|
+
if (ancestor.value !== child) return null
|
|
251
|
+
return { key: propertyKeyName(ancestor) }
|
|
252
|
+
}
|
|
253
|
+
if (ancestor.type === 'ObjectExpression' || ancestor.type === 'ArrayExpression' || TRANSPARENT_NODES.has(ancestor.type)) {
|
|
254
|
+
child = ancestor
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function hasCommandProperty(ancestors, node) {
|
|
263
|
+
let child = node
|
|
264
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
265
|
+
const ancestor = ancestors[index]
|
|
266
|
+
if (ancestor.type === 'Property') {
|
|
267
|
+
if (ancestor.value !== child) return false
|
|
268
|
+
if (COMMAND_PROPERTY_KEYS.has(propertyKeyName(ancestor))) return true
|
|
269
|
+
child = ancestor
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
if (ancestor.type === 'ObjectExpression' || ancestor.type === 'ArrayExpression' || TRANSPARENT_NODES.has(ancestor.type)) {
|
|
273
|
+
child = ancestor
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Classify a string literal. Comments never reach the AST. Equality comparisons
|
|
283
|
+
* and documentation strings are ignored; grant/command contexts and unknown
|
|
284
|
+
* contexts warn (unknown stays conservative).
|
|
285
|
+
*/
|
|
286
|
+
function classifyLiteral(node, ancestors) {
|
|
287
|
+
if (isEqualityOperand(ancestors)) return 'ignore'
|
|
288
|
+
if (isGrantCallArgument(ancestors, node)) return 'command'
|
|
289
|
+
if (isSpawnArrayElement(ancestors, node)) return 'command'
|
|
290
|
+
if (isSwitchCaseValue(ancestors, node)) return 'command'
|
|
291
|
+
if (hasCommandProperty(ancestors, node)) return 'command'
|
|
292
|
+
const property = propertyContext(ancestors, node)
|
|
293
|
+
if (isConsoleArgument(ancestors, node)) return 'doc'
|
|
294
|
+
if (property && DOC_PROPERTY_KEYS.has(property.key)) return 'doc'
|
|
295
|
+
return 'unknown'
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function literalStringValue(node) {
|
|
299
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
300
|
+
if (node.type === 'TemplateElement' && node.value) {
|
|
301
|
+
if (typeof node.value.cooked === 'string') return node.value.cooked
|
|
302
|
+
if (typeof node.value.raw === 'string') return node.value.raw
|
|
303
|
+
}
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function findLiteralHits(sources, values) {
|
|
308
|
+
const wanted = new Set(values)
|
|
309
|
+
const hits = []
|
|
310
|
+
const parseErrors = []
|
|
311
|
+
let parsedCount = 0
|
|
312
|
+
for (const source of sources) {
|
|
313
|
+
if (isDeclarationFile(source.relative)) continue
|
|
314
|
+
const parsed = parseSource(source)
|
|
315
|
+
if (parsed.error) {
|
|
316
|
+
parseErrors.push({ file: source.relative, line: parsed.error.line, message: parsed.error.message })
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
parsedCount += 1
|
|
320
|
+
walkAst(parsed.ast, (node, ancestors) => {
|
|
321
|
+
const value = literalStringValue(node)
|
|
322
|
+
if (value === null || !wanted.has(value)) return
|
|
323
|
+
const context = classifyLiteral(node, ancestors)
|
|
324
|
+
if (context === 'ignore' || context === 'doc') return
|
|
325
|
+
hits.push({ file: source.relative, line: node.loc?.start?.line ?? 1, text: value })
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
return { hits, parseErrors, parsedCount }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function literalCandidates(base) {
|
|
332
|
+
const candidates = [base]
|
|
333
|
+
const extension = path.posix.extname(base)
|
|
334
|
+
if (SOURCE_EXTENSIONS.includes(extension)) {
|
|
335
|
+
const stem = base.slice(0, -extension.length)
|
|
336
|
+
for (const candidate of SOURCE_EXTENSIONS) if (candidate !== extension) candidates.push(`${stem}${candidate}`)
|
|
337
|
+
} else {
|
|
338
|
+
for (const candidate of SOURCE_EXTENSIONS) candidates.push(`${base}${candidate}`)
|
|
339
|
+
}
|
|
340
|
+
for (const candidate of SOURCE_EXTENSIONS) candidates.push(path.posix.join(base, `index${candidate}`))
|
|
341
|
+
return candidates
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveLocalSpecifier(fromRelative, specifier, byRelative) {
|
|
345
|
+
const spec = String(specifier).replace(/\\/g, '/')
|
|
346
|
+
if (!(spec.startsWith('./') || spec.startsWith('../') || spec === '.' || spec === '..')) return { kind: 'package' }
|
|
347
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(normalizeRelative(fromRelative)), spec))
|
|
348
|
+
if (base === '.' || base === '..' || base.startsWith('../') || path.posix.isAbsolute(base)) return { kind: 'unresolved' }
|
|
349
|
+
for (const candidate of literalCandidates(base)) {
|
|
350
|
+
if (byRelative.has(candidate)) return { kind: 'local', relative: candidate }
|
|
351
|
+
}
|
|
352
|
+
return { kind: 'unresolved' }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function collectModuleEdges(ast) {
|
|
356
|
+
const edges = []
|
|
357
|
+
walkAst(ast, (node) => {
|
|
358
|
+
if (node.type === 'ImportDeclaration' || node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') {
|
|
359
|
+
if (node.source?.type === 'Literal' && typeof node.source.value === 'string') {
|
|
360
|
+
edges.push({ specifier: node.source.value, line: node.loc?.start?.line ?? 1 })
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
// require() is only import-time when it runs at module scope; a lazy require
|
|
365
|
+
// inside apply()/a factory must not be treated as part of the import closure.
|
|
366
|
+
walkModuleScope(ast, (node) => {
|
|
367
|
+
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require') {
|
|
368
|
+
const argument = node.arguments[0]
|
|
369
|
+
if (argument?.type === 'Literal' && typeof argument.value === 'string') {
|
|
370
|
+
edges.push({ specifier: argument.value, line: node.loc?.start?.line ?? 1 })
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
return edges
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isSetIntervalCall(call) {
|
|
378
|
+
if (call.callee.type === 'Identifier' && call.callee.name === 'setInterval') return true
|
|
379
|
+
return call.callee.type === 'MemberExpression'
|
|
380
|
+
&& identifierName(call.callee.object) === 'globalThis'
|
|
381
|
+
&& memberPropertyName(call.callee) === 'setInterval'
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isProcessOnCall(call) {
|
|
385
|
+
return call.callee.type === 'MemberExpression'
|
|
386
|
+
&& identifierName(call.callee.object) === 'process'
|
|
387
|
+
&& memberPropertyName(call.callee) === 'on'
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function assignmentTargetKind(node) {
|
|
391
|
+
if (node.left.type !== 'MemberExpression') return null
|
|
392
|
+
const property = memberPropertyName(node.left)
|
|
393
|
+
if (!property) return null
|
|
394
|
+
const object = node.left.object
|
|
395
|
+
if (object.type === 'MemberExpression') {
|
|
396
|
+
return identifierName(object.object) === 'process' && memberPropertyName(object) === 'env' ? 'process.env' : null
|
|
397
|
+
}
|
|
398
|
+
return identifierName(object) === 'globalThis' ? 'globalThis' : null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function collectSideEffectHits(ast, file, hits) {
|
|
402
|
+
const push = (clause, pattern, node) => hits.push({ clause, pattern, file, line: node.loc?.start?.line ?? 1 })
|
|
403
|
+
walkModuleScope(ast, (node) => {
|
|
404
|
+
if (node.type === 'AwaitExpression' || (node.type === 'ForOfStatement' && node.await === true)) {
|
|
405
|
+
push('C-10.1', 'top-level await', node)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
if (node.type === 'UnaryExpression' && node.operator === 'void' && node.argument?.type === 'CallExpression') {
|
|
409
|
+
push('C-10.2', 'top-level immediate main call', node)
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
if (node.type === 'AssignmentExpression') {
|
|
413
|
+
const kind = assignmentTargetKind(node)
|
|
414
|
+
if (kind === 'process.env') push('C-10.2', 'top-level process.env assignment', node)
|
|
415
|
+
else if (kind === 'globalThis') push('C-10.2', 'top-level globalThis assignment', node)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
if (node.type !== 'CallExpression') return
|
|
419
|
+
if (isSetIntervalCall(node)) push('C-10.2', 'top-level setInterval', node)
|
|
420
|
+
if (isProcessOnCall(node)) push('C-10.2', 'top-level process.on', node)
|
|
421
|
+
const name = calledFunctionName(node)
|
|
422
|
+
if (name && FS_WRITE_METHODS.has(name)) push('C-10.4', 'top-level fs mutation', node)
|
|
423
|
+
if (name && C10_SUBPROCESS_METHODS.has(name)) push('C-10.4', 'top-level subprocess spawn', node)
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function analyseImportClosure(sources, entryRelative) {
|
|
428
|
+
const byRelative = new Map()
|
|
429
|
+
for (const source of sources) byRelative.set(normalizeRelative(source.relative), source)
|
|
430
|
+
const queue = []
|
|
431
|
+
if (entryRelative) {
|
|
432
|
+
const entry = normalizeRelative(entryRelative)
|
|
433
|
+
if (!byRelative.has(entry)) return { error: `entry module ${entryRelative} was not found among scanned sources` }
|
|
434
|
+
queue.push(entry)
|
|
435
|
+
} else {
|
|
436
|
+
// Programmatic callers without an entry: keep the legacy "all sources" shape.
|
|
437
|
+
for (const relative of byRelative.keys()) queue.push(relative)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const visited = new Set()
|
|
441
|
+
const hits = []
|
|
442
|
+
const parseErrors = []
|
|
443
|
+
const coverage = []
|
|
444
|
+
const seenCoverage = new Set()
|
|
445
|
+
const localEdges = []
|
|
446
|
+
const addCoverage = (key, record) => {
|
|
447
|
+
if (seenCoverage.has(key)) return
|
|
448
|
+
seenCoverage.add(key)
|
|
449
|
+
coverage.push(record)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
while (queue.length > 0) {
|
|
453
|
+
const relative = queue.shift()
|
|
454
|
+
if (visited.has(relative)) continue
|
|
455
|
+
visited.add(relative)
|
|
456
|
+
if (isDeclarationFile(relative)) {
|
|
457
|
+
addCoverage(`declaration:${relative}`, detail('C-10.3', 'NA', `not-run: declaration file ${relative} has no runtime behavior`, [relative]))
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
const parsed = parseSource(byRelative.get(relative))
|
|
461
|
+
if (parsed.error) {
|
|
462
|
+
parseErrors.push({ file: relative, line: parsed.error.line, message: parsed.error.message })
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
collectSideEffectHits(parsed.ast, relative, hits)
|
|
466
|
+
for (const edge of collectModuleEdges(parsed.ast)) {
|
|
467
|
+
const resolution = resolveLocalSpecifier(relative, edge.specifier, byRelative)
|
|
468
|
+
if (resolution.kind === 'local') {
|
|
469
|
+
localEdges.push({ from: relative, to: resolution.relative, line: edge.line })
|
|
470
|
+
queue.push(resolution.relative)
|
|
471
|
+
} else if (resolution.kind === 'package') {
|
|
472
|
+
addCoverage(`package:${edge.specifier}`, detail('C-10.3', 'NA', `not-run: package import '${edge.specifier}' from ${relative}:${edge.line} is outside the local closure`, [`${relative}:${edge.line}`]))
|
|
473
|
+
} else {
|
|
474
|
+
addCoverage(`unresolved:${relative}:${edge.specifier}`, detail('C-10.3', 'NA', `not-run: local import '${edge.specifier}' from ${relative}:${edge.line} could not be resolved`, [`${relative}:${edge.line}`]))
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { visited, hits, parseErrors, coverage, localEdges }
|
|
480
|
+
}
|
|
481
|
+
|
|
50
482
|
function hitEvidence(hits, limit = 3) {
|
|
51
483
|
return hits.slice(0, limit).map(hit => `${hit.file}:${hit.line}`)
|
|
52
484
|
}
|
|
53
485
|
|
|
54
486
|
/** DSC-10: advisory only; never FAIL. */
|
|
55
|
-
export function checkImportTimeAdvisory(sources) {
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
487
|
+
export function checkImportTimeAdvisory(sources, entryRelative = null) {
|
|
488
|
+
if (sources.length === 0) {
|
|
489
|
+
return check({
|
|
490
|
+
id: 'DSC-10',
|
|
491
|
+
clause: 'C-10',
|
|
492
|
+
layer: 'L0',
|
|
493
|
+
result: 'NA',
|
|
494
|
+
advisory: true,
|
|
495
|
+
coverage: 'not-run',
|
|
496
|
+
reason: 'no source files scanned; C-10 not-run',
|
|
497
|
+
details: [],
|
|
498
|
+
evidence: [],
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const analysis = analyseImportClosure(sources, entryRelative)
|
|
503
|
+
if (analysis.error) {
|
|
504
|
+
const evidence = normalizeRelative(entryRelative)
|
|
505
|
+
return check({
|
|
506
|
+
id: 'DSC-10',
|
|
507
|
+
clause: 'C-10',
|
|
508
|
+
layer: 'L0',
|
|
509
|
+
result: 'ERROR',
|
|
510
|
+
advisory: true,
|
|
511
|
+
coverage: 'not-run',
|
|
512
|
+
reason: `${analysis.error}; C-10 not-run`,
|
|
513
|
+
details: [detail('C-10', 'ERROR', `not-run: ${analysis.error}`, [evidence])],
|
|
514
|
+
evidence: [evidence],
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const { visited, hits, parseErrors, coverage, localEdges } = analysis
|
|
519
|
+
const details = hits.slice(0, 20).map(hit => detail(hit.clause, 'WARN', `${hit.pattern} at ${hit.file}:${hit.line}`, [`${hit.file}:${hit.line}`]))
|
|
520
|
+
details.push(...coverage.slice(0, 10))
|
|
521
|
+
if (localEdges.length > 0) {
|
|
522
|
+
details.push(detail('C-10.3', 'PASS', `local relative import closure analysed (${visited.size} module(s))`, [...visited].slice(0, 5)))
|
|
523
|
+
}
|
|
524
|
+
for (const error of parseErrors) {
|
|
525
|
+
details.push(detail('C-10', 'ERROR', `not-run: AST parse failed in ${error.file}:${error.line} (${error.message})`, [`${error.file}:${error.line}`]))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let result
|
|
529
|
+
let reason
|
|
530
|
+
if (parseErrors.length > 0) {
|
|
531
|
+
result = 'ERROR'
|
|
532
|
+
reason = `AST parse failed in ${parseErrors.length} file(s); C-10 not-run`
|
|
533
|
+
if (hits.length > 0) reason += `; ${hits.length} side-effect clue(s) found before the gap`
|
|
534
|
+
} else if (hits.length > 0) {
|
|
535
|
+
result = 'WARN'
|
|
536
|
+
reason = `advisory: ${hits.length} import-time side-effect clue(s)`
|
|
537
|
+
} else {
|
|
538
|
+
result = 'PASS'
|
|
539
|
+
reason = ''
|
|
66
540
|
}
|
|
67
|
-
|
|
541
|
+
|
|
542
|
+
const evidence = hits.length > 0
|
|
543
|
+
? hitEvidence(hits)
|
|
544
|
+
: parseErrors.length > 0
|
|
545
|
+
? hitEvidence(parseErrors)
|
|
546
|
+
: coverage.flatMap(item => item.evidence).slice(0, 3)
|
|
547
|
+
|
|
68
548
|
return check({
|
|
69
549
|
id: 'DSC-10',
|
|
70
550
|
clause: 'C-10',
|
|
71
551
|
layer: 'L0',
|
|
72
552
|
result,
|
|
73
553
|
advisory: true,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
554
|
+
coverage: result === 'ERROR' ? 'not-run' : 'full',
|
|
555
|
+
reason,
|
|
556
|
+
details,
|
|
557
|
+
evidence,
|
|
77
558
|
})
|
|
78
559
|
}
|
|
79
560
|
|
|
@@ -116,13 +597,20 @@ export function checkCapabilities(manifest, sources, patch) {
|
|
|
116
597
|
if (patch?.enabledPluginManagerTools) {
|
|
117
598
|
details.push(detail('C-11.3', 'FAIL', 'bundle patch enables @deepseek-ai/dsh-plugin-manager/tools', ['dsh.bundle.patch']))
|
|
118
599
|
}
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
600
|
+
const dangerScan = findLiteralHits(sources, ['danger-full-access'])
|
|
601
|
+
if (dangerScan.hits.length > 0) {
|
|
602
|
+
details.push(detail('C-11.4', 'WARN', `danger-full-access literal in ${dangerScan.hits.length} execution context(s)`, hitEvidence(dangerScan.hits)))
|
|
603
|
+
}
|
|
604
|
+
const dangerUnparsableAll = dangerScan.parseErrors.length > 0 && dangerScan.parsedCount === 0
|
|
605
|
+
if (dangerScan.parseErrors.length > 0) {
|
|
606
|
+
details.push(detail('C-11.4', dangerUnparsableAll ? 'ERROR' : 'NA', `not-run: ${dangerScan.parseErrors.length} source file(s) could not be parsed for string literals (${dangerScan.parseErrors[0].file}:${dangerScan.parseErrors[0].line})`, hitEvidence(dangerScan.parseErrors)))
|
|
607
|
+
}
|
|
121
608
|
if (details.length === 0) details.push(detail('C-11.1', 'PASS', 'dsh.capabilities is declared and consistent with static triggers'))
|
|
122
609
|
|
|
123
610
|
const result = worst(details.map(item => item.result))
|
|
124
611
|
return check({
|
|
125
612
|
id: 'DSC-11', clause: 'C-11', layer: 'L0', result,
|
|
613
|
+
coverage: dangerUnparsableAll ? 'not-run' : dangerScan.parseErrors.length > 0 ? 'partial' : 'full',
|
|
126
614
|
reason: details.filter(item => item.result !== 'PASS').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
127
615
|
details,
|
|
128
616
|
evidence: ['package.json#dsh.capabilities', 'dsh.bundle.patch'],
|
|
@@ -171,13 +659,20 @@ export function checkCredentials(manifest, sources) {
|
|
|
171
659
|
|
|
172
660
|
const providerHits = regexHits(sources, /ctx\.provide\(\s*['"](?:shell|credentials|sandboxPolicy|sandbox|deepseekAccount)['"]/g)
|
|
173
661
|
if (providerHits.length > 0) details.push(detail('C-12.3', 'WARN', 'global seam provider registration detected; declaration ownership needs review', hitEvidence(providerHits)))
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
662
|
+
const literalScan = findLiteralHits(sources, ['danger-full-access', '--add-dir'])
|
|
663
|
+
if (literalScan.hits.length > 0) {
|
|
664
|
+
details.push(detail('C-12.4', 'WARN', `danger-full-access/--add-dir literal in ${literalScan.hits.length} execution context(s)`, hitEvidence(literalScan.hits)))
|
|
665
|
+
}
|
|
666
|
+
const literalUnparsableAll = literalScan.parseErrors.length > 0 && literalScan.parsedCount === 0
|
|
667
|
+
if (literalScan.parseErrors.length > 0) {
|
|
668
|
+
details.push(detail('C-12.4', literalUnparsableAll ? 'ERROR' : 'NA', `not-run: ${literalScan.parseErrors.length} source file(s) could not be parsed for string literals (${literalScan.parseErrors[0].file}:${literalScan.parseErrors[0].line})`, hitEvidence(literalScan.parseErrors)))
|
|
669
|
+
}
|
|
176
670
|
details.push(...validateCommitments(manifest), ...validateDisclosure(manifest))
|
|
177
671
|
|
|
178
672
|
const result = worst(details.map(item => item.result))
|
|
179
673
|
return check({
|
|
180
674
|
id: 'DSC-12', clause: 'C-12', layer: 'L0', result,
|
|
675
|
+
coverage: literalUnparsableAll ? 'not-run' : literalScan.parseErrors.length > 0 ? 'partial' : 'full',
|
|
181
676
|
reason: details.filter(item => item.result !== 'PASS' && item.result !== 'NA').map(item => `${item.item}: ${item.reason}`).join(' | '),
|
|
182
677
|
details,
|
|
183
678
|
evidence: ['package.json#dsh.capabilities'],
|