@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,111 @@
|
|
|
1
|
+
// federation-loader.js — M5 deliverable #8 (FR-304's "declared" half):
|
|
2
|
+
// loadRemoteGraphExport(filePath) reads an exportGraphJSON-shaped file
|
|
3
|
+
// (dataflow export --format json's own artifact) — chosen over the
|
|
4
|
+
// local server's signed-graph reader (`scanner/src/server/`, whose
|
|
5
|
+
// loadSignedGraph is what `agentic-security explore` uses) for the
|
|
6
|
+
// cross-machine reason this deliverable's own scoping investigation
|
|
7
|
+
// found: loadSignedGraph authenticates against a PER-INSTALL HMAC key,
|
|
8
|
+
// which is the wrong trust model for a file that crossed a repo/machine
|
|
9
|
+
// boundary in the common case (two repos scanned on two different
|
|
10
|
+
// machines sign under two different keys by default, so pointing
|
|
11
|
+
// loadSignedGraph at a second repo's checkout would, in the common case,
|
|
12
|
+
// correctly report 'tampered' even though nothing was actually
|
|
13
|
+
// tampered with). exportGraphJSON's portable, embedded-digest artifact
|
|
14
|
+
// is a SELF-CONSISTENCY check instead — never authentication, disclosed
|
|
15
|
+
// as such everywhere this module or its callers describe it.
|
|
16
|
+
//
|
|
17
|
+
// Mirrors that reader's own four-distinct-outcome discipline, with one
|
|
18
|
+
// structural difference: a digest mismatch here is NOT a blocking
|
|
19
|
+
// failure (`ok:false`) the way all four of its reasons are — it is a
|
|
20
|
+
// WARNING the caller must show, never silently swallowed, and does not
|
|
21
|
+
// by itself block a --yes write (the operator is explicitly asserting
|
|
22
|
+
// this file). `ok:true, digestMatches:false` is therefore a real,
|
|
23
|
+
// valid, non-failing outcome; only `missing`/`malformed`/
|
|
24
|
+
// `invalid-graph` set `ok:false`.
|
|
25
|
+
//
|
|
26
|
+
// The self-consistency check recomputes `computeGraphDigest(parsed.graph)`
|
|
27
|
+
// and compares it to the file's own embedded `bodyDigest` (final
|
|
28
|
+
// whole-branch review, M5 deliverable #8, B1 — NOT `digest`, a real bug
|
|
29
|
+
// that survived five clean task-level reviews). `export-json.js`'s
|
|
30
|
+
// `digest` field always identifies the SOURCE graph an export was taken
|
|
31
|
+
// from, regardless of redaction/filtering — comparing it against a
|
|
32
|
+
// recomputation over `parsed.graph` (which IS the redacted/filtered
|
|
33
|
+
// `graph:` body under the CLI's own default settings) fails under the
|
|
34
|
+
// exact common case a self-consistency check exists to pass: an
|
|
35
|
+
// un-tampered, default-redacted export would permanently read
|
|
36
|
+
// `digestMatches: false`, and a genuine tamper would be indistinguishable
|
|
37
|
+
// from routine redaction. `bodyDigest` identifies the exact bytes this
|
|
38
|
+
// file actually contains, which is what a self-consistency check over a
|
|
39
|
+
// received file needs.
|
|
40
|
+
|
|
41
|
+
import * as fs from 'node:fs';
|
|
42
|
+
import { validateGraph } from './validate.js';
|
|
43
|
+
import { computeGraphDigest } from './export-json.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} filePath
|
|
47
|
+
* @returns {{
|
|
48
|
+
* ok: boolean,
|
|
49
|
+
* graph: object|null,
|
|
50
|
+
* digest: string|null,
|
|
51
|
+
* digestMatches: boolean|null,
|
|
52
|
+
* reason: 'missing'|'malformed'|'invalid-graph'|'digest-mismatch'|null,
|
|
53
|
+
* message: string|null,
|
|
54
|
+
* }}
|
|
55
|
+
*/
|
|
56
|
+
export function loadRemoteGraphExport(filePath) {
|
|
57
|
+
if (!filePath || typeof filePath !== 'string' || !fs.existsSync(filePath)) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false, graph: null, digest: null, digestMatches: null, reason: 'missing',
|
|
60
|
+
message: `No remote graph export found at ${filePath}. Run \`dataflow export --format json\` in the remote repository and point --remote-graph at the resulting file.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let body;
|
|
65
|
+
try {
|
|
66
|
+
body = fs.readFileSync(filePath, 'utf8');
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false, graph: null, digest: null, digestMatches: null, reason: 'missing',
|
|
70
|
+
message: `Remote graph export found at ${filePath} but could not be read: ${e && e.message ? e.message : e}.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(body);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false, graph: null, digest: null, digestMatches: null, reason: 'malformed',
|
|
80
|
+
message: `Remote graph export at ${filePath} is not valid JSON (${e && e.message ? e.message : e}).`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
85
|
+
|| typeof parsed.bodyDigest !== 'string' || !parsed.bodyDigest
|
|
86
|
+
|| !parsed.graph || typeof parsed.graph !== 'object' || Array.isArray(parsed.graph)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false, graph: null, digest: null, digestMatches: null, reason: 'malformed',
|
|
89
|
+
message: `Remote graph export at ${filePath} does not look like an \`exportGraphJSON\` artifact — expected top-level "bodyDigest" (string) and "graph" (object) fields. Run \`dataflow export --format json\` to produce a valid one.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { valid, errors } = validateGraph(parsed.graph);
|
|
94
|
+
if (!valid) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false, graph: null, digest: parsed.bodyDigest, digestMatches: null, reason: 'invalid-graph',
|
|
97
|
+
message: `Remote graph export at ${filePath} does not contain a well-formed DataFlowGraph v1 document: ${errors.map((e) => `${e.path}: ${e.message}`).join('; ')}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const recomputed = computeGraphDigest(parsed.graph);
|
|
102
|
+
const digestMatches = recomputed === parsed.bodyDigest;
|
|
103
|
+
if (!digestMatches) {
|
|
104
|
+
return {
|
|
105
|
+
ok: true, graph: parsed.graph, digest: parsed.bodyDigest, digestMatches: false, reason: 'digest-mismatch',
|
|
106
|
+
message: 'WARNING: the remote export\'s embedded digest does not match its own content (self-consistency check failed) — this is NOT authentication, only a check that the file has not been altered since it was exported. Proceeding is a real trust decision the operator is making explicitly.',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { ok: true, graph: parsed.graph, digest: parsed.bodyDigest, digestMatches: true, reason: null, message: null };
|
|
111
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { pathIsCoveredByPrefix } from '../dataflow/access-paths.js';
|
|
2
|
+
|
|
3
|
+
// State: Map<accessPath: string, Set<dataElementId: string>>. See
|
|
4
|
+
// scanner/src/lineage/DESIGN_INTRAPROCEDURAL.md for the full design
|
|
5
|
+
// rationale (why this shape instead of dataflow/access-paths.js's
|
|
6
|
+
// Set<accessPath>, ancestor/descendant asymmetry, why redundant
|
|
7
|
+
// ancestor+descendant entries are an accepted, deferred optimization).
|
|
8
|
+
|
|
9
|
+
export function emptyState() {
|
|
10
|
+
return new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function identitiesAt(state, path) {
|
|
14
|
+
const result = new Set();
|
|
15
|
+
const exact = state.get(path);
|
|
16
|
+
if (exact) for (const id of exact) result.add(id);
|
|
17
|
+
for (const [candidatePath, ids] of state) {
|
|
18
|
+
if (candidatePath === path) continue;
|
|
19
|
+
// Ancestor coverage (pre-existing): a coarser recorded fact about a
|
|
20
|
+
// container implies the same fact about anything under it, when nothing
|
|
21
|
+
// more specific overrides it.
|
|
22
|
+
// Descendant coverage (NEW, fixing FR-301's real violation — see the
|
|
23
|
+
// "Corrected design" note in DESIGN_INTRAPROCEDURAL.md §3 for the full
|
|
24
|
+
// story): asking about a container AS A WHOLE (e.g. `return rec` after
|
|
25
|
+
// `rec = {email: X, ssn: Y}`) must aggregate everything recorded under
|
|
26
|
+
// it — the object legitimately carries every field's identity. This does
|
|
27
|
+
// NOT reintroduce cross-field leakage: querying `rec.email` never picks
|
|
28
|
+
// up a SIBLING path like `rec.ssn` (neither is a prefix of the other),
|
|
29
|
+
// it only aggregates when you ask about an actual ancestor of both.
|
|
30
|
+
if (pathIsCoveredByPrefix(path, candidatePath) || pathIsCoveredByPrefix(candidatePath, path)) {
|
|
31
|
+
for (const id of ids) result.add(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function addIdentity(state, path, dataElementId) {
|
|
38
|
+
const current = state.get(path);
|
|
39
|
+
if (current && current.has(dataElementId)) return state;
|
|
40
|
+
const next = new Map(state);
|
|
41
|
+
next.set(path, new Set([...(current ?? []), dataElementId]));
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function removeIdentitiesAt(state, path) {
|
|
46
|
+
const next = new Map();
|
|
47
|
+
for (const [p, ids] of state) {
|
|
48
|
+
if (p === path || pathIsCoveredByPrefix(p, path)) continue;
|
|
49
|
+
next.set(p, ids);
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function joinStates(a, b) {
|
|
55
|
+
const next = new Map(a);
|
|
56
|
+
for (const [path, ids] of b) {
|
|
57
|
+
const current = next.get(path);
|
|
58
|
+
next.set(path, current ? new Set([...current, ...ids]) : new Set(ids));
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function statesEqual(a, b) {
|
|
64
|
+
if (a.size !== b.size) return false;
|
|
65
|
+
for (const [path, ids] of a) {
|
|
66
|
+
const other = b.get(path);
|
|
67
|
+
if (!other || other.size !== ids.size) return false;
|
|
68
|
+
for (const id of ids) if (!other.has(id)) return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function hashState(state) {
|
|
74
|
+
return [...state.entries()]
|
|
75
|
+
.map(([path, ids]) => `${path}=${[...ids].sort().join(',')}`)
|
|
76
|
+
.sort()
|
|
77
|
+
.join('|');
|
|
78
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Generates the deterministic flagship DataFlowGraph v1 fixture (Data
|
|
3
|
+
// Flow Explorer PRD Appendix D.2/D.3 — the synthetic "payments-platform"
|
|
4
|
+
// application). Run with `node build-flagship-fixture.mjs` from anywhere;
|
|
5
|
+
// writes flagship-graph.json next to this script. Re-running must
|
|
6
|
+
// produce byte-identical output (AC-14) — there is no Date.now()/random
|
|
7
|
+
// anywhere in this file; `generatedAt` is a fixed synthetic timestamp,
|
|
8
|
+
// per Appendix D.1's rule that fixture content can never leak
|
|
9
|
+
// non-reproducible values into what looks like a real scan artifact.
|
|
10
|
+
//
|
|
11
|
+
// PRD Appendix D.1: "Production UI code may not contain special cases
|
|
12
|
+
// keyed to fixture filenames, node names, endpoints, commits, authors, or
|
|
13
|
+
// expected verdicts." This generator is the ONE place fixture-specific
|
|
14
|
+
// facts are allowed to live; `graph.extensions.fixtureNodeKeys` /
|
|
15
|
+
// `fixtureFlowKeys` give tests and (later) UI fixtures a genuinely
|
|
16
|
+
// generic lookup table rather than hardcoded ids, without smuggling
|
|
17
|
+
// fixture-awareness into scanner/src/lineage's own production modules.
|
|
18
|
+
//
|
|
19
|
+
// One deliberate extension beyond Appendix D.2's literal 13-row table:
|
|
20
|
+
// D.3's flow.pii.analytics path text reads "Support/Registration source
|
|
21
|
+
// -> Events Service -> Analytics DB/Provider", naming an "Events Service"
|
|
22
|
+
// hop that has no row of its own in D.2. Rather than silently reusing an
|
|
23
|
+
// unrelated node for that role, this generator adds one extra node
|
|
24
|
+
// (node.events) to make the path the PRD itself describes actually
|
|
25
|
+
// representable. Documented here, not hidden.
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { emptyGraphEnvelope } from '../schema.js';
|
|
31
|
+
import { graphId, nodeId, dataElementId, edgeId, flowId, transformationId, evidenceId } from '../ids.js';
|
|
32
|
+
import { emptyProtection } from '../protection.js';
|
|
33
|
+
import { classifyDataElementName } from '../classification.js';
|
|
34
|
+
|
|
35
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const REPO = 'payments-platform';
|
|
37
|
+
const COMMIT = 'fixture0000000000000000000000000000000';
|
|
38
|
+
const GENERATED_AT = '2026-08-29T00:00:00.000Z';
|
|
39
|
+
|
|
40
|
+
function node({ key, kind, subtype, label, aliases = [], lifecycleStages = [], externality = 'internal', dataElementIds = [] }) {
|
|
41
|
+
const id = nodeId(kind, [REPO, key]);
|
|
42
|
+
return {
|
|
43
|
+
id, kind, subtype, label, aliases,
|
|
44
|
+
location: null,
|
|
45
|
+
system: { application: REPO, environment: 'production' },
|
|
46
|
+
destination: null,
|
|
47
|
+
externality: { value: externality, evidenceRefs: [] },
|
|
48
|
+
lifecycleStages, governanceRefs: {}, dataElementIds, evidenceRefs: [],
|
|
49
|
+
confidence: { score: 1, tier: 'high' },
|
|
50
|
+
coverageStatus: 'modeled',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dataElement(name, discriminator) {
|
|
55
|
+
const { classes } = classifyDataElementName(name);
|
|
56
|
+
return {
|
|
57
|
+
id: dataElementId(name, [REPO, discriminator]),
|
|
58
|
+
name, aliases: [], declaredType: null,
|
|
59
|
+
dataClasses: classes, aiContexts: [],
|
|
60
|
+
sourceLocations: [], dataSubjectCategory: null,
|
|
61
|
+
classificationEvidence: [], manualOverride: false,
|
|
62
|
+
firstSeenProvenance: { commit: COMMIT, note: 'fixture evidence only, not default production content' },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function edge({ from, to, mappings = [], protocolName = 'in-process', destinationResolution = 'literal', boundaryCrossings = [], protection = emptyProtection() }) {
|
|
67
|
+
// Discriminator includes toPath values and all dataElementIds carried in the mapping.
|
|
68
|
+
// Sort dataElementIds for determinism (matching flowId approach in ids.js).
|
|
69
|
+
const allDataElementIds = mappings.length > 0
|
|
70
|
+
? [...new Set(mappings.flatMap((m) => m.dataElementIds || []))].sort()
|
|
71
|
+
: [];
|
|
72
|
+
const toPathValues = mappings.map((m) => m.toPath);
|
|
73
|
+
const discriminatorParts = [...toPathValues, ...allDataElementIds];
|
|
74
|
+
return {
|
|
75
|
+
id: edgeId(from, to, 'data_flow', discriminatorParts),
|
|
76
|
+
from, to, relationship: 'data_flow',
|
|
77
|
+
fieldMappings: mappings,
|
|
78
|
+
protocol: { name: protocolName, destinationResolution },
|
|
79
|
+
boundaryCrossings, provenance: 'code', protection, evidenceRefs: [], coverageStatus: 'modeled',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function flow({ dataElementIds, source, sink, edgeIds, transformationIds = [], policyVerdict, protectionSummary, discriminator = [], governanceRefs = {}, limitations = [] }) {
|
|
84
|
+
return {
|
|
85
|
+
id: flowId(source, sink, dataElementIds, discriminator),
|
|
86
|
+
dataElementIds, source, sink, edgeIds, transformationIds,
|
|
87
|
+
alternatePathCount: 0, policyVerdict, protectionSummary,
|
|
88
|
+
evidenceRefs: [], confidence: { score: 1, tier: 'high' },
|
|
89
|
+
coverageStatus: 'modeled', findingRefs: [], governanceRefs, limitations,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function build() {
|
|
94
|
+
const graph = emptyGraphEnvelope({
|
|
95
|
+
graphId: graphId({ repository: REPO, commit: COMMIT, configHash: 'fixture-default' }),
|
|
96
|
+
generatedAt: GENERATED_AT,
|
|
97
|
+
scope: { source: 'fixture', repository: REPO, commit: COMMIT, environment: 'production' },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// --- Data elements (defined first: node.dataElementIds below is derived
|
|
101
|
+
// from which elements actually flow through each node per the edges/
|
|
102
|
+
// flows this generator builds, never guessed independently of them) ---
|
|
103
|
+
const cardNumber = dataElement('card_number', 'payments');
|
|
104
|
+
const diagnosis = dataElement('diagnosis', 'support');
|
|
105
|
+
const email = dataElement('email', 'events');
|
|
106
|
+
const cardIds = [cardNumber.id];
|
|
107
|
+
const phiIds = [diagnosis.id];
|
|
108
|
+
const piiIds = [email.id];
|
|
109
|
+
|
|
110
|
+
// --- Nodes (Appendix D.2 + the documented node.events extension) ---
|
|
111
|
+
// dataElementIds per node is derived from the edges/flows below: web is
|
|
112
|
+
// the shared collection point for all three elements; payments/logs/
|
|
113
|
+
// postgres/payment_api/ai/model see card_number (the PCI path fans out
|
|
114
|
+
// to log/db/external-API/AI sinks); ai/model/vector see diagnosis (the
|
|
115
|
+
// PHI-plus-AI path); events/analytics/unresolved see email (the PII
|
|
116
|
+
// path). gateway/retention/deletion have no data-carrying edge in this
|
|
117
|
+
// fixture (retention/deletion are declared process steps, not data-flow
|
|
118
|
+
// hops — see the comment at their edges below), so they stay empty.
|
|
119
|
+
const web = node({ key: 'web', kind: 'source', subtype: 'web-app', label: 'Web App', aliases: ['Checkout Form', 'Registration Form'], lifecycleStages: ['collection'], externality: 'internal', dataElementIds: [cardNumber.id, diagnosis.id, email.id] });
|
|
120
|
+
const gateway = node({ key: 'gateway', kind: 'api', subtype: 'api-gateway', label: 'API Gateway', lifecycleStages: ['processing'] });
|
|
121
|
+
const payments = node({ key: 'payments', kind: 'process', subtype: 'service', label: 'Payments Service', lifecycleStages: ['processing'], dataElementIds: [cardNumber.id] });
|
|
122
|
+
const events = node({ key: 'events', kind: 'process', subtype: 'service', label: 'Events Service', lifecycleStages: ['processing'], dataElementIds: [email.id] });
|
|
123
|
+
const ai = node({ key: 'ai', kind: 'process', subtype: 'ai-assistant', label: 'AI Assistant', lifecycleStages: ['processing'], dataElementIds: [cardNumber.id, diagnosis.id] });
|
|
124
|
+
const postgres = node({ key: 'postgres', kind: 'store', subtype: 'postgres-table', label: 'PostgreSQL', lifecycleStages: ['storage'], dataElementIds: [cardNumber.id] });
|
|
125
|
+
const logs = node({ key: 'logs', kind: 'log', subtype: 'application-logs', label: 'Application Logs', lifecycleStages: ['storage'], dataElementIds: [cardNumber.id] });
|
|
126
|
+
const paymentApi = node({ key: 'payment_api', kind: 'external', subtype: 'payment-api', label: 'Payment API', aliases: ['Payment Processor'], lifecycleStages: ['sharing'], externality: 'external', dataElementIds: [cardNumber.id] });
|
|
127
|
+
const analytics = node({ key: 'analytics', kind: 'external', subtype: 'analytics-api', label: 'Analytics API', aliases: ['Analytics Provider', 'Analytics DB'], lifecycleStages: ['sharing'], externality: 'external', dataElementIds: [email.id] });
|
|
128
|
+
const model = node({ key: 'model', kind: 'external', subtype: 'ai-model-provider', label: 'Model Provider', lifecycleStages: ['sharing'], externality: 'external', dataElementIds: [cardNumber.id, diagnosis.id] });
|
|
129
|
+
const vector = node({ key: 'vector', kind: 'store', subtype: 'vector-store', label: 'Vector Store', lifecycleStages: ['storage'], externality: 'unknown', dataElementIds: [diagnosis.id] });
|
|
130
|
+
const unresolved = node({ key: 'unresolved', kind: 'unresolved', subtype: 'unresolved-destination', label: 'Unresolved Destination', lifecycleStages: ['sharing'], externality: 'unknown', dataElementIds: [email.id] });
|
|
131
|
+
const retention = node({ key: 'retention', kind: 'process', subtype: 'retention-policy', label: 'Retention Policy', lifecycleStages: ['retention'] });
|
|
132
|
+
const deletion = node({ key: 'deletion', kind: 'process', subtype: 'deletion-job', label: 'Deletion Job', lifecycleStages: ['deletion'] });
|
|
133
|
+
|
|
134
|
+
[web, gateway, payments, events, ai, postgres, logs, paymentApi, analytics, model, vector, unresolved, retention, deletion]
|
|
135
|
+
.forEach((n) => { graph.nodes.push(n); });
|
|
136
|
+
[cardNumber, diagnosis, email].forEach((d) => { graph.dataElements.push(d); });
|
|
137
|
+
|
|
138
|
+
// --- flow.pci.masked_log: Web -> Payments -> maskCard() -> Application Logs (handling protected) ---
|
|
139
|
+
const maskTransform = { id: transformationId(payments.id, 'maskCard'), inputPath: 'payment.pan', outputPath: 'maskedPan', callee: 'maskCard', location: { file: 'services/payment.js', line: 55 }, kind: 'mask', reversibility: 'irreversible', algorithm: null, appliesToAllPaths: true, controlCredit: true, controlCreditReason: 'maskCard() proven on this branch (all feasible paths)' };
|
|
140
|
+
graph.transformations.push(maskTransform);
|
|
141
|
+
const e1a = edge({ from: web.id, to: payments.id, mappings: [{ fromPath: 'req.body.card_number', toPath: 'payment.pan', dataElementIds: cardIds, mappingType: 'rename', transformationIds: [] }] });
|
|
142
|
+
const maskedProtection = emptyProtection();
|
|
143
|
+
maskedProtection.handling = { verdict: 'protected', evidenceGrade: 'code' };
|
|
144
|
+
const e1b = edge({ from: payments.id, to: logs.id, mappings: [{ fromPath: 'payment.pan', toPath: 'maskedPan', dataElementIds: cardIds, mappingType: 'transformation', transformationIds: [maskTransform.id] }], protection: maskedProtection, boundaryCrossings: [] });
|
|
145
|
+
graph.edges.push(e1a, e1b);
|
|
146
|
+
const flowMaskedLog = flow({ dataElementIds: cardIds, source: web.id, sink: logs.id, edgeIds: [e1a.id, e1b.id], transformationIds: [maskTransform.id], policyVerdict: 'not_evaluated', protectionSummary: 'protected', discriminator: ['masked-branch'] });
|
|
147
|
+
graph.flows.push(flowMaskedLog);
|
|
148
|
+
|
|
149
|
+
// --- flow.pci.raw_log: Web -> Payments -> raw logger (RAW PCI, unprotected) ---
|
|
150
|
+
const e2 = edge({ from: payments.id, to: logs.id, mappings: [{ fromPath: 'payment.pan', toPath: 'payment.pan', dataElementIds: cardIds, mappingType: 'identity', transformationIds: [] }], protection: (() => { const p = emptyProtection(); p.handling = { verdict: 'unprotected', evidenceGrade: 'code' }; return p; })() });
|
|
151
|
+
graph.edges.push(e2);
|
|
152
|
+
const flowRawLog = flow({ dataElementIds: cardIds, source: web.id, sink: logs.id, edgeIds: [e1a.id, e2.id], policyVerdict: 'not_evaluated', protectionSummary: 'unprotected', discriminator: ['raw-branch'], limitations: ['RAW PCI: card_number logged without masking on this branch'] });
|
|
153
|
+
graph.flows.push(flowRawLog);
|
|
154
|
+
|
|
155
|
+
// --- flow.pci.database: Web -> Payments -> payments.pan (at rest unknown) ---
|
|
156
|
+
const dbProtection = emptyProtection();
|
|
157
|
+
dbProtection.atRest = { verdict: 'unknown', evidenceGrade: 'none' };
|
|
158
|
+
const e3 = edge({ from: payments.id, to: postgres.id, mappings: [{ fromPath: 'payment.pan', toPath: 'payments.pan', dataElementIds: cardIds, mappingType: 'identity', transformationIds: [] }], protection: dbProtection });
|
|
159
|
+
graph.edges.push(e3);
|
|
160
|
+
const flowDatabase = flow({ dataElementIds: cardIds, source: web.id, sink: postgres.id, edgeIds: [e1a.id, e3.id], policyVerdict: 'not_evaluated', protectionSummary: 'unknown', limitations: ['No correlated at-rest encryption configuration found for this store'] });
|
|
161
|
+
graph.flows.push(flowDatabase);
|
|
162
|
+
|
|
163
|
+
// --- flow.pci.payment_api: Web -> Payments -> http://payments.example/charge (transit unprotected) ---
|
|
164
|
+
const httpProtection = emptyProtection();
|
|
165
|
+
httpProtection.transit = { verdict: 'unprotected', evidenceGrade: 'code' };
|
|
166
|
+
const e4 = edge({ from: payments.id, to: paymentApi.id, mappings: [{ fromPath: 'payment.pan', toPath: 'payload.cardNumber', dataElementIds: cardIds, mappingType: 'rename', transformationIds: [] }], protocolName: 'http', destinationResolution: 'literal', boundaryCrossings: ['trust-zone:external'], protection: httpProtection });
|
|
167
|
+
graph.edges.push(e4);
|
|
168
|
+
const flowPaymentApi = flow({ dataElementIds: cardIds, source: web.id, sink: paymentApi.id, edgeIds: [e1a.id, e4.id], policyVerdict: 'not_evaluated', protectionSummary: 'unprotected', limitations: ['Cleartext HTTP scheme: no TLS termination evidence found'] });
|
|
169
|
+
graph.flows.push(flowPaymentApi);
|
|
170
|
+
|
|
171
|
+
// --- flow.pci.ai: Payments -> AI Assistant -> Model Provider (review) ---
|
|
172
|
+
const e5a = edge({ from: payments.id, to: ai.id, mappings: [{ fromPath: 'payment.pan', toPath: 'promptContext.paymentCard', dataElementIds: cardIds, mappingType: 'rename', transformationIds: [] }] });
|
|
173
|
+
const e5b = edge({ from: ai.id, to: model.id, mappings: [{ fromPath: 'promptContext.paymentCard', toPath: 'model.messages[].content', dataElementIds: cardIds, mappingType: 'projection', transformationIds: [] }], boundaryCrossings: ['trust-zone:external'] });
|
|
174
|
+
graph.edges.push(e5a, e5b);
|
|
175
|
+
const flowPciAi = flow({ dataElementIds: cardIds, source: web.id, sink: model.id, edgeIds: [e1a.id, e5a.id, e5b.id], policyVerdict: 'manual_review_required', protectionSummary: 'unknown', governanceRefs: { recipient: 'manual_required', purpose: 'manual_required', lawfulBasis: 'manual_required' }, limitations: ['AI recipient/purpose/retention evidence not established from code alone'] });
|
|
176
|
+
graph.flows.push(flowPciAi);
|
|
177
|
+
|
|
178
|
+
// --- flow.phi.ai: Support Form (Web) -> AI Assistant -> Model Provider + Vector Store ---
|
|
179
|
+
const e6a = edge({ from: web.id, to: ai.id, mappings: [{ fromPath: 'req.body.diagnosis', toPath: 'promptContext.summary', dataElementIds: phiIds, mappingType: 'rename', transformationIds: [] }] });
|
|
180
|
+
const e6b = edge({ from: ai.id, to: model.id, mappings: [{ fromPath: 'promptContext.summary', toPath: 'model.messages[].content', dataElementIds: phiIds, mappingType: 'projection', transformationIds: [] }], boundaryCrossings: ['trust-zone:external'] });
|
|
181
|
+
const e6c = edge({ from: ai.id, to: vector.id, mappings: [{ fromPath: 'promptContext.summary', toPath: 'vector.document', dataElementIds: phiIds, mappingType: 'transformation', transformationIds: [] }] });
|
|
182
|
+
graph.edges.push(e6a, e6b, e6c);
|
|
183
|
+
const flowPhiAi = flow({
|
|
184
|
+
dataElementIds: phiIds, source: web.id, sink: model.id, edgeIds: [e6a.id, e6b.id, e6c.id],
|
|
185
|
+
policyVerdict: 'manual_review_required', protectionSummary: 'unknown',
|
|
186
|
+
governanceRefs: { lawfulBasis: 'manual_required', retention: 'unknown', transfer: 'review' },
|
|
187
|
+
limitations: ['Lawful basis, retention, and transfer mechanism not established from code alone'],
|
|
188
|
+
});
|
|
189
|
+
graph.flows.push(flowPhiAi);
|
|
190
|
+
|
|
191
|
+
// --- flow.pii.analytics: Web (Registration) -> Events Service -> Analytics API (90-day retention only if evidenced) ---
|
|
192
|
+
const e7a = edge({ from: web.id, to: events.id, mappings: [{ fromPath: 'req.body.email', toPath: 'event.email', dataElementIds: piiIds, mappingType: 'rename', transformationIds: [] }] });
|
|
193
|
+
const e7b = edge({ from: events.id, to: analytics.id, mappings: [{ fromPath: 'event.email', toPath: 'traits.email', dataElementIds: piiIds, mappingType: 'projection', transformationIds: [] }], boundaryCrossings: ['trust-zone:external'] });
|
|
194
|
+
graph.edges.push(e7a, e7b);
|
|
195
|
+
const flowPiiAnalytics = flow({
|
|
196
|
+
dataElementIds: piiIds, source: web.id, sink: analytics.id, edgeIds: [e7a.id, e7b.id],
|
|
197
|
+
policyVerdict: 'not_evaluated', protectionSummary: 'unknown',
|
|
198
|
+
governanceRefs: { retention: 'unknown', deletion: 'not_found' },
|
|
199
|
+
limitations: ['No correlated retention/deletion policy evidence found for this recipient'],
|
|
200
|
+
});
|
|
201
|
+
graph.flows.push(flowPiiAnalytics);
|
|
202
|
+
// Retention/deletion process nodes are wired for the fixture's governance
|
|
203
|
+
// story even without a resolved edge protection verdict — they represent
|
|
204
|
+
// declared process steps, not a data-flow edge with its own verdict.
|
|
205
|
+
graph.edges.push(edge({ from: analytics.id, to: retention.id, mappings: [] }));
|
|
206
|
+
graph.edges.push(edge({ from: retention.id, to: deletion.id, mappings: [] }));
|
|
207
|
+
|
|
208
|
+
// --- flow.pii.unresolved: inbound source -> dynamic outbound call ---
|
|
209
|
+
const e8 = edge({ from: web.id, to: unresolved.id, mappings: [{ fromPath: 'req.body.email', toPath: 'unknown', dataElementIds: piiIds, mappingType: 'unknown', transformationIds: [] }], destinationResolution: 'dynamic', boundaryCrossings: ['trust-zone:unknown'] });
|
|
210
|
+
graph.edges.push(e8);
|
|
211
|
+
const flowUnresolved = flow({
|
|
212
|
+
dataElementIds: piiIds, source: web.id, sink: unresolved.id, edgeIds: [e8.id],
|
|
213
|
+
policyVerdict: 'not_evaluated', protectionSummary: 'unknown',
|
|
214
|
+
limitations: ['Destination computed from an unresolved runtime value (dynamic URL expression)'],
|
|
215
|
+
});
|
|
216
|
+
graph.flows.push(flowUnresolved);
|
|
217
|
+
|
|
218
|
+
// --- Evidence (one representative entry per flow, matching PRD 16's
|
|
219
|
+
// four-question shape). Each entry is the evidence FOR the protection
|
|
220
|
+
// verdict on the specific edge/flow named in its claim, so it is wired
|
|
221
|
+
// back via evidenceRefs on that edge and that flow — an evidence object
|
|
222
|
+
// nothing references is structurally orphaned and proves nothing.
|
|
223
|
+
function evidence(claim, evType, note) {
|
|
224
|
+
return { id: evidenceId(claim, note), claim, evidenceType: evType, location: { note }, producer: 'lineage-fixture-builder', confidenceTier: 'high', snippet: null, timestamp: GENERATED_AT, commit: COMMIT, limitations: [], conflict: false };
|
|
225
|
+
}
|
|
226
|
+
const evMaskedLog = evidence('card_number reaches Application Logs via maskCard() on the masked branch', 'code', 'services/payment.js:55');
|
|
227
|
+
const evRawLog = evidence('card_number reaches Application Logs raw on a separate branch', 'code', 'services/payment.js:60');
|
|
228
|
+
const evDatabase = evidence('card_number reaches payments.pan with no correlated at-rest configuration', 'code', 'services/payment.js:70');
|
|
229
|
+
const evPaymentApi = evidence('card_number reaches http://payments.example/charge over cleartext HTTP', 'code', 'clients/gateway.js:72');
|
|
230
|
+
graph.evidence.push(evMaskedLog, evRawLog, evDatabase, evPaymentApi);
|
|
231
|
+
|
|
232
|
+
e1b.evidenceRefs = [evMaskedLog.id];
|
|
233
|
+
flowMaskedLog.evidenceRefs = [evMaskedLog.id];
|
|
234
|
+
e2.evidenceRefs = [evRawLog.id];
|
|
235
|
+
flowRawLog.evidenceRefs = [evRawLog.id];
|
|
236
|
+
e3.evidenceRefs = [evDatabase.id];
|
|
237
|
+
flowDatabase.evidenceRefs = [evDatabase.id];
|
|
238
|
+
e4.evidenceRefs = [evPaymentApi.id];
|
|
239
|
+
flowPaymentApi.evidenceRefs = [evPaymentApi.id];
|
|
240
|
+
|
|
241
|
+
graph.coverage = { languages: [{ language: 'js', filesExpected: 6, filesAnalyzed: 6 }], parseFailures: [], destinationResolutionStatus: 'complete-for-fixture', pathBudgetTruncation: false };
|
|
242
|
+
graph.limitations = ['This is a synthetic fixture graph, not a real repository scan. See scope.source.'];
|
|
243
|
+
graph.scanHealth = { status: 'complete', reason: 'fixture' };
|
|
244
|
+
graph.taxonomy = { version: '1.0.0', source: 'built-in + CONFIDENTIAL extension' };
|
|
245
|
+
|
|
246
|
+
graph.extensions = {
|
|
247
|
+
fixtureNodeKeys: {
|
|
248
|
+
'node.web': web.id, 'node.gateway': gateway.id, 'node.payments': payments.id,
|
|
249
|
+
'node.ai': ai.id, 'node.postgres': postgres.id, 'node.logs': logs.id,
|
|
250
|
+
'node.payment_api': paymentApi.id, 'node.analytics': analytics.id, 'node.model': model.id,
|
|
251
|
+
'node.vector': vector.id, 'node.unresolved': unresolved.id, 'node.retention': retention.id,
|
|
252
|
+
'node.deletion': deletion.id, 'node.events': events.id,
|
|
253
|
+
},
|
|
254
|
+
fixtureFlowKeys: {
|
|
255
|
+
'flow.pci.masked_log': flowMaskedLog.id, 'flow.pci.raw_log': flowRawLog.id,
|
|
256
|
+
'flow.pci.database': flowDatabase.id, 'flow.pci.payment_api': flowPaymentApi.id,
|
|
257
|
+
'flow.pci.ai': flowPciAi.id, 'flow.phi.ai': flowPhiAi.id,
|
|
258
|
+
'flow.pii.analytics': flowPiiAnalytics.id, 'flow.pii.unresolved': flowUnresolved.id,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return graph;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function main() {
|
|
266
|
+
const graph = build();
|
|
267
|
+
const outPath = path.join(__dirname, 'flagship-graph.json');
|
|
268
|
+
fs.writeFileSync(outPath, JSON.stringify(graph, null, 2) + '\n');
|
|
269
|
+
console.log(`wrote ${outPath}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
main();
|