dsh-standards 1.5.0 → 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/src/cli.mjs +8 -3
- package/40-checker/src/declarations.mjs +10 -10
- package/40-checker/src/index.mjs +2 -1
- package/40-checker/src/report.mjs +68 -2
- package/40-checker/src/security.mjs +1065 -0
- package/40-checker/src/util.mjs +53 -0
- package/CHANGELOG.md +14 -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 +75 -2
package/40-checker/src/cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { runManifestChecks, DEFAULT_RUNTIME_VERSION } from './manifest.mjs'
|
|
6
6
|
import { checkCapabilities, checkCredentials, checkImportTimeAdvisory, checkLabels } from './declarations.mjs'
|
|
7
|
+
import { runSecurityChecks } from './security.mjs'
|
|
7
8
|
import { runRuntimeChecks } from './runtime.mjs'
|
|
8
9
|
import { runStaticBundleChecks } from './bundle-static.mjs'
|
|
9
10
|
import { spawn, spawnSync } from 'node:child_process'
|
|
@@ -32,7 +33,8 @@ Usage:
|
|
|
32
33
|
|
|
33
34
|
Options:
|
|
34
35
|
--format text|json|sarif output format (default: text)
|
|
35
|
-
--scope mvp|ds-c|entry
|
|
36
|
+
--scope mvp|ds-c|entry|security mvp = 8 MVP ids; ds-c = full claim (default); entry = compatibility gate;
|
|
37
|
+
security = DSC-15/16/17/21 are blocking, DSC-18/19/20/22 advisory
|
|
36
38
|
--profile package|listing|both evaluation profile (default: package; listing if --market-index is set)
|
|
37
39
|
--runtime [path|auto] built @deepseek-ai/cordis lib/index.js (or its package dir); bare or
|
|
38
40
|
'auto' resolves from the installed dsh; enables real L1-L3
|
|
@@ -161,10 +163,10 @@ function parseArgs(argv) {
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
if (!['text', 'json', 'sarif'].includes(options.format)) throw new Error(`unsupported --format: ${options.format}`)
|
|
164
|
-
if (!['mvp', 'ds-c', 'entry'].includes(options.scope)) throw new Error(`unsupported --scope: ${options.scope}`)
|
|
166
|
+
if (!['mvp', 'ds-c', 'entry', 'security'].includes(options.scope)) throw new Error(`unsupported --scope: ${options.scope}`)
|
|
165
167
|
if (!['require', 'server-entry'].includes(options.entryRuntime)) throw new Error('--entry-runtime must be require or server-entry')
|
|
166
168
|
if (!['runtime', 'static'].includes(options.entryClaim)) throw new Error('--entry-claim must be runtime or static')
|
|
167
|
-
if (options.staticOnly) { options.scope = 'ds-c'; options.profile = 'listing' }
|
|
169
|
+
if (options.staticOnly) { if (options.scope !== 'security') options.scope = 'ds-c'; options.profile = 'listing' }
|
|
168
170
|
if (!options.profile) options.profile = options.marketIndex ? 'listing' : 'package'
|
|
169
171
|
if (!['package', 'listing', 'both'].includes(options.profile)) throw new Error(`unsupported --profile: ${options.profile}`)
|
|
170
172
|
if (options.staticOnly && options.treatAsPlugin) throw new Error('--static-only and --treat-as-plugin are mutually exclusive')
|
|
@@ -602,6 +604,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
602
604
|
]
|
|
603
605
|
const declarationAdjusted = declarationChecks.map(item => (item.result === 'WARN' && ['DSC-11', 'DSC-13'].includes(item.id) ? { ...item, advisory: true } : item))
|
|
604
606
|
const advisoryChecks = [checkImportTimeAdvisory(sources, entry?.relative, scan)]
|
|
607
|
+
const securityChecks = runSecurityChecks({ dir, manifest, sources, entry, kind })
|
|
605
608
|
|
|
606
609
|
if (options.scope === 'entry' && options.entryRuntime === 'server-entry' && kind !== 'plugin') options.treatAsPlugin = true
|
|
607
610
|
let runtimeKind = kind
|
|
@@ -721,6 +724,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
721
724
|
runCheck,
|
|
722
725
|
advisoryChecks[0],
|
|
723
726
|
...declarationAdjusted,
|
|
727
|
+
...(options.scope === 'security' ? securityChecks : []),
|
|
724
728
|
]
|
|
725
729
|
|
|
726
730
|
if (g4?.result === 'FAIL') {
|
|
@@ -756,6 +760,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
756
760
|
advice: options.advice,
|
|
757
761
|
declarations: declarationsOf(manifest),
|
|
758
762
|
runtimeChecks: runtimeSummary,
|
|
763
|
+
securityChecks,
|
|
759
764
|
})
|
|
760
765
|
// An explicitly requested runtime/host run that could not produce a result
|
|
761
766
|
// is not a clean static pass: the caller asked for execution, so say the run
|
|
@@ -86,7 +86,7 @@ const C10_SUBPROCESS_METHODS = new Set(['execSync', 'execFileSync', 'spawnSync',
|
|
|
86
86
|
|
|
87
87
|
const PARSE_CACHE = new WeakMap()
|
|
88
88
|
|
|
89
|
-
function parseSource(source) {
|
|
89
|
+
export function parseSource(source) {
|
|
90
90
|
const cached = PARSE_CACHE.get(source)
|
|
91
91
|
if (cached) return cached
|
|
92
92
|
const record = { ast: null, error: null }
|
|
@@ -107,21 +107,21 @@ function parseSource(source) {
|
|
|
107
107
|
return record
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function normalizeRelative(relative) {
|
|
110
|
+
export function normalizeRelative(relative) {
|
|
111
111
|
return String(relative ?? '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Declaration files have no runtime behavior; parsing them for import-time
|
|
115
115
|
// side effects or execution strings only creates noise.
|
|
116
|
-
function isDeclarationFile(relative) {
|
|
116
|
+
export function isDeclarationFile(relative) {
|
|
117
117
|
return /\.d\.[cm]?ts$/i.test(String(relative ?? ''))
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function identifierName(node) {
|
|
120
|
+
export function identifierName(node) {
|
|
121
121
|
return node?.type === 'Identifier' ? node.name : null
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function memberPropertyName(node) {
|
|
124
|
+
export function memberPropertyName(node) {
|
|
125
125
|
if (!node || node.type !== 'MemberExpression') return null
|
|
126
126
|
if (!node.computed && node.property?.type === 'Identifier') return node.property.name
|
|
127
127
|
if (node.property?.type === 'Literal' && typeof node.property.value === 'string') return node.property.value
|
|
@@ -141,7 +141,7 @@ function calledFunctionName(call) {
|
|
|
141
141
|
return null
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function walkAst(root, visit) {
|
|
144
|
+
export function walkAst(root, visit) {
|
|
145
145
|
const ancestors = []
|
|
146
146
|
const walkNode = (node) => {
|
|
147
147
|
if (!node || typeof node.type !== 'string') return
|
|
@@ -163,7 +163,7 @@ function walkAst(root, visit) {
|
|
|
163
163
|
|
|
164
164
|
// Module scope means "runs while the module is evaluated". Function bodies and
|
|
165
165
|
// class method bodies are skipped; top-level if/try/loop bodies are not.
|
|
166
|
-
function walkModuleScope(ast, visit) {
|
|
166
|
+
export function walkModuleScope(ast, visit) {
|
|
167
167
|
const ancestors = []
|
|
168
168
|
const walkNode = (node) => {
|
|
169
169
|
if (!node || typeof node.type !== 'string') return
|
|
@@ -295,7 +295,7 @@ function classifyLiteral(node, ancestors) {
|
|
|
295
295
|
return 'unknown'
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
function literalStringValue(node) {
|
|
298
|
+
export function literalStringValue(node) {
|
|
299
299
|
if (node.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
300
300
|
if (node.type === 'TemplateElement' && node.value) {
|
|
301
301
|
if (typeof node.value.cooked === 'string') return node.value.cooked
|
|
@@ -341,7 +341,7 @@ function literalCandidates(base) {
|
|
|
341
341
|
return candidates
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
function resolveLocalSpecifier(fromRelative, specifier, byRelative) {
|
|
344
|
+
export function resolveLocalSpecifier(fromRelative, specifier, byRelative) {
|
|
345
345
|
const spec = String(specifier).replace(/\\/g, '/')
|
|
346
346
|
if (!(spec.startsWith('./') || spec.startsWith('../') || spec === '.' || spec === '..')) return { kind: 'package' }
|
|
347
347
|
const base = path.posix.normalize(path.posix.join(path.posix.dirname(normalizeRelative(fromRelative)), spec))
|
|
@@ -352,7 +352,7 @@ function resolveLocalSpecifier(fromRelative, specifier, byRelative) {
|
|
|
352
352
|
return { kind: 'unresolved' }
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
function collectModuleEdges(ast) {
|
|
355
|
+
export function collectModuleEdges(ast) {
|
|
356
356
|
const edges = []
|
|
357
357
|
walkAst(ast, (node) => {
|
|
358
358
|
if (node.type === 'ImportDeclaration' || node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') {
|
package/40-checker/src/index.mjs
CHANGED
|
@@ -5,7 +5,8 @@ export { checkCapabilities, checkCredentials, checkLabels, checkImportTimeAdviso
|
|
|
5
5
|
export { readSources, readSourcesWithStats, listSourceFiles, scanSourceFiles } from './util.mjs'
|
|
6
6
|
export { runRuntimeChecks } from './runtime.mjs'
|
|
7
7
|
export { runStaticBundleChecks } from './bundle-static.mjs'
|
|
8
|
-
export {
|
|
8
|
+
export { runSecurityChecks, checkLifecycleScripts, checkDynamicExecution, checkSensitiveFileReads, checkEgressEndpoints, checkOpaqueArtifacts, checkPromptInjectionClues, checkSupplyChain, checkProvenance, SECURITY_IDS } from './security.mjs'
|
|
9
|
+
export { buildReport, formatText, formatSarif, writeReport, summarizeBlocking, buildSecurityView, DECLARATION_CHECKS, SECURITY_CHECKS, SECURITY_MANDATORY } from './report.mjs'
|
|
9
10
|
export { joinAdvice, DOCS_BASE } from './remediation.mjs'
|
|
10
11
|
export { satisfies, validRange, parseVersion, compareVersions } from './semver.mjs'
|
|
11
12
|
export { parseInstallSpec } from './install-spec.mjs'
|
|
@@ -13,8 +13,13 @@ export const ENTRY_RUN = ['DSC-01', 'DSC-02', 'DSC-03', 'DSC-11', 'DSC-12', 'DSC
|
|
|
13
13
|
export const ENTRY_BLOCKING = ['C-1.1', 'C-1.2', 'C-1.4', 'C-3.1', 'C-3.2', 'C-3.5']
|
|
14
14
|
export const ENTRY_RUNTIME_BLOCKING = [...ENTRY_BLOCKING, 'C-2.2', 'C-2.3']
|
|
15
15
|
|
|
16
|
+
/** The four security checks that block in `--scope security`; the rest are advisory. */
|
|
17
|
+
export const SECURITY_MANDATORY = ['DSC-15', 'DSC-16', 'DSC-17', 'DSC-21']
|
|
18
|
+
export const SECURITY_CHECKS = new Set(['DSC-15', 'DSC-16', 'DSC-17', 'DSC-18', 'DSC-19', 'DSC-20', 'DSC-21', 'DSC-22'])
|
|
19
|
+
|
|
16
20
|
function mandatoryFor(scope, profile, staticOnly, entryClaim) {
|
|
17
21
|
if (scope === 'mvp') return MVP_CHECKS
|
|
22
|
+
if (scope === 'security') return SECURITY_MANDATORY
|
|
18
23
|
if (scope === 'entry') return ENTRY_RUN.filter(id => (entryClaim !== 'static' || id !== 'DSC-14') && (id !== 'DSC-03' || profile === 'listing' || profile === 'both'))
|
|
19
24
|
if (staticOnly) return STATIC_MANDATORY
|
|
20
25
|
return profile === 'listing' || profile === 'both' ? LISTING_MANDATORY : PACKAGE_MANDATORY
|
|
@@ -68,6 +73,11 @@ export function howToEnableFor(item) {
|
|
|
68
73
|
if (id === 'DSC-14') {
|
|
69
74
|
return 'pass --host <app-boot/lib/index.js> for bundle G0-G3, or --runtime <path> for a plugin; omit --no-exec and --entry-claim static'
|
|
70
75
|
}
|
|
76
|
+
if (id === 'DSC-15') return 'fix package.json#scripts so install lifecycle commands are auditable; --scope security makes DSC-15 blocking'
|
|
77
|
+
if (['DSC-16', 'DSC-17', 'DSC-21'].includes(id)) return 'provide a readable entry/source tree and rerun; --scope security makes this check blocking'
|
|
78
|
+
if (id === 'DSC-19') return 'provide a readable package tree; binaries are recorded by sha256, never executed'
|
|
79
|
+
if (id === 'DSC-22') return 'supply registry metadata or an attestation/signature field; without input this check is not-run'
|
|
80
|
+
if (['DSC-18', 'DSC-20'].includes(id)) return 'this check is advisory; it never blocks and needs no extra input'
|
|
71
81
|
if (['DSC-11', 'DSC-12', 'DSC-13'].includes(id)) {
|
|
72
82
|
if (/manifest unavailable/.test(reason)) return 'fix package.json; these declaration checks cannot run without a manifest'
|
|
73
83
|
return 'run the full profile without --no-exec; declaration checks need source files'
|
|
@@ -108,8 +118,58 @@ function allowedNa(check, scope, profile, kind) {
|
|
|
108
118
|
return false
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
|
|
121
|
+
function capSecurityCheck(item) {
|
|
122
|
+
if (item.result !== 'FAIL') return item
|
|
123
|
+
return {
|
|
124
|
+
...item,
|
|
125
|
+
result: 'WARN',
|
|
126
|
+
reason: `capped WARN (non-security scope): ${item.reason ?? ''}`.trim(),
|
|
127
|
+
details: (item.details ?? []).map(detailRow => (
|
|
128
|
+
detailRow.result === 'FAIL'
|
|
129
|
+
? { ...detailRow, result: 'WARN', reason: `capped WARN (non-security scope): ${detailRow.reason ?? ''}`.trim() }
|
|
130
|
+
: detailRow
|
|
131
|
+
)),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The additive `security` block. In `--scope security` the eight checks keep
|
|
137
|
+
* their real verdict (and four of them are mandatory); in every other scope a
|
|
138
|
+
* FAIL is capped to WARN so the default verdict cannot change.
|
|
139
|
+
*/
|
|
140
|
+
export function buildSecurityView(securityChecks, scope) {
|
|
141
|
+
const input = Array.isArray(securityChecks) ? securityChecks.filter(item => SECURITY_CHECKS.has(item.id)) : []
|
|
142
|
+
const checks = scope === 'security' ? input : input.map(capSecurityCheck)
|
|
143
|
+
// Only DSC-15/16/17 may block; DSC-21 is mandatory but its findings are
|
|
144
|
+
// capped at WARN, and DSC-18/19/20/22 are advisory by definition.
|
|
145
|
+
const blocking = checks
|
|
146
|
+
.filter(item => item.result === 'FAIL' && ['DSC-15', 'DSC-16', 'DSC-17'].includes(item.id))
|
|
147
|
+
.map(item => item.id)
|
|
148
|
+
const warn = checks.filter(item => item.result === 'WARN' && item.advisory !== true).map(item => item.id)
|
|
149
|
+
const advisory = checks.filter(item => item.result === 'WARN' && item.advisory === true).map(item => item.id)
|
|
150
|
+
const notRun = checks
|
|
151
|
+
.filter(item => item.result === 'NA' || item.result === 'ERROR' || item.coverage === 'not-run')
|
|
152
|
+
.map(item => item.id)
|
|
153
|
+
const mandatoryNotRun = SECURITY_MANDATORY.some(id => !checks.some(item => item.id === id && ['PASS', 'WARN', 'FAIL'].includes(item.result)))
|
|
154
|
+
let status
|
|
155
|
+
if (blocking.length > 0) status = 'fail'
|
|
156
|
+
else if (scope === 'security' && mandatoryNotRun) status = 'incomplete'
|
|
157
|
+
else if (warn.length > 0 || advisory.length > 0 || mandatoryNotRun) status = 'warn'
|
|
158
|
+
else status = 'pass'
|
|
159
|
+
return {
|
|
160
|
+
scope: scope === 'security' ? 'security' : 'advisory',
|
|
161
|
+
status,
|
|
162
|
+
checks,
|
|
163
|
+
blocking,
|
|
164
|
+
warn,
|
|
165
|
+
advisory,
|
|
166
|
+
notRun,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime', advice = true, declarations = null, runtimeChecks = null, securityChecks = null }) {
|
|
112
171
|
const mandatory = mandatoryFor(scope, profile, staticOnly, entryClaim)
|
|
172
|
+
const security = buildSecurityView(securityChecks, scope)
|
|
113
173
|
const adjustedChecks = scope === 'entry' ? checks.map(item => adjustEntryCheck(item, entryClaim)) : checks
|
|
114
174
|
const runtimeIds = new Set(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'])
|
|
115
175
|
const effectiveChecks = staticOnly
|
|
@@ -161,7 +221,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
|
|
|
161
221
|
} else if (status === 'pass' || status === 'warn') {
|
|
162
222
|
level = `DSH-ENTRY static${status === 'warn' ? ' (warnings)' : ''}`
|
|
163
223
|
}
|
|
164
|
-
} else if (scope !== 'mvp' && (status === 'pass' || status === 'warn')) {
|
|
224
|
+
} else if (scope !== 'mvp' && scope !== 'security' && (status === 'pass' || status === 'warn')) {
|
|
165
225
|
const suffix = status === 'warn' ? ', warnings' : ''
|
|
166
226
|
level = staticOnly ? `DS-C static${status === 'warn' ? ' (warnings)' : ''}` : profile === 'listing' || profile === 'both' ? `DS-C (listing${suffix})` : `DS-C (package${suffix})`
|
|
167
227
|
}
|
|
@@ -181,6 +241,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
|
|
|
181
241
|
const hygieneBucket = effectiveChecks.filter(item => item.result === 'WARN').map(item => bucketRow(item, {
|
|
182
242
|
declaration: DECLARATION_CHECKS.has(item.id),
|
|
183
243
|
advisory: item.advisory === true,
|
|
244
|
+
...(SECURITY_CHECKS.has(item.id) ? { security: true } : {}),
|
|
184
245
|
}))
|
|
185
246
|
const notCheckedBucket = effectiveChecks
|
|
186
247
|
.filter(item => item.result === 'NA' || item.result === 'ERROR')
|
|
@@ -240,6 +301,7 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
|
|
|
240
301
|
entryBlocking,
|
|
241
302
|
},
|
|
242
303
|
buckets,
|
|
304
|
+
security,
|
|
243
305
|
notChecked,
|
|
244
306
|
metrics,
|
|
245
307
|
conclusion: {
|
|
@@ -369,6 +431,10 @@ export function formatText(report) {
|
|
|
369
431
|
const passed = (report.checks ?? []).filter(item => item.result === 'PASS')
|
|
370
432
|
lines.push(`passed (${passed.length}): ${passed.map(item => item.id).join(', ') || 'none'}`)
|
|
371
433
|
lines.push(`coverage: total=${coverage.total ?? report.checks.length} checked=${coverage.checked ?? coverage.pass + coverage.warn + coverage.fail} pass=${coverage.pass} warn=${coverage.warn} fail=${coverage.fail} na=${coverage.na} notRun=${coverage.notRun} partial=${coverage.partial ?? 0} error=${coverage.error} advisory=${coverage.advisory}`)
|
|
434
|
+
if (report.security) {
|
|
435
|
+
const security = report.security
|
|
436
|
+
lines.push(`security: scope=${security.scope} status=${security.status} blocking=[${(security.blocking ?? []).join(', ')}] warn=[${(security.warn ?? []).join(', ')}] advisory=[${(security.advisory ?? []).join(', ')}] notRun=[${(security.notRun ?? []).join(', ')}]`)
|
|
437
|
+
}
|
|
372
438
|
if (coverage.entryBlocking) {
|
|
373
439
|
const blocking = coverage.entryBlocking
|
|
374
440
|
lines.push(`entryBlocking: checked=${blocking.checked?.length ?? '?'}/${blocking.required} failed=[${(blocking.failed ?? []).join(', ')}] notChecked=[${(blocking.notChecked ?? []).join(', ')}] pass=${blocking.pass}`)
|