@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5

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.
Files changed (195) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +779 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +379 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +841 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/dist/frontend/index.html +21 -0
  72. package/dist/frontend/src/app.js +176 -0
  73. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  74. package/dist/frontend/src/components/filter-rail.js +119 -0
  75. package/dist/frontend/src/components/query-bar.js +126 -0
  76. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  77. package/dist/frontend/src/export-entry.js +36 -0
  78. package/dist/frontend/src/lib/api-client.js +92 -0
  79. package/dist/frontend/src/lib/contrast.js +34 -0
  80. package/dist/frontend/src/lib/dom.js +24 -0
  81. package/dist/frontend/src/lib/escape-html.js +16 -0
  82. package/dist/frontend/src/lib/flow-path.js +40 -0
  83. package/dist/frontend/src/lib/focus-controls.js +149 -0
  84. package/dist/frontend/src/lib/protection-visual.js +46 -0
  85. package/dist/frontend/src/lib/query-language.js +240 -0
  86. package/dist/frontend/src/lib/row-filters.js +43 -0
  87. package/dist/frontend/src/lib/state.js +84 -0
  88. package/dist/frontend/src/main.js +83 -0
  89. package/dist/frontend/src/shell.js +184 -0
  90. package/dist/frontend/src/views/architecture-view.js +798 -0
  91. package/dist/frontend/src/views/inventory-view.js +292 -0
  92. package/dist/frontend/src/views/privacy-view.js +172 -0
  93. package/dist/frontend/src/views/trace-view.js +206 -0
  94. package/dist/frontend/styles/architecture-view.css +93 -0
  95. package/dist/frontend/styles/filter-rail.css +34 -0
  96. package/dist/frontend/styles/inspector.css +69 -0
  97. package/dist/frontend/styles/inventory-view.css +74 -0
  98. package/dist/frontend/styles/privacy-view.css +86 -0
  99. package/dist/frontend/styles/query-bar.css +107 -0
  100. package/dist/frontend/styles/shell.css +155 -0
  101. package/dist/frontend/styles/tokens.css +128 -0
  102. package/dist/frontend/styles/trace-view.css +95 -0
  103. package/package.json +13 -6
  104. package/src/dataflow/CLAUDE.md +1 -1
  105. package/src/dataflow/catalog.js +42 -0
  106. package/src/dataflow/orm-write-catalog.js +175 -0
  107. package/src/engine.js +92 -1
  108. package/src/ir/CLAUDE.md +1 -0
  109. package/src/ir/chrome-probe.mjs +150 -0
  110. package/src/ir/parser-js.js +94 -7
  111. package/src/lineage/CLAUDE.md +1203 -0
  112. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  113. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  114. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  115. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  116. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  117. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  118. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  119. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  120. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  121. package/src/lineage/classification.js +56 -0
  122. package/src/lineage/coverage.js +658 -0
  123. package/src/lineage/cross-repo-link.js +107 -0
  124. package/src/lineage/dataflow-graph.schema.json +184 -0
  125. package/src/lineage/decision-story.js +206 -0
  126. package/src/lineage/drift-policy.js +279 -0
  127. package/src/lineage/driver.js +135 -0
  128. package/src/lineage/engine.js +992 -0
  129. package/src/lineage/export-briefing.js +628 -0
  130. package/src/lineage/export-csv.js +62 -0
  131. package/src/lineage/export-json.js +238 -0
  132. package/src/lineage/export-privacy.js +258 -0
  133. package/src/lineage/federation-loader.js +111 -0
  134. package/src/lineage/field-identity.js +78 -0
  135. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  136. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  137. package/src/lineage/flow-grade.js +221 -0
  138. package/src/lineage/governance-edit.js +169 -0
  139. package/src/lineage/graph-builder.js +1114 -0
  140. package/src/lineage/graph-diff.js +431 -0
  141. package/src/lineage/graph-snapshot.js +180 -0
  142. package/src/lineage/handling-analyzer.js +168 -0
  143. package/src/lineage/ids.js +349 -0
  144. package/src/lineage/impact-assessment.js +76 -0
  145. package/src/lineage/impact-engine.js +268 -0
  146. package/src/lineage/index.js +281 -0
  147. package/src/lineage/language-coverage-tiers.js +58 -0
  148. package/src/lineage/obligation-mapping.js +126 -0
  149. package/src/lineage/obligation-predicates.js +235 -0
  150. package/src/lineage/observation-adapters.js +282 -0
  151. package/src/lineage/observation-correlation.js +622 -0
  152. package/src/lineage/observation-store.js +497 -0
  153. package/src/lineage/path-query.js +410 -0
  154. package/src/lineage/path-store.js +400 -0
  155. package/src/lineage/protection.js +53 -0
  156. package/src/lineage/recipient-profile.js +192 -0
  157. package/src/lineage/recipient-registry.js +394 -0
  158. package/src/lineage/redact-graph.js +224 -0
  159. package/src/lineage/remediation.js +417 -0
  160. package/src/lineage/resolve-destination.js +91 -0
  161. package/src/lineage/runtime-observation.js +464 -0
  162. package/src/lineage/scenario-diff.js +84 -0
  163. package/src/lineage/scenario-engine.js +251 -0
  164. package/src/lineage/scenario.js +101 -0
  165. package/src/lineage/schema.js +167 -0
  166. package/src/lineage/sink-registry.js +427 -0
  167. package/src/lineage/source-registry.js +357 -0
  168. package/src/lineage/source-seeding.js +212 -0
  169. package/src/lineage/summaries.js +590 -0
  170. package/src/lineage/transform-catalog.js +397 -0
  171. package/src/lineage/transit-protection.js +150 -0
  172. package/src/lineage/validate.js +285 -0
  173. package/src/mcp/CLAUDE.md +7 -1
  174. package/src/mcp/dataflow-tools.js +160 -0
  175. package/src/mcp/server.js +1 -1
  176. package/src/mcp/tools.js +2 -1
  177. package/src/pipeline/scan-health.js +19 -1
  178. package/src/posture/CLAUDE.md +19 -0
  179. package/src/posture/artifact-registry.js +52 -0
  180. package/src/posture/auditor-walkthrough.js +76 -0
  181. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  182. package/src/posture/obligation-evidence-pack.js +202 -0
  183. package/src/posture/remediation-ledger.js +337 -0
  184. package/src/server/CLAUDE.md +47 -0
  185. package/src/server/graph-loader.js +141 -0
  186. package/src/server/http-server.js +325 -0
  187. package/src/server/routes.js +129 -0
  188. package/src/server/security.js +111 -0
  189. package/src/server/static-assets.js +144 -0
  190. package/src/shared/frontend-root.js +52 -0
  191. package/dist/11.index.js +0 -353
  192. package/dist/259.index.js +0 -975
  193. package/dist/317.index.js +0 -300
  194. package/dist/609.index.js +0 -741
  195. package/dist/838.index.js +0 -152
@@ -0,0 +1,36 @@
1
+ // export-entry.js — Milestone 4, sub-project Self-contained HTML report.
2
+ //
3
+ // The entry point for a bundled, offline, self-contained export — NOT
4
+ // main.js, which is the `explore`-server-specific entry point (token
5
+ // extraction + authenticated fetch, both irrelevant and unavailable
6
+ // offline). This file's only job: read the graph the report generator
7
+ // embedded as a global, and call the SAME, UNCHANGED bootstrap() every
8
+ // other entry point uses.
9
+ //
10
+ // `window.__AGENTIC_SECURITY_EXPORTED_GRAPH__` is set by an inline
11
+ // <script> the report generator writes BEFORE this bundled script in the
12
+ // emitted HTML — see scanner/scripts/generate-html-report.mjs.
13
+ import { bootstrap } from './app.js';
14
+ import { el, clear } from './lib/dom.js';
15
+
16
+ function showError(rootEl, message) {
17
+ clear(rootEl);
18
+ rootEl.appendChild(el('div', { class: 'app-error' }, [
19
+ el('div', { class: 'app-error__title' }, 'Data Flow Explorer report could not load'),
20
+ el('div', { class: 'app-error__message' }, message),
21
+ ]));
22
+ }
23
+
24
+ function init() {
25
+ const rootEl = document.getElementById('app-root');
26
+ const graph = typeof window !== 'undefined' ? window.__AGENTIC_SECURITY_EXPORTED_GRAPH__ : undefined;
27
+ if (!graph) {
28
+ showError(rootEl, 'No embedded graph data found in this report file. It may be corrupted — regenerate it with `agentic-security` and open the new file.');
29
+ return;
30
+ }
31
+ bootstrap(rootEl, graph);
32
+ }
33
+
34
+ if (typeof document !== 'undefined' && typeof document.getElementById === 'function' && document.getElementById('app-root')) {
35
+ init();
36
+ }
@@ -0,0 +1,92 @@
1
+ // api-client.js — Milestone 3, sub-project Wire.
2
+ //
3
+ // The frontend's entire integration surface with the `explore` server
4
+ // (scanner/src/server/). Two small functions: extract the session token
5
+ // once from the URL fragment, then use it to fetch the graph. Every other
6
+ // view's own rendering code stays untouched — this file is the whole
7
+ // integration point (see the M3-Wire scoping doc's Decision 3).
8
+ //
9
+ // The session token travels via the URL FRAGMENT (`#token=<hex>`), never a
10
+ // query string or a cookie — a URL fragment is never sent to the server in
11
+ // any HTTP request, so the initial page-load request itself cannot leak the
12
+ // token into a server access log, yet the fragment IS readable by this
13
+ // page's own JS via `location.hash` (see Decision 1).
14
+
15
+ const TOKEN_HEADER = 'x-agentic-security-token';
16
+
17
+ /**
18
+ * Parses `location.hash` (or an explicitly-supplied hash string, for
19
+ * testing) for a `#token=<64-hex-char>` parameter. Other params (e.g.
20
+ * `view`/`selected`/`filters`, which shell.js starts writing into the same
21
+ * hash after the first render) are ignored here — this function only ever
22
+ * cares about `token`.
23
+ *
24
+ * @param {string} [hash] - defaults to the real `location.hash` in a browser
25
+ * @returns {string|null} the token, or null if absent/malformed
26
+ */
27
+ export function extractTokenFromFragment(hash = (typeof location !== 'undefined' ? location.hash : '')) {
28
+ const raw = String(hash ?? '').replace(/^#/, '');
29
+ if (!raw) return null;
30
+
31
+ let params;
32
+ try {
33
+ params = new URLSearchParams(raw);
34
+ } catch {
35
+ return null;
36
+ }
37
+
38
+ const token = params.get('token');
39
+ if (!token || !/^[0-9a-f]{64}$/i.test(token)) return null;
40
+ return token;
41
+ }
42
+
43
+ /**
44
+ * Fetches the full graph from the explore server's `GET /api/v1/graph`
45
+ * endpoint, authenticating via the `x-agentic-security-token` header (never
46
+ * a query string), and unwraps the response envelope's `.data` field (see
47
+ * scanner/src/server/routes.js's `wrapResponse`) to hand back the plain
48
+ * graph object `bootstrap()` already expects.
49
+ *
50
+ * @param {object} opts
51
+ * @param {string} opts.token - required
52
+ * @param {string} [opts.baseUrl=''] - defaults to a relative fetch against
53
+ * the page's own origin, which is the ONLY thing a real browser page is
54
+ * ever expected to pass (same-origin, per the M3-Wire scoping doc's
55
+ * load-bearing CORS-disabled correction). Overridable so a real live-HTTP
56
+ * test can point this at a real running server without a headless
57
+ * browser — never used by the shipped frontend itself.
58
+ * @returns {Promise<object>} the plain graph object
59
+ */
60
+ export async function fetchGraph({ token, baseUrl = '' } = {}) {
61
+ if (!token) {
62
+ throw new Error('fetchGraph: a session token is required');
63
+ }
64
+
65
+ const res = await fetch(`${baseUrl}/api/v1/graph`, {
66
+ headers: { [TOKEN_HEADER]: token },
67
+ });
68
+
69
+ if (!res.ok) {
70
+ let detail = '';
71
+ try {
72
+ const body = await res.json();
73
+ if (body && typeof body.error === 'string') detail = `: ${body.error}`;
74
+ } catch {
75
+ /* non-JSON error body — report the status alone */
76
+ }
77
+ throw new Error(`fetchGraph: request failed with status ${res.status}${detail}`);
78
+ }
79
+
80
+ let envelope;
81
+ try {
82
+ envelope = await res.json();
83
+ } catch {
84
+ throw new Error('fetchGraph: response was not valid JSON');
85
+ }
86
+
87
+ if (!envelope || typeof envelope !== 'object' || !('data' in envelope)) {
88
+ throw new Error('fetchGraph: unexpected response shape (missing .data)');
89
+ }
90
+
91
+ return envelope.data;
92
+ }
@@ -0,0 +1,34 @@
1
+ // WCAG 2.x relative-luminance contrast ratio, from first principles
2
+ // (https://www.w3.org/TR/WCAG21/#dfn-relative-luminance /
3
+ // https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio). No existing contrast
4
+ // checker exists anywhere in this repo — this is a small, dependency-free,
5
+ // directly-testable implementation rather than a new npm dependency.
6
+
7
+ function srgbChannelToLinear(c8) {
8
+ const c = c8 / 255;
9
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
10
+ }
11
+
12
+ function hexToRgb(hex) {
13
+ const m = /^#([0-9a-f]{6})$/i.exec(String(hex));
14
+ if (!m) throw new Error(`hexToRgb: invalid hex color "${hex}" (expected #RRGGBB)`);
15
+ const int = parseInt(m[1], 16);
16
+ return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
17
+ }
18
+
19
+ function relativeLuminance([r, g, b]) {
20
+ const [rl, gl, bl] = [r, g, b].map(srgbChannelToLinear);
21
+ return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
22
+ }
23
+
24
+ export function contrastRatio(hexA, hexB) {
25
+ const lumA = relativeLuminance(hexToRgb(hexA));
26
+ const lumB = relativeLuminance(hexToRgb(hexB));
27
+ const lighter = Math.max(lumA, lumB);
28
+ const darker = Math.min(lumA, lumB);
29
+ return (lighter + 0.05) / (darker + 0.05);
30
+ }
31
+
32
+ export function meetsAA(hexA, hexB, { largeText = false } = {}) {
33
+ return contrastRatio(hexA, hexB) >= (largeText ? 3 : 4.5);
34
+ }
@@ -0,0 +1,24 @@
1
+ // Safe DOM element builder. Attributes are set via setAttribute/className/
2
+ // addEventListener; text children go through createTextNode. Never uses
3
+ // innerHTML — graph-derived text can never become live markup through this
4
+ // function (PRD §16 last line, AC-15 hygiene, even though the formal
5
+ // adversarial-fixture gate is Milestone 3's).
6
+
7
+ export function el(tag, attrs = {}, children = []) {
8
+ const node = document.createElement(tag);
9
+ for (const [key, value] of Object.entries(attrs)) {
10
+ if (value === undefined || value === null || value === false) continue;
11
+ if (key === 'class') node.className = String(value);
12
+ else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2).toLowerCase(), value);
13
+ else node.setAttribute(key, String(value));
14
+ }
15
+ for (const child of [].concat(children)) {
16
+ if (child === undefined || child === null || child === false) continue;
17
+ node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
18
+ }
19
+ return node;
20
+ }
21
+
22
+ export function clear(node) {
23
+ while (node.firstChild) node.removeChild(node.firstChild);
24
+ }
@@ -0,0 +1,16 @@
1
+ // Safe HTML-text escaping. Neither existing in-repo escaper is quote-complete
2
+ // (scanner/src/posture/fleet.js's `esc` skips `'`; scanner/src/badge.js's
3
+ // `_xmlEscape` skips `"` and `'`) — this one escapes all five HTML-significant
4
+ // characters so it is safe in both text content and quoted attribute values.
5
+
6
+ const ENTITIES = Object.freeze({
7
+ '&': '&amp;',
8
+ '<': '&lt;',
9
+ '>': '&gt;',
10
+ '"': '&quot;',
11
+ "'": '&#39;',
12
+ });
13
+
14
+ export function escapeHtml(value) {
15
+ return String(value ?? '').replace(/[&<>"']/g, (c) => ENTITIES[c]);
16
+ }
@@ -0,0 +1,40 @@
1
+ // Shared per-flow path/topology helpers, used by every view that needs to
2
+ // know "which nodes does this flow actually touch" — extracted here after
3
+ // Architecture View independently reimplemented the same node-collection
4
+ // logic inline in computeFlowSummary, to avoid a third divergent copy when
5
+ // Privacy View needed it too.
6
+
7
+ export function flowPathNodeIds(graph, flow) {
8
+ const ids = new Set([flow.source, flow.sink]);
9
+ for (const edgeId of flow.edgeIds) {
10
+ const edge = graph.edges.find((e) => e.id === edgeId);
11
+ if (edge) {
12
+ ids.add(edge.from);
13
+ ids.add(edge.to);
14
+ }
15
+ }
16
+ return ids;
17
+ }
18
+
19
+ // Backend AI node-subtype vocabulary (scanner/src/lineage/schema.js's
20
+ // SOURCE_CATEGORIES/SINK_CATEGORIES, AI-flavored entries only). This list
21
+ // must be hand-kept in sync with that schema file — the frontend never
22
+ // imports scanner/src/lineage/ at runtime (see frontend/CLAUDE.md).
23
+ // Corrected this increment: the previous set ('ai-assistant',
24
+ // 'vector-store') matched neither real enum and silently never matched
25
+ // any real node.
26
+ export const AI_SUBTYPES = new Set([
27
+ 'ai-model-provider', 'ai-local-model', 'ai-agent', 'ai-tool',
28
+ 'ai-vector-store', 'ai-memory', 'ai-training', 'ai-evaluation', 'ai-telemetry',
29
+ 'ai-model-output', 'ai-tool-result', 'ai-retrieved-document',
30
+ ]);
31
+
32
+ // AI relevance is computed from flow/node TOPOLOGY (does the path touch an
33
+ // AI-kind node), never from dataElement.aiContexts — that field is never
34
+ // populated by name-only classification (see scanner/src/lineage's
35
+ // classification.js), so an aiContexts-based filter would show zero AI
36
+ // relevance despite real AI-processing flows existing in the graph.
37
+ export function isAiRelevantFlow(graph, flow) {
38
+ const pathNodeIds = flowPathNodeIds(graph, flow);
39
+ return graph.nodes.some((n) => pathNodeIds.has(n.id) && AI_SUBTYPES.has(n.subtype));
40
+ }
@@ -0,0 +1,149 @@
1
+ // Milestone 3, sub-project M3-UX-Query, Task 3. Nine focus-control graph
2
+ // traversals over the SAME DataFlowGraph v1 structure the query language
3
+ // (lib/query-language.js) reads. Each function returns the SAME
4
+ // `{nodeIds: Set<string>, edgeIds: Set<string>}` shape
5
+ // `views/architecture-view.js`'s own `resolveSelection` already produces
6
+ // for `selection` — so the render layer needs zero new consumption code to
7
+ // accept output from any of these. `resetToOverview` is deliberately NOT
8
+ // implemented here: Task 4 wires the existing `resolveSelection(graph,
9
+ // null)` directly for that control (see this file's own header note in the
10
+ // task brief) — duplicating it here would just be a second, divergent copy
11
+ // of the same empty-selection shape.
12
+
13
+ function buildAdjacency(graph) {
14
+ const forward = new Map(); // nodeId -> [{edgeId, toId}]
15
+ const backward = new Map(); // nodeId -> [{edgeId, fromId}]
16
+ for (const n of graph.nodes) { forward.set(n.id, []); backward.set(n.id, []); }
17
+ for (const e of graph.edges) {
18
+ forward.get(e.from)?.push({ edgeId: e.id, toId: e.to });
19
+ backward.get(e.to)?.push({ edgeId: e.id, fromId: e.from });
20
+ }
21
+ return { forward, backward };
22
+ }
23
+
24
+ function bfsDirection(graph, startId, adjacencyKey, adjacency) {
25
+ const nodeIds = new Set([startId]);
26
+ const edgeIds = new Set();
27
+ const queue = [startId];
28
+ while (queue.length > 0) {
29
+ const current = queue.shift();
30
+ for (const { edgeId, toId, fromId } of adjacency.get(current) ?? []) {
31
+ const nextId = adjacencyKey === 'forward' ? toId : fromId;
32
+ edgeIds.add(edgeId);
33
+ if (!nodeIds.has(nextId)) { nodeIds.add(nextId); queue.push(nextId); }
34
+ }
35
+ }
36
+ return { nodeIds, edgeIds };
37
+ }
38
+
39
+ export function showDownstream(graph, nodeId) {
40
+ const { forward } = buildAdjacency(graph);
41
+ return bfsDirection(graph, nodeId, 'forward', forward);
42
+ }
43
+
44
+ export function showUpstream(graph, nodeId) {
45
+ const { backward } = buildAdjacency(graph);
46
+ return bfsDirection(graph, nodeId, 'backward', backward);
47
+ }
48
+
49
+ export function showAllPaths(graph, nodeId) {
50
+ const down = showDownstream(graph, nodeId);
51
+ const up = showUpstream(graph, nodeId);
52
+ return {
53
+ nodeIds: new Set([...down.nodeIds, ...up.nodeIds]),
54
+ edgeIds: new Set([...down.edgeIds, ...up.edgeIds]),
55
+ };
56
+ }
57
+
58
+ export function showShortestPath(graph, fromId, toId) {
59
+ const { forward } = buildAdjacency(graph);
60
+ const cameFrom = new Map(); // nodeId -> {viaEdgeId, fromId}
61
+ const visited = new Set([fromId]);
62
+ const queue = [fromId];
63
+ let found = false;
64
+ while (queue.length > 0 && !found) {
65
+ const current = queue.shift();
66
+ for (const { edgeId, toId: nextId } of forward.get(current) ?? []) {
67
+ if (visited.has(nextId)) continue;
68
+ visited.add(nextId);
69
+ cameFrom.set(nextId, { viaEdgeId: edgeId, fromId: current });
70
+ if (nextId === toId) { found = true; break; }
71
+ queue.push(nextId);
72
+ }
73
+ }
74
+ if (!found) return { nodeIds: new Set(), edgeIds: new Set() };
75
+ const nodeIds = new Set([toId]);
76
+ const edgeIds = new Set();
77
+ let cursor = toId;
78
+ while (cursor !== fromId) {
79
+ const step = cameFrom.get(cursor);
80
+ edgeIds.add(step.viaEdgeId);
81
+ nodeIds.add(step.fromId);
82
+ cursor = step.fromId;
83
+ }
84
+ return { nodeIds, edgeIds };
85
+ }
86
+
87
+ export function showExternalPathsOnly(graph) {
88
+ const externalNodeIds = new Set(graph.nodes.filter((n) => n.externality?.value === 'external').map((n) => n.id));
89
+ const nodeIds = new Set();
90
+ const edgeIds = new Set();
91
+ for (const flow of graph.flows) {
92
+ const pathNodeIds = new Set([flow.source, flow.sink]);
93
+ for (const edgeId of flow.edgeIds) {
94
+ const edge = graph.edges.find((e) => e.id === edgeId);
95
+ if (edge) { pathNodeIds.add(edge.from); pathNodeIds.add(edge.to); }
96
+ }
97
+ if ([...pathNodeIds].some((id) => externalNodeIds.has(id))) {
98
+ for (const id of pathNodeIds) nodeIds.add(id);
99
+ for (const edgeId of flow.edgeIds) edgeIds.add(edgeId);
100
+ }
101
+ }
102
+ return { nodeIds, edgeIds };
103
+ }
104
+
105
+ const UNPROTECTED_VERDICTS = new Set(['unprotected', 'mixed', 'unknown']);
106
+ export function showUnprotectedPathsOnly(graph) {
107
+ const nodeIds = new Set();
108
+ const edgeIds = new Set();
109
+ for (const edge of graph.edges) {
110
+ const verdicts = [edge.protection.transit.verdict, edge.protection.atRest.verdict, edge.protection.handling.verdict];
111
+ if (verdicts.some((v) => UNPROTECTED_VERDICTS.has(v))) {
112
+ edgeIds.add(edge.id);
113
+ nodeIds.add(edge.from);
114
+ nodeIds.add(edge.to);
115
+ }
116
+ }
117
+ return { nodeIds, edgeIds };
118
+ }
119
+
120
+ export function showAliases(graph, nodeId) {
121
+ const node = graph.nodes.find((n) => n.id === nodeId);
122
+ const nodeIds = new Set([nodeId]);
123
+ // Real, honest implementation. This plan's own Global Constraints section
124
+ // disclosed node.aliases as "confirmed always empty in real scan output" —
125
+ // that claim does NOT hold against the real committed flagship fixture
126
+ // (Web App/Payment API/Analytics API all carry non-empty aliases arrays
127
+ // there; see test/focus-controls.test.js for the correction). What IS
128
+ // true, confirmed against that same fixture: every alias entry is an
129
+ // alternate DISPLAY NAME for the node itself (e.g. "Checkout Form" is
130
+ // another name for the Web App node), never a pointer to a distinct
131
+ // sibling node record — no alias string in the fixture matches any other
132
+ // real node's label or id. This loop is still real, correct logic (not a
133
+ // no-op stub): it looks up each alias against the graph's own nodes and
134
+ // only adds a match it actually finds, so it will do the right thing the
135
+ // moment (if ever) alias data that DOES reference a distinct node record
136
+ // shows up in a real scan — nothing here is invented alias data.
137
+ for (const alias of node?.aliases ?? []) {
138
+ const aliasNode = graph.nodes.find((n) => n.label === alias || n.id === alias);
139
+ if (aliasNode) nodeIds.add(aliasNode.id);
140
+ }
141
+ return { nodeIds, edgeIds: new Set() };
142
+ }
143
+
144
+ export function showDisconnected(graph) {
145
+ const connectedIds = new Set();
146
+ for (const e of graph.edges) { connectedIds.add(e.from); connectedIds.add(e.to); }
147
+ const nodeIds = new Set(graph.nodes.filter((n) => !connectedIds.has(n.id)).map((n) => n.id));
148
+ return { nodeIds, edgeIds: new Set() };
149
+ }
@@ -0,0 +1,46 @@
1
+ // The single source of truth for how a protection verdict renders — every
2
+ // view/component must call protectionVisual() rather than hardcoding a
3
+ // color or glyph, so AC-20 (verdicts distinguishable without color) holds
4
+ // everywhere by construction, not by convention.
5
+ //
6
+ // These verdict strings are NOT imported from scanner/src/lineage/protection.js
7
+ // — the browser bundle never imports anything under scanner/src/lineage/ at
8
+ // runtime (see frontend/CLAUDE.md). Keep this list in sync by hand if the
9
+ // backend's PROTECTION_VERDICTS/FLOW_SUMMARY_VALUES enums ever change.
10
+
11
+ // PRD §8.4, exact quoted precedence: "unprotected/prohibited → mixed →
12
+ // unknown/manual_required → protected/permitted → not_assessed". Read as
13
+ // display priority when aggregating multiple verdicts into one, not a raw
14
+ // severity order: not_assessed is LOWEST priority because it means "no
15
+ // information," and any real signal (even a protected one) should be shown
16
+ // instead of "not assessed." not_applicable isn't named in the PRD list;
17
+ // treated at the same lowest priority as not_assessed (both mean "nothing
18
+ // to prioritize").
19
+ export const VERDICT_PRECEDENCE = Object.freeze([
20
+ 'unprotected',
21
+ 'mixed',
22
+ 'unknown',
23
+ 'protected',
24
+ 'not_assessed',
25
+ 'not_applicable',
26
+ ]);
27
+
28
+ const VISUALS = Object.freeze({
29
+ protected: { verdict: 'protected', label: 'Protected', glyph: '✓', lineStyle: 'solid', colorVar: '--status-protected' },
30
+ unprotected: { verdict: 'unprotected', label: 'Unprotected', glyph: '✗', lineStyle: 'solid', colorVar: '--status-unprotected' },
31
+ mixed: { verdict: 'mixed', label: 'Mixed', glyph: '±', lineStyle: 'solid', colorVar: '--status-unprotected' },
32
+ unknown: { verdict: 'unknown', label: 'Unknown', glyph: '?', lineStyle: 'dashed', colorVar: '--status-unknown' },
33
+ not_applicable: { verdict: 'not_applicable', label: 'Not applicable', glyph: '·', lineStyle: 'dotted', colorVar: '--text-secondary' },
34
+ not_assessed: { verdict: 'not_assessed', label: 'Not assessed', glyph: '–', lineStyle: 'dotted', colorVar: '--status-unknown' },
35
+ });
36
+
37
+ export function protectionVisual(verdict) {
38
+ return VISUALS[verdict] ?? VISUALS.not_assessed;
39
+ }
40
+
41
+ export function worstVerdict(verdicts) {
42
+ for (const tier of VERDICT_PRECEDENCE) {
43
+ if (verdicts.includes(tier)) return tier;
44
+ }
45
+ return 'not_assessed';
46
+ }