@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.
- package/CHANGELOG.md +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +379 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +841 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- 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 +13 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +144 -0
- package/src/shared/frontend-root.js +52 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- 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
|
+
'&': '&',
|
|
8
|
+
'<': '<',
|
|
9
|
+
'>': '>',
|
|
10
|
+
'"': '"',
|
|
11
|
+
"'": ''',
|
|
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
|
+
}
|