@clear-capabilities/agentic-security-scanner 0.147.0 → 0.148.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/CHANGELOG.md +204 -0
- package/bin/agentic-security.js +47 -15
- package/dist/1122.index.js +79 -2
- package/dist/3180.index.js +73 -1
- package/dist/5051.index.js +77 -6
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/nist-800-171-r3.json +1269 -0
- package/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +2 -2
- package/src/posture/auditor-walkthrough.js +89 -6
- package/src/posture/compliance-frameworks/nist-800-171-r3.json +1269 -0
- package/src/server/static-assets.js +11 -6
- package/src/shared/frontend-root.js +52 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Data Flow Explorer design tokens (PRD §8.1). Dark is the reference theme.
|
|
3
|
+
* Values are copied verbatim from the PRD — do not adjust without a
|
|
4
|
+
* deliberate, separately-reviewed change.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
/* Surfaces */
|
|
9
|
+
--surface-canvas: #061625;
|
|
10
|
+
--surface-panel: #0B1E2F;
|
|
11
|
+
--surface-elevated: #102A40;
|
|
12
|
+
--border-default: #28445A;
|
|
13
|
+
|
|
14
|
+
/* Text */
|
|
15
|
+
--text-primary: #F3F7FA;
|
|
16
|
+
--text-secondary: #9FB3C5;
|
|
17
|
+
|
|
18
|
+
/* Selection / focus */
|
|
19
|
+
--accent-selection: #3DA9FF;
|
|
20
|
+
|
|
21
|
+
/* Protection verdict status */
|
|
22
|
+
--status-protected: #59D17D;
|
|
23
|
+
--status-unprotected: #FF625C;
|
|
24
|
+
--status-unknown: #F5B83D;
|
|
25
|
+
|
|
26
|
+
/* Text color for content painted directly on a status token background
|
|
27
|
+
(e.g. the coverage banner). NOT the same as --text-primary, which is
|
|
28
|
+
sized against --surface-canvas/--surface-panel, not against a status
|
|
29
|
+
color. Verified with lib/contrast.js's contrastRatio(): #061625 on
|
|
30
|
+
dark --status-unknown (#F5B83D) is 10.27:1. See the light-theme block
|
|
31
|
+
below for the light-theme value and its own verified ratio. */
|
|
32
|
+
--status-banner-text: #061625;
|
|
33
|
+
|
|
34
|
+
/* AI processing context */
|
|
35
|
+
--context-ai: #B47AFF;
|
|
36
|
+
|
|
37
|
+
/* Data class overlays */
|
|
38
|
+
--class-pii: #28D1C5;
|
|
39
|
+
--class-phi: #A97BFF;
|
|
40
|
+
--class-pci: #4CA7FF;
|
|
41
|
+
|
|
42
|
+
/* Spacing (8px grid, PRD §7.7) */
|
|
43
|
+
--space-1: 8px;
|
|
44
|
+
--space-2: 16px;
|
|
45
|
+
--space-3: 24px;
|
|
46
|
+
--space-4: 32px;
|
|
47
|
+
|
|
48
|
+
/* Radius / border (PRD §8.2) */
|
|
49
|
+
--radius-default: 8px;
|
|
50
|
+
--border-width: 1px;
|
|
51
|
+
|
|
52
|
+
/* Typography (PRD §8.2) — bundled system stack; never a remote/CDN font */
|
|
53
|
+
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
54
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
55
|
+
--font-size-view-title: 23px;
|
|
56
|
+
--font-size-panel-title: 15px;
|
|
57
|
+
--font-size-node-title: 14px;
|
|
58
|
+
--font-size-body: 13px;
|
|
59
|
+
--font-size-code: 12.5px;
|
|
60
|
+
|
|
61
|
+
/* Motion (PRD §8.2 — focus/topology change only, no ambient animation) */
|
|
62
|
+
--motion-duration: 200ms;
|
|
63
|
+
--motion-duration-reduced: 0ms;
|
|
64
|
+
|
|
65
|
+
/* Shell region sizing (PRD §7.7) */
|
|
66
|
+
--header-height: 56px;
|
|
67
|
+
--view-tabs-height: 44px;
|
|
68
|
+
--left-rail-width: 248px;
|
|
69
|
+
--left-rail-collapsed-width: 56px;
|
|
70
|
+
--right-inspector-width: 360px;
|
|
71
|
+
--context-rail-height: 72px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Light theme — same semantic tokens, WCAG-AA-equivalent contrast (PRD §8.1/§23) */
|
|
75
|
+
:root[data-theme="light"] {
|
|
76
|
+
--surface-canvas: #FFFFFF;
|
|
77
|
+
--surface-panel: #F4F7FA;
|
|
78
|
+
--surface-elevated: #E7EEF3;
|
|
79
|
+
--border-default: #C7D3DC;
|
|
80
|
+
|
|
81
|
+
--text-primary: #0B1E2F;
|
|
82
|
+
--text-secondary: #3E5568;
|
|
83
|
+
|
|
84
|
+
--accent-selection: #1B6FC2;
|
|
85
|
+
|
|
86
|
+
/* status-protected and status-unknown darkened slightly from their
|
|
87
|
+
original values (#1E8A4C, #9A6B00) — Milestone 3, sub-project A11y,
|
|
88
|
+
found via test/tokens-contrast.test.js that both originals failed
|
|
89
|
+
WCAG AA (4.5:1) against --surface-panel (#F4F7FA): 4.07:1 and 4.36:1
|
|
90
|
+
respectively. Verified with lib/contrast.js's contrastRatio() against
|
|
91
|
+
BOTH backgrounds a status color plausibly renders on: --status-
|
|
92
|
+
protected (#1C8047) is 4.97:1 on --surface-canvas / 4.62:1 on
|
|
93
|
+
--surface-panel; --status-unknown (#956700) is 4.98:1 / 4.63:1. Both
|
|
94
|
+
now clear AA with margin (>=4.6) rather than sitting at the exact
|
|
95
|
+
4.50 boundary, which is fragile against hex-rounding. --status-
|
|
96
|
+
unprotected was already compliant (5.26:1 / 4.89:1), unchanged. */
|
|
97
|
+
--status-protected: #1C8047;
|
|
98
|
+
--status-unprotected: #C7362D;
|
|
99
|
+
--status-unknown: #956700;
|
|
100
|
+
|
|
101
|
+
/* Verified with lib/contrast.js's contrastRatio(): #FFFFFF on light
|
|
102
|
+
--status-unknown (#956700) is 4.98:1 (>= 4.5:1 AA). The dark-block
|
|
103
|
+
value (#061625, --surface-canvas's own dark value) is 3.90:1 here --
|
|
104
|
+
below AA -- which is the bug this token pair fixes: shell.css must
|
|
105
|
+
never hardcode dark theme's value and expect it to also work in
|
|
106
|
+
light theme. */
|
|
107
|
+
--status-banner-text: #FFFFFF;
|
|
108
|
+
|
|
109
|
+
--context-ai: #6E38B8;
|
|
110
|
+
|
|
111
|
+
--class-pii: #0E7C73;
|
|
112
|
+
--class-phi: #6E44B8;
|
|
113
|
+
--class-pci: #1C63A8;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@media (prefers-reduced-motion: reduce) {
|
|
117
|
+
:root {
|
|
118
|
+
--motion-duration: var(--motion-duration-reduced);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
body {
|
|
123
|
+
background: var(--surface-canvas);
|
|
124
|
+
color: var(--text-primary);
|
|
125
|
+
font-family: var(--font-family);
|
|
126
|
+
font-size: var(--font-size-body);
|
|
127
|
+
margin: 0;
|
|
128
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
.trace-view {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-2);
|
|
5
|
+
max-width: 640px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.trace-step {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: var(--space-2);
|
|
11
|
+
padding: var(--space-2);
|
|
12
|
+
background: var(--surface-panel);
|
|
13
|
+
border: var(--border-width) solid var(--border-default);
|
|
14
|
+
border-radius: var(--radius-default);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.trace-step-number {
|
|
18
|
+
flex: 0 0 auto;
|
|
19
|
+
width: 24px;
|
|
20
|
+
height: 24px;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
background: var(--surface-elevated);
|
|
23
|
+
color: var(--text-primary);
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
font-size: var(--font-size-code);
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.trace-step-body {
|
|
32
|
+
flex: 1 1 auto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.trace-step-kind {
|
|
36
|
+
color: var(--text-secondary);
|
|
37
|
+
font-size: var(--font-size-code);
|
|
38
|
+
text-transform: uppercase;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.trace-step-mapping {
|
|
42
|
+
font-family: var(--font-mono);
|
|
43
|
+
font-size: var(--font-size-code);
|
|
44
|
+
color: var(--text-primary);
|
|
45
|
+
margin: 4px 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.trace-step-node {
|
|
49
|
+
color: var(--text-secondary);
|
|
50
|
+
font-size: var(--font-size-body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.trace-step-boundary {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
border: 1px solid var(--status-unprotected);
|
|
56
|
+
color: var(--status-unprotected);
|
|
57
|
+
border-radius: var(--radius-default);
|
|
58
|
+
padding: 1px 6px;
|
|
59
|
+
font-size: var(--font-size-code);
|
|
60
|
+
margin-top: 4px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.trace-branch-group {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: var(--space-2);
|
|
67
|
+
padding-left: var(--space-2);
|
|
68
|
+
border-left: 2px solid var(--accent-selection);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.trace-branch-label {
|
|
72
|
+
color: var(--text-secondary);
|
|
73
|
+
font-size: var(--font-size-code);
|
|
74
|
+
text-transform: uppercase;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.trace-alternates {
|
|
78
|
+
margin-top: var(--space-2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.trace-alternate-item {
|
|
82
|
+
padding: var(--space-1) 0;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
color: var(--text-secondary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.trace-alternate-item:hover {
|
|
88
|
+
color: var(--text-primary);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.trace-empty {
|
|
92
|
+
color: var(--text-secondary);
|
|
93
|
+
font-style: italic;
|
|
94
|
+
padding: var(--space-2);
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.148.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"lodash-es": "^4.18.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
|
-
"build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && rm -rf dist/compliance-frameworks && mkdir -p dist/compliance-frameworks && cp src/posture/compliance-frameworks/*.json dist/compliance-frameworks/ && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
59
|
+
"build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && rm -rf dist/compliance-frameworks && mkdir -p dist/compliance-frameworks && cp src/posture/compliance-frameworks/*.json dist/compliance-frameworks/ && node scripts/copy-frontend.mjs && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
60
60
|
"prepare": "node ../scripts/pre-push-gate.mjs --install-hook",
|
|
61
61
|
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs && node ../scripts/release-check.mjs",
|
|
62
62
|
"test": "node ../scripts/run-unit-tests.mjs && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && node --experimental-test-module-mocks --test test/fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection-k8s-admission.test.js && node --experimental-test-module-mocks --test test/lineage-fault-injection.test.js && npm run test:python",
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
// nist-ai-600-1 NIST AI Risk Management Framework, GenAI profile
|
|
11
11
|
// nist-privacy-1-1 NIST Privacy Framework 1.1 (see posture/privacy-framework.js
|
|
12
12
|
// for the assessment + remediation layer over it)
|
|
13
|
+
// nist-800-171-r3 NIST SP 800-171 Rev. 3 — Protecting CUI in Nonfederal
|
|
14
|
+
// Systems. FULL 97-requirement catalogue; 43 of those are
|
|
15
|
+
// organisational/physical and carry no automated signal at
|
|
16
|
+
// all, deliberately carried so the negative space is visible.
|
|
17
|
+
// Control text is generated from the CSV export by
|
|
18
|
+
// scripts/nist-800-171/build-catalog.py; the codeTestable
|
|
19
|
+
// rating is OURS, not NIST's (that export rates nothing for
|
|
20
|
+
// testability) and lives in nist-800-171/code-testability.json.
|
|
13
21
|
// owasp-asvs-5 OWASP Application Security Verification Standard 5.0
|
|
14
22
|
// owasp-llm-top-10 OWASP Top 10 for LLM Applications 2025
|
|
15
23
|
// eu-ai-act EU AI Act (Regulation 2024/1689)
|
|
@@ -345,6 +353,60 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
345
353
|
families.get(k).push(f);
|
|
346
354
|
}
|
|
347
355
|
|
|
356
|
+
// VACUOUS SATISFACTION GUARD (shared-evaluator half).
|
|
357
|
+
//
|
|
358
|
+
// The `family:` branch below clears a mapping when no findings of that family
|
|
359
|
+
// are open — which is ALSO true of a scan that examined nothing at all. So
|
|
360
|
+
// pointing `--report <framework>` at an empty directory, a wrong path, or a
|
|
361
|
+
// scan that failed to read anything reported mapped controls as fully
|
|
362
|
+
// evidenced, on the strength of having looked at zero files.
|
|
363
|
+
//
|
|
364
|
+
// privacy-framework.js's `bucketOf` has carried this guard for its own module
|
|
365
|
+
// since it was found by that module's test, and its comment already named
|
|
366
|
+
// `evaluateFramework` as the place the hazard originates — but the guard was
|
|
367
|
+
// never applied HERE, so every framework reached through `--report` /
|
|
368
|
+
// `--walkthrough` / `--gap` / `--format oscal` still had it. Found while
|
|
369
|
+
// adding NIST SP 800-171 Rev. 3, whose 50 mapped controls made a
|
|
370
|
+
// zero-file scan report 32 controls `present`.
|
|
371
|
+
//
|
|
372
|
+
// Reads THREE count fields, not the two privacy-framework.js reads. Measured,
|
|
373
|
+
// not assumed: a real scan of a clean one-file project persists
|
|
374
|
+
// `scanned.files: 1` and `_scanMeta.filesScanned: 1` but NO top-level
|
|
375
|
+
// `filesScanned` — that key exists only on the in-memory object engine.js
|
|
376
|
+
// returns. privacy-framework.js reads `filesScanned ?? _scanMeta.filesScanned`
|
|
377
|
+
// and gets away with it because engine.js hands it the live in-memory object;
|
|
378
|
+
// this evaluator is reached from the CLI with the PERSISTED artifact, where
|
|
379
|
+
// that first key is absent. Mirroring privacy's exact field list here would
|
|
380
|
+
// therefore have read 0 for every real scan and degraded a genuinely clean
|
|
381
|
+
// project to "not assessed" — the opposite failure, and worse, because it
|
|
382
|
+
// punishes the operator who did everything right.
|
|
383
|
+
//
|
|
384
|
+
// ABSENT IS UNKNOWN, NOT ZERO. Degrading requires POSITIVE evidence that
|
|
385
|
+
// nothing was examined: at least one count field actually present AND every
|
|
386
|
+
// present one reading 0. A scan object carrying no count fields at all
|
|
387
|
+
// (synthetic fixtures, older artifacts, third-party callers) is treated as
|
|
388
|
+
// assessable and behaves exactly as before. Same precedent as relevance.js —
|
|
389
|
+
// never assert the negative without evidence, and `unknown` is not `false`.
|
|
390
|
+
// The findings/components/... fallbacks then rescue the remaining case: an
|
|
391
|
+
// artifact that records 0 files but plainly holds real results.
|
|
392
|
+
//
|
|
393
|
+
// An unassessable scan does not remove or fail a control: the mapping is
|
|
394
|
+
// disclosed as unverifiable, which caps the control at 'partial' via the same
|
|
395
|
+
// path a `rule:` mapping already uses. Recall-preserving, same precedent as
|
|
396
|
+
// falsification.js / proof-gate.js.
|
|
397
|
+
const _counts = [
|
|
398
|
+
scan && scan.filesScanned,
|
|
399
|
+
scan && scan._scanMeta && scan._scanMeta.filesScanned,
|
|
400
|
+
scan && scan.scanned && scan.scanned.files,
|
|
401
|
+
].filter((v) => typeof v === 'number' && Number.isFinite(v));
|
|
402
|
+
const _examinedNothing = _counts.length > 0 && _counts.every((v) => v === 0);
|
|
403
|
+
const assessable = !_examinedNothing
|
|
404
|
+
|| findings.length > 0
|
|
405
|
+
|| components.length > 0
|
|
406
|
+
|| secrets.length > 0
|
|
407
|
+
|| logicVulns.length > 0
|
|
408
|
+
|| supplyChain.length > 0;
|
|
409
|
+
|
|
348
410
|
const results = [];
|
|
349
411
|
for (const c of fw.controls || []) {
|
|
350
412
|
const obs = [];
|
|
@@ -447,6 +509,11 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
447
509
|
allCleared = false;
|
|
448
510
|
contributingFindings.push(...open);
|
|
449
511
|
obs.push(`${open.length} open ${fam} finding(s) at ${minSeverity}+.`);
|
|
512
|
+
} else if (!assessable) {
|
|
513
|
+
// Empty bucket, but the scan examined nothing — see the guard above.
|
|
514
|
+
// Not evidence, and deliberately not a failure either.
|
|
515
|
+
obs.push(`⚠ ${fam}: the scan examined no files, so an empty finding bucket is not evidence (not assessed).`);
|
|
516
|
+
hasUnverifiableMapping = true;
|
|
450
517
|
} else {
|
|
451
518
|
obs.push(`✓ ${fam}: no open ${minSeverity}+ findings.`);
|
|
452
519
|
anyCleared = true;
|
|
@@ -465,7 +532,15 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
465
532
|
'aibom': 'aibom.json',
|
|
466
533
|
'attack-taxonomy': 'last-scan.json',
|
|
467
534
|
'why-fired': 'last-scan.json',
|
|
468
|
-
|
|
535
|
+
// Two real spellings, both live in this codebase: security-trend.js and
|
|
536
|
+
// router.js read `scan-history.json` (a FILE), findings-memory.js uses
|
|
537
|
+
// `scan-history` (a DIRECTORY). Only the directory was listed here, so
|
|
538
|
+
// on a normal scan — which writes the .json — every control mapped to
|
|
539
|
+
// module:scan-history reported the artifact missing and could never
|
|
540
|
+
// clear. Five bundled frameworks were affected. An array means "any of
|
|
541
|
+
// these satisfies it", which is the honest reading: the control asks
|
|
542
|
+
// whether a scan history exists, not which shape it took.
|
|
543
|
+
'scan-history': ['scan-history.json', 'scan-history/'],
|
|
469
544
|
'integrity': 'last-scan.json.sig',
|
|
470
545
|
'watch-mode': 'watch-status.json',
|
|
471
546
|
'cve-alert-daemon': 'cve-alerts/',
|
|
@@ -480,17 +555,25 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
480
555
|
'security-fixer': '.../agents/security-fixer.md',
|
|
481
556
|
'mcp-tools': '.../scanner/src/mcp/tools.js',
|
|
482
557
|
};
|
|
558
|
+
// A table entry is either one path or an array of acceptable ones. An
|
|
559
|
+
// array means the artifact has more than one real spelling in this
|
|
560
|
+
// codebase and any of them evidences the control; the FIRST is the
|
|
561
|
+
// canonical name used in the observation text when none is found, so
|
|
562
|
+
// the message still names something a reader can go create.
|
|
483
563
|
const target = ARTIFACT[mod];
|
|
564
|
+
const candidates = target == null ? [] : (Array.isArray(target) ? target : [target]);
|
|
484
565
|
// A '.../' sentinel marks a source-relative artifact (project source,
|
|
485
566
|
// e.g. a hook or agent file) — resolve it against the scan root itself.
|
|
486
567
|
// Everything else is a runtime artifact under the STATE dir. Without
|
|
487
568
|
// this, `statePath(scanRoot, '.../x')` never resolves and the
|
|
488
569
|
// control falsely reads "not present" for every project.
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
: statePath(scanRoot,
|
|
492
|
-
const
|
|
493
|
-
|
|
570
|
+
const _resolve = (t) => t.startsWith('.../')
|
|
571
|
+
? path.join(scanRoot, t.slice(4))
|
|
572
|
+
: statePath(scanRoot, t);
|
|
573
|
+
const found = candidates.find((t) => { try { return fs.existsSync(_resolve(t)); } catch { return false; } });
|
|
574
|
+
const resolved = found ? _resolve(found) : null;
|
|
575
|
+
const label = (found || candidates[0] || '(unmapped)').replace(/^\.\.\.\//, '');
|
|
576
|
+
if (resolved) {
|
|
494
577
|
obs.push(`✓ ${mod}: ${label} present.`);
|
|
495
578
|
anySignal = true;
|
|
496
579
|
anyCleared = true;
|