@clear-capabilities/agentic-security-scanner 0.147.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 (39) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/1122.index.js +79 -2
  3. package/dist/3180.index.js +73 -1
  4. package/dist/5051.index.js +77 -6
  5. package/dist/frontend/index.html +21 -0
  6. package/dist/frontend/src/app.js +176 -0
  7. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  8. package/dist/frontend/src/components/filter-rail.js +119 -0
  9. package/dist/frontend/src/components/query-bar.js +126 -0
  10. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  11. package/dist/frontend/src/export-entry.js +36 -0
  12. package/dist/frontend/src/lib/api-client.js +92 -0
  13. package/dist/frontend/src/lib/contrast.js +34 -0
  14. package/dist/frontend/src/lib/dom.js +24 -0
  15. package/dist/frontend/src/lib/escape-html.js +16 -0
  16. package/dist/frontend/src/lib/flow-path.js +40 -0
  17. package/dist/frontend/src/lib/focus-controls.js +149 -0
  18. package/dist/frontend/src/lib/protection-visual.js +46 -0
  19. package/dist/frontend/src/lib/query-language.js +240 -0
  20. package/dist/frontend/src/lib/row-filters.js +43 -0
  21. package/dist/frontend/src/lib/state.js +84 -0
  22. package/dist/frontend/src/main.js +83 -0
  23. package/dist/frontend/src/shell.js +184 -0
  24. package/dist/frontend/src/views/architecture-view.js +798 -0
  25. package/dist/frontend/src/views/inventory-view.js +292 -0
  26. package/dist/frontend/src/views/privacy-view.js +172 -0
  27. package/dist/frontend/src/views/trace-view.js +206 -0
  28. package/dist/frontend/styles/architecture-view.css +93 -0
  29. package/dist/frontend/styles/filter-rail.css +34 -0
  30. package/dist/frontend/styles/inspector.css +69 -0
  31. package/dist/frontend/styles/inventory-view.css +74 -0
  32. package/dist/frontend/styles/privacy-view.css +86 -0
  33. package/dist/frontend/styles/query-bar.css +107 -0
  34. package/dist/frontend/styles/shell.css +155 -0
  35. package/dist/frontend/styles/tokens.css +128 -0
  36. package/dist/frontend/styles/trace-view.css +95 -0
  37. package/package.json +2 -2
  38. package/src/server/static-assets.js +11 -6
  39. package/src/shared/frontend-root.js +52 -0
@@ -0,0 +1,141 @@
1
+ import { el, clear } from '../lib/dom.js';
2
+ import { protectionVisual } from '../lib/protection-visual.js';
3
+
4
+ export function computeInspectorViewModel(graph, selectedId) {
5
+ if (!selectedId) return null;
6
+
7
+ const flow = graph.flows.find((f) => f.id === selectedId);
8
+ const edge = !flow && graph.edges.find((e) => e.id === selectedId);
9
+ const node = !flow && !edge && graph.nodes.find((n) => n.id === selectedId);
10
+ const dataElement = !flow && !edge && !node && graph.dataElements.find((d) => d.id === selectedId);
11
+ const transformation = !flow && !edge && !node && !dataElement && graph.transformations.find((t) => t.id === selectedId);
12
+ const target = flow || edge || node || dataElement || transformation;
13
+ if (!target) return null;
14
+
15
+ const kind = flow ? 'flow' : edge ? 'edge' : node ? 'node' : dataElement ? 'dataElement' : 'transformation';
16
+ const evidenceRefs = target.evidenceRefs ?? [];
17
+ const evidenceItems = evidenceRefs
18
+ .map((id) => graph.evidence.find((ev) => ev.id === id))
19
+ .filter(Boolean);
20
+ const supporting = evidenceItems.filter((e) => !e.conflict);
21
+ const conflicting = evidenceItems.filter((e) => e.conflict);
22
+
23
+ return {
24
+ kind,
25
+ id: target.id,
26
+ claim: buildClaimText(graph, kind, target),
27
+ supporting,
28
+ conflicting,
29
+ limitations: target.limitations ?? [],
30
+ target,
31
+ };
32
+ }
33
+
34
+ function buildClaimText(graph, kind, target) {
35
+ if (kind === 'flow') {
36
+ const dataElement = graph.dataElements.find((d) => target.dataElementIds.includes(d.id));
37
+ const source = graph.nodes.find((n) => n.id === target.source);
38
+ const sink = graph.nodes.find((n) => n.id === target.sink);
39
+ return `${dataElement?.name ?? 'field'} flows from ${source?.label ?? 'unknown source'} to ${sink?.label ?? 'unknown destination'}: ${target.protectionSummary}`;
40
+ }
41
+ if (kind === 'edge') {
42
+ const from = graph.nodes.find((n) => n.id === target.from);
43
+ const to = graph.nodes.find((n) => n.id === target.to);
44
+ return `${from?.label ?? '?'} → ${to?.label ?? '?'}: handling ${target.protection.handling.verdict}, transit ${target.protection.transit.verdict}, at rest ${target.protection.atRest.verdict}`;
45
+ }
46
+ if (kind === 'dataElement') {
47
+ return `${target.name}: ${(target.dataClasses ?? []).join(', ') || 'no data classes recorded'}`;
48
+ }
49
+ if (kind === 'transformation') {
50
+ return `${target.kind} transformation (${target.reversibility})`;
51
+ }
52
+ return `${target.label} (${target.kind}/${target.subtype})`;
53
+ }
54
+
55
+ /** @param {ReturnType<typeof computeInspectorViewModel>} viewModel */
56
+ export function renderInspector(viewModel, inspectorEl) {
57
+ clear(inspectorEl);
58
+ if (!viewModel) {
59
+ inspectorEl.appendChild(el('p', { class: 'inspector-empty' }, 'Select a node, edge, or flow to see its evidence.'));
60
+ return;
61
+ }
62
+
63
+ const container = el('div', { class: 'inspector' });
64
+ container.appendChild(el('h3', { class: 'inspector-title' }, 'Evidence inspector'));
65
+
66
+ container.appendChild(el('p', { class: 'inspector-claim' }, viewModel.claim));
67
+
68
+ container.appendChild(el('h4', { class: 'inspector-section-title' }, 'Supporting evidence'));
69
+ if (viewModel.supporting.length === 0) {
70
+ container.appendChild(el('p', { class: 'inspector-empty' }, 'No supporting evidence recorded.'));
71
+ } else {
72
+ container.appendChild(
73
+ el(
74
+ 'ul',
75
+ { class: 'inspector-evidence-list' },
76
+ viewModel.supporting.map((ev) => renderEvidenceItem(ev)),
77
+ ),
78
+ );
79
+ }
80
+
81
+ container.appendChild(el('h4', { class: 'inspector-section-title' }, 'Conflicting evidence'));
82
+ if (viewModel.conflicting.length === 0) {
83
+ container.appendChild(el('p', { class: 'inspector-empty' }, 'None recorded.'));
84
+ } else {
85
+ container.appendChild(
86
+ el(
87
+ 'ul',
88
+ { class: 'inspector-evidence-list' },
89
+ viewModel.conflicting.map((ev) => renderEvidenceItem(ev)),
90
+ ),
91
+ );
92
+ }
93
+
94
+ container.appendChild(el('h4', { class: 'inspector-section-title' }, 'What the scanner does not know'));
95
+ if (viewModel.limitations.length === 0) {
96
+ container.appendChild(el('p', { class: 'inspector-empty' }, 'No limitations recorded for this claim.'));
97
+ } else {
98
+ container.appendChild(
99
+ el(
100
+ 'ul',
101
+ { class: 'inspector-limitations-list' },
102
+ viewModel.limitations.map((text) => el('li', {}, text)),
103
+ ),
104
+ );
105
+ }
106
+
107
+ if (viewModel.kind === 'edge' || viewModel.kind === 'flow') {
108
+ container.appendChild(renderVerdictBadges(viewModel));
109
+ }
110
+
111
+ inspectorEl.appendChild(container);
112
+ }
113
+
114
+ function renderEvidenceItem(evidence) {
115
+ return el('li', { class: 'inspector-evidence-item' }, [
116
+ el('span', { class: 'inspector-evidence-claim' }, evidence.claim),
117
+ el('span', { class: 'inspector-evidence-location' }, evidence.location?.note ?? 'location unknown'),
118
+ ]);
119
+ }
120
+
121
+ function renderVerdictBadges(viewModel) {
122
+ const target = viewModel.target;
123
+ const dims = viewModel.kind === 'edge'
124
+ ? [
125
+ ['Transit', target.protection.transit.verdict],
126
+ ['At rest', target.protection.atRest.verdict],
127
+ ['Handling', target.protection.handling.verdict],
128
+ ]
129
+ : [['Protection summary', target.protectionSummary]];
130
+ return el(
131
+ 'div',
132
+ { class: 'inspector-verdicts' },
133
+ dims.map(([label, verdict]) => {
134
+ const visual = protectionVisual(verdict);
135
+ return el('div', { class: 'inspector-verdict-row' }, [
136
+ el('span', { class: 'inspector-verdict-dim-label' }, `${label}: `),
137
+ el('span', { class: 'inspector-verdict-badge', style: `border-color: var(${visual.colorVar})` }, `${visual.glyph} ${visual.label}`),
138
+ ]);
139
+ }),
140
+ );
141
+ }
@@ -0,0 +1,119 @@
1
+ import { el, clear } from '../lib/dom.js';
2
+
3
+ // A fixed enum, not derived from "whatever protectionSummary values happen
4
+ // to be present in this fixture" — a filter chip for `unprotected` must
5
+ // still exist even on a graph where nothing currently is, so a user can
6
+ // confirm that's genuinely true rather than the chip silently not existing.
7
+ const PROTECTION_TIERS = Object.freeze(['protected', 'unprotected', 'mixed', 'unknown']);
8
+
9
+ export function computeFilterFacets(graph) {
10
+ const dataClasses = [...new Set((graph.dataElements ?? []).flatMap((d) => d.dataClasses ?? []))].sort();
11
+ const sourceCategories = [...new Set(graph.nodes.filter((n) => n.kind === 'source').map((n) => n.subtype).filter(Boolean))].sort();
12
+ const sinkCategories = [...new Set(graph.nodes.filter((n) => n.kind === 'sink').map((n) => n.subtype).filter(Boolean))].sort();
13
+ const destinationExternalities = [...new Set(graph.nodes.map((n) => n.externality?.value).filter(Boolean))].sort();
14
+ const transitVerdicts = [...new Set(graph.edges.map((e) => e.protection.transit.verdict))].sort();
15
+ const atRestVerdicts = [...new Set(graph.edges.map((e) => e.protection.atRest.verdict))].sort();
16
+ const handlingVerdicts = [...new Set(graph.edges.map((e) => e.protection.handling.verdict))].sort();
17
+ const policyVerdicts = [...new Set(graph.flows.map((f) => f.policyVerdict))].sort();
18
+ return {
19
+ dataClasses, protectionTiers: PROTECTION_TIERS,
20
+ sourceCategories, sinkCategories, destinationExternalities,
21
+ transitVerdicts, atRestVerdicts, handlingVerdicts, policyVerdicts,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * @param {ReturnType<typeof computeFilterFacets>} facets
27
+ * @param {{dataClass?: string[], protection?: string[], ai?: boolean}} currentFilters
28
+ * @param {HTMLElement} railEl
29
+ * @param {(next: object) => void} onFiltersChange
30
+ */
31
+ export function renderFilterRail(facets, currentFilters, railEl, onFiltersChange) {
32
+ clear(railEl);
33
+
34
+ const dataClassChips = el(
35
+ 'div',
36
+ { class: 'filter-rail-group' },
37
+ [el('h4', {}, 'Data class'), ...facets.dataClasses.map((cls) => renderChip(cls, currentFilters.dataClass?.includes(cls) ?? false, () => toggleListFilter(currentFilters, 'dataClass', cls, onFiltersChange)))],
38
+ );
39
+
40
+ const protectionChips = el(
41
+ 'div',
42
+ { class: 'filter-rail-group' },
43
+ [el('h4', {}, 'Protection'), ...facets.protectionTiers.map((tier) => renderChip(tier, currentFilters.protection?.includes(tier) ?? false, () => toggleListFilter(currentFilters, 'protection', tier, onFiltersChange)))],
44
+ );
45
+
46
+ const sourceCategoryChips = el(
47
+ 'div',
48
+ { class: 'filter-rail-group' },
49
+ [el('h4', {}, 'Source category'), ...facets.sourceCategories.map((cat) => renderChip(cat, currentFilters.sourceCategory?.includes(cat) ?? false, () => toggleListFilter(currentFilters, 'sourceCategory', cat, onFiltersChange)))],
50
+ );
51
+
52
+ const sinkCategoryChips = el(
53
+ 'div',
54
+ { class: 'filter-rail-group' },
55
+ [el('h4', {}, 'Sink category'), ...facets.sinkCategories.map((cat) => renderChip(cat, currentFilters.sinkCategory?.includes(cat) ?? false, () => toggleListFilter(currentFilters, 'sinkCategory', cat, onFiltersChange)))],
56
+ );
57
+
58
+ const destinationExternalityChips = el(
59
+ 'div',
60
+ { class: 'filter-rail-group' },
61
+ [el('h4', {}, 'Destination externality'), ...facets.destinationExternalities.map((val) => renderChip(val, currentFilters.destinationExternality?.includes(val) ?? false, () => toggleListFilter(currentFilters, 'destinationExternality', val, onFiltersChange)))],
62
+ );
63
+
64
+ const transitVerdictChips = el(
65
+ 'div',
66
+ { class: 'filter-rail-group' },
67
+ [el('h4', {}, 'Transit'), ...facets.transitVerdicts.map((v) => renderChip(v, currentFilters.transitVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'transitVerdict', v, onFiltersChange)))],
68
+ );
69
+
70
+ const atRestVerdictChips = el(
71
+ 'div',
72
+ { class: 'filter-rail-group' },
73
+ [el('h4', {}, 'At rest'), ...facets.atRestVerdicts.map((v) => renderChip(v, currentFilters.atRestVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'atRestVerdict', v, onFiltersChange)))],
74
+ );
75
+
76
+ const handlingVerdictChips = el(
77
+ 'div',
78
+ { class: 'filter-rail-group' },
79
+ [el('h4', {}, 'Handling'), ...facets.handlingVerdicts.map((v) => renderChip(v, currentFilters.handlingVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'handlingVerdict', v, onFiltersChange)))],
80
+ );
81
+
82
+ const policyVerdictChips = el(
83
+ 'div',
84
+ { class: 'filter-rail-group' },
85
+ [el('h4', {}, 'Policy verdict'), ...facets.policyVerdicts.map((v) => renderChip(v, currentFilters.policyVerdict?.includes(v) ?? false, () => toggleListFilter(currentFilters, 'policyVerdict', v, onFiltersChange)))],
86
+ );
87
+
88
+ const aiChip = el(
89
+ 'div',
90
+ { class: 'filter-rail-group' },
91
+ [el('h4', {}, 'AI'), renderChip('AI processing', currentFilters.ai === true, () => onFiltersChange({ ...currentFilters, ai: !currentFilters.ai }))],
92
+ );
93
+
94
+ railEl.appendChild(el('div', { class: 'filter-rail' }, [
95
+ dataClassChips, protectionChips,
96
+ sourceCategoryChips, sinkCategoryChips, destinationExternalityChips,
97
+ transitVerdictChips, atRestVerdictChips, handlingVerdictChips,
98
+ policyVerdictChips, aiChip,
99
+ ]));
100
+ }
101
+
102
+ function toggleListFilter(currentFilters, key, value, onFiltersChange) {
103
+ const current = currentFilters[key] ?? [];
104
+ const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
105
+ onFiltersChange({ ...currentFilters, [key]: next });
106
+ }
107
+
108
+ function renderChip(label, active, onClick) {
109
+ return el(
110
+ 'button',
111
+ {
112
+ class: 'filter-chip',
113
+ 'data-active': String(active),
114
+ 'aria-pressed': String(active),
115
+ onClick,
116
+ },
117
+ label,
118
+ );
119
+ }
@@ -0,0 +1,126 @@
1
+ // Query bar: PRD §15.2's query-language DSL, wired into a real text input
2
+ // plus two saved-view chips. Split the same way every other component/view
3
+ // in this repo is: a pure compute function and a thin DOM-building render
4
+ // function.
5
+ //
6
+ // `computeQueryBarViewModel` is deliberately SYNTAX-only (it calls
7
+ // `parseQuery`, never `compileQuery`) — it operates on `state` alone, no
8
+ // graph, matching the exact signature this task's own brief specifies. A
9
+ // query can still fail at EVALUATION time even after parsing cleanly (an
10
+ // unrecognized field name — see query-language.js's own `compileQuery`
11
+ // comment), which needs a real graph to detect. `compileQuerySafely` below
12
+ // is the graph-aware superset app.js actually uses to build a safe-to-call
13
+ // predicate; `computeQueryBarViewModel` stays graph-free so it can be
14
+ // unit-tested in total isolation, per the brief's own contract.
15
+
16
+ import { parseQuery, compileQuery } from '../lib/query-language.js';
17
+ import { el, clear } from '../lib/dom.js';
18
+
19
+ // The two saved-view query strings this sub-project's own task brief names,
20
+ // spot-checked against the real flagship fixture (frontend/src/data/
21
+ // flagship-graph.js) before finalizing: `class:PCI` matches 5 of 8 real
22
+ // flows, `class:(PII,PHI) AND ai:true` matches 1 of 8 — both non-empty and
23
+ // sensible (a broad PCI-exposure view vs. a narrow AI+regulated-data
24
+ // intersection), so neither needed adjustment from the brief's own text.
25
+ export const SAVED_VIEWS = Object.freeze([
26
+ Object.freeze({ label: 'PCI Exposure', query: 'class:PCI' }),
27
+ Object.freeze({ label: 'AI + Regulated Data', query: 'class:(PII,PHI) AND ai:true' }),
28
+ ]);
29
+
30
+ /**
31
+ * @param {{filters?: {query?: string}}} state
32
+ * @returns {{queryText: string, error: {message: string, pos: number} | null}}
33
+ */
34
+ export function computeQueryBarViewModel(state) {
35
+ const queryText = state.filters?.query ?? '';
36
+ const { error } = parseQuery(queryText);
37
+ return { queryText, error: error ?? null };
38
+ }
39
+
40
+ /**
41
+ * Compiles a query string end-to-end against a real graph: parse, compile,
42
+ * and a trial evaluation against every real flow in `graph` — so a caller
43
+ * gets EITHER a real, safe-to-call predicate OR a structured error, never a
44
+ * predicate that might throw partway through filtering some rows and not
45
+ * others. A malformed query (a syntax error, OR an unrecognized field name,
46
+ * which only throws at EVALUATION time — see query-language.js's own
47
+ * compileQuery comment) never narrows the active filter: on error this
48
+ * returns a pass-through predicate (`() => true`, matching every flow), so
49
+ * views render exactly as if no query were active rather than silently
50
+ * hiding all/some rows.
51
+ *
52
+ * @param {object} graph
53
+ * @param {string} queryText
54
+ * @returns {{predicate: (flow: object) => boolean, error: {message: string, pos: number|null} | null}}
55
+ */
56
+ export function compileQuerySafely(graph, queryText) {
57
+ const { ast, error } = parseQuery(queryText ?? '');
58
+ if (error) return { predicate: () => true, error };
59
+ try {
60
+ const predicate = compileQuery(ast, graph);
61
+ for (const flow of graph.flows) predicate(flow);
62
+ return { predicate, error: null };
63
+ } catch (err) {
64
+ return { predicate: () => true, error: { message: err && err.message ? err.message : String(err), pos: null } };
65
+ }
66
+ }
67
+
68
+ function renderError(errorEl, error) {
69
+ errorEl.textContent = error ? `Query error: ${error.message}${typeof error.pos === 'number' ? ` (position ${error.pos})` : ''}` : '';
70
+ errorEl.setAttribute('data-visible', String(Boolean(error)));
71
+ }
72
+
73
+ /**
74
+ * @param {ReturnType<typeof computeQueryBarViewModel>} viewModel
75
+ * @param {HTMLElement} railEl
76
+ * @param {(nextQuery: string) => void} onQueryChange - called ONLY with a
77
+ * query that parses cleanly. On a parse error the input's own displayed
78
+ * value still updates (so the user can see/fix what they typed) but this
79
+ * is never invoked with the broken text — the caller's active filter
80
+ * never changes on a malformed query.
81
+ */
82
+ export function renderQueryBar(viewModel, railEl, onQueryChange) {
83
+ clear(railEl);
84
+
85
+ const errorEl = el('div', { class: 'query-bar__error' }, '');
86
+
87
+ const input = el('input', {
88
+ type: 'text',
89
+ class: 'query-bar__input',
90
+ value: viewModel.queryText,
91
+ placeholder: 'e.g. class:PCI AND ai:true',
92
+ 'aria-label': 'Query',
93
+ onInput: () => {
94
+ const text = input.value;
95
+ const { error } = parseQuery(text);
96
+ renderError(errorEl, error ?? null);
97
+ if (!error) onQueryChange(text);
98
+ },
99
+ });
100
+ renderError(errorEl, viewModel.error);
101
+ input.setAttribute('aria-invalid', String(Boolean(viewModel.error)));
102
+
103
+ const chips = el(
104
+ 'div',
105
+ { class: 'query-bar__saved-views' },
106
+ SAVED_VIEWS.map((view) =>
107
+ el(
108
+ 'button',
109
+ {
110
+ class: 'query-bar__saved-view-chip',
111
+ type: 'button',
112
+ 'data-query': view.query,
113
+ onClick: () => {
114
+ input.value = view.query;
115
+ input.setAttribute('value', view.query);
116
+ renderError(errorEl, null);
117
+ onQueryChange(view.query);
118
+ },
119
+ },
120
+ view.label,
121
+ ),
122
+ ),
123
+ );
124
+
125
+ railEl.appendChild(el('div', { class: 'query-bar' }, [input, errorEl, chips]));
126
+ }