@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,355 @@
|
|
|
1
|
+
# DESIGN_HANDLING_ANALYZER.md — Sub-project D's binding design record (Milestone 2, increments 1-2)
|
|
2
|
+
|
|
3
|
+
**Status:** landed as Milestone 2, Sub-project D, increments **1** (FR-403's
|
|
4
|
+
taxonomy label, §1-§4) and **2** (FR-307's multi-path control-credit rule,
|
|
5
|
+
§5) — two slices of a "Large" sub-project, per
|
|
6
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-d1-plan.md`
|
|
7
|
+
and
|
|
8
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-d2-plan.md`.
|
|
9
|
+
Binding on later Sub-project D increments the same way `DESIGN_
|
|
10
|
+
DESTINATION_RESOLVER.md` binds Sub-project A — MUCH shorter than
|
|
11
|
+
`DESIGN_GRAPH_BUILDER.md`/`DESIGN_PATH_PROVENANCE.md`/`DESIGN_REGISTRIES.md`
|
|
12
|
+
on purpose: this is two slices of a larger sub-project, not the whole thing.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. What this increment actually is
|
|
17
|
+
|
|
18
|
+
FR-403 asks: for a field reaching a sink, what HANDLING was applied to it
|
|
19
|
+
along the way — raw, masked, hashed, tokenized, encrypted, etc.? Increment 1
|
|
20
|
+
produces that TAXONOMY LABEL for one already-reconstructed path (the caller
|
|
21
|
+
picks which path — increment 1 does not iterate multiple paths to one
|
|
22
|
+
sink). Increment 1 deliberately did NOT implement FR-307's multi-path
|
|
23
|
+
control-credit rule (AC-12: "a transform on one branch cannot make the full
|
|
24
|
+
flow green") — that needed comparing MULTIPLE paths to the same sink, named
|
|
25
|
+
here at the time as a distinct, larger follow-up (D2) rather than silently
|
|
26
|
+
attempted. **D2 is now done** — see §5 below.
|
|
27
|
+
|
|
28
|
+
No new detection. This increment is entirely a RECLASSIFICATION of
|
|
29
|
+
`transform-catalog.js`'s already-shipped `recognizeTransformation` output
|
|
30
|
+
(Sub-project D, increment 4) onto a new, narrower vocabulary, applied to a
|
|
31
|
+
single reconstructed path's hops (Sub-project C's already-shipped
|
|
32
|
+
`path-query.js#reconstructPaths`).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. The `handling` verdict shape and the kind → HANDLING_VALUES mapping
|
|
37
|
+
|
|
38
|
+
`classifyHandling(path, callGraph)` (`handling-analyzer.js`) returns:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
{
|
|
42
|
+
handling: HandlingValue, // schema.js's HANDLING_VALUES
|
|
43
|
+
recognizedTransform: TransformDecision | null, // transform-catalog.js's
|
|
44
|
+
// own recognizeTransformation()
|
|
45
|
+
// return shape, unmodified
|
|
46
|
+
hopIndex: number | null, // index into path.hops where the
|
|
47
|
+
// transform was found, else null
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`schema.js`'s `HANDLING_VALUES` is FR-403's own 8-value taxonomy: `raw`,
|
|
52
|
+
`masked`, `redacted`, `hashed`, `tokenized`, `encrypted`, `aggregated`,
|
|
53
|
+
`unknown`.
|
|
54
|
+
|
|
55
|
+
The `transform-catalog.js` `kind` → `HANDLING_VALUES` mapping:
|
|
56
|
+
|
|
57
|
+
| `kind` | `handling` | Why |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `mask` | `masked` | direct |
|
|
60
|
+
| `redact` | `redacted` | direct |
|
|
61
|
+
| `hash` | `hashed` | direct |
|
|
62
|
+
| `tokenize` | `tokenized` | direct |
|
|
63
|
+
| `encrypt` | `encrypted` | direct |
|
|
64
|
+
| `decrypt` | `unknown` | a decrypt immediately before a sink is actively the OPPOSITE of protection — never mapped to a protective label |
|
|
65
|
+
| `encode` | `unknown` | encoding is reversible obfuscation, not itself a protective handling state |
|
|
66
|
+
| `decode` | `unknown` | the direct counterpart of `encode` |
|
|
67
|
+
| `truncate` | `unknown` | general-purpose shortening, not necessarily a privacy control (`transform-catalog.js`'s own disclosed low-confidence note) |
|
|
68
|
+
| `normalize` | `unknown` | reversibility itself is `unknown` at the catalog level; no protective claim is possible |
|
|
69
|
+
| `aggregate` | `unknown` | **disclosed, not a drive-by decision**: `HANDLING_VALUES` carries its own `aggregated` value, but awarding it needs shape-level reasoning about a WHOLE collection (is EVERY record aggregated, or just this one hop's local variable?) that a single-hop, single-path classifier cannot do soundly. Deferred beyond D2, still open (§6) — `classifyHandling` never emits `'aggregated'`, the same way `transform-catalog.js` itself never emits `custom`/`unknown` as a recognized kind: an honest gap, not a guess. |
|
|
70
|
+
|
|
71
|
+
`custom`/`unknown` never reach this table because `recognizeTransformation`
|
|
72
|
+
never emits them (`transform-catalog.js`'s own documented contract) — a
|
|
73
|
+
`null` `recognizeTransformation` result (no match at that hop) is simply
|
|
74
|
+
skipped, not routed through this table at all.
|
|
75
|
+
|
|
76
|
+
A path with NO recognized transform on ANY hop, and a literal/direct field
|
|
77
|
+
reaching the sink, is `raw` — `classifyHandling`'s own fallback when the
|
|
78
|
+
walk exhausts every hop with no match:
|
|
79
|
+
`{handling: 'raw', recognizedTransform: null, hopIndex: null}`.
|
|
80
|
+
|
|
81
|
+
### A load-bearing naming note
|
|
82
|
+
|
|
83
|
+
`protection.js`'s `PROTECTION_DIMENSIONS` already includes a dimension
|
|
84
|
+
literally named `'handling'` — a per-EDGE `{verdict, evidenceGrade}` object
|
|
85
|
+
scored from `PROTECTION_VERDICTS` (`protected`/`unprotected`/`unknown`/
|
|
86
|
+
`not_applicable`/`not_assessed`). That is a DIFFERENT field
|
|
87
|
+
(`edge.protection.handling`), a DIFFERENT vocabulary, and a DIFFERENT
|
|
88
|
+
question ("is this dimension protected?" vs. "what taxonomy label applies
|
|
89
|
+
to this hop?"). The two share a name because this taxonomy is precisely
|
|
90
|
+
what a later Milestone 2 analyzer will read to DECIDE that verdict — see
|
|
91
|
+
`transform-catalog.js`'s own header: recognizing that a transform happened
|
|
92
|
+
is Sub-project D's job; deciding whether it earns "protected" is a later
|
|
93
|
+
FR-401-405 analyzer reading this output. This increment sets only
|
|
94
|
+
`flow.handling` (the taxonomy string). It never writes
|
|
95
|
+
`edge.protection.handling` and never reads it either.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 3. `classifyHandling(path, callGraph)` — the walk
|
|
100
|
+
|
|
101
|
+
Walks `path.hops` in the order `path-query.js` already materializes them
|
|
102
|
+
(source → sink — see that module's own header: "a human reads a flow
|
|
103
|
+
source -> sink, so the arrays are reversed here, once, at the point a
|
|
104
|
+
candidate is emitted"). For each hop:
|
|
105
|
+
|
|
106
|
+
1. Resolve `callGraph.functions.get(hop.scope)?.cfg?.nodes?.[hop.siteNodeId]`
|
|
107
|
+
defensively — `callGraph.functions` is a real `Map` (`ir/callgraph.js`'s
|
|
108
|
+
own shape), never a plain object; a missing/malformed lookup at any step
|
|
109
|
+
is `undefined`, not a throw, and the hop is skipped.
|
|
110
|
+
2. When that CFG node resolves, collect every `{kind: 'call'}` expression
|
|
111
|
+
reachable from it — mirroring `graph-builder.js`'s own per-hop
|
|
112
|
+
transformation-extraction loop (§7 of `DESIGN_GRAPH_BUILDER.md`)
|
|
113
|
+
exactly: `exprRoots`/`walkExpr` over the node (`source-seeding.js`,
|
|
114
|
+
already shipped) plus the node's OWN call when the node itself is
|
|
115
|
+
`kind: 'call'` (a bare call statement — `exprRoots` only ever yields a
|
|
116
|
+
call node's `callee`/`args`, never a `{kind:'call'}` shape for the node
|
|
117
|
+
itself, so `graph-builder.js` unshifts that case explicitly and this
|
|
118
|
+
module mirrors it). This is NOT the narrower "only literal `call`-kind
|
|
119
|
+
CFG nodes" reading a first pass at this plan's own wording might
|
|
120
|
+
suggest — the AC-02 fixture this increment must pass
|
|
121
|
+
(`maskCard(cardNumber)` as an assignment RHS) proves why: the hop
|
|
122
|
+
carrying the `call-resolved`/`call-arg-bind` production for that call is
|
|
123
|
+
stamped with the CALLER's `assign`-kind CFG node id (`engine.js`'s
|
|
124
|
+
`stepCtx` wrapper stamps every hop with the CURRENT worklist node,
|
|
125
|
+
whatever its kind), never a literal `call`-kind node. Restricting the
|
|
126
|
+
walk to `node.kind === 'call'` alone would silently never find
|
|
127
|
+
`maskCard` here at all — verified live before settling on this reading.
|
|
128
|
+
3. For each collected call, in expression order, build a
|
|
129
|
+
`transform-catalog.js` descriptor and call `recognizeTransformation`.
|
|
130
|
+
The FIRST recognized transform found — across the whole path, hop by
|
|
131
|
+
hop, call by call — wins; nothing later is consulted.
|
|
132
|
+
|
|
133
|
+
### Callee-descriptor construction: a disclosed third copy, not a cycle
|
|
134
|
+
|
|
135
|
+
`graph-builder.js` already has a private (unexported) `calleeDescriptor`
|
|
136
|
+
helper for this exact shape, and `registry-real-code.test.js` has its own
|
|
137
|
+
independent copy for a structural cross-check. This increment needs a
|
|
138
|
+
THIRD. Two ways to avoid tripling it were considered and rejected:
|
|
139
|
+
|
|
140
|
+
- **Export `calleeDescriptor` from `graph-builder.js` and import it here.**
|
|
141
|
+
Rejected: this increment ALSO wires `classifyHandling` INTO
|
|
142
|
+
`graph-builder.js`'s own flow-construction loop (§4 below) — so
|
|
143
|
+
`graph-builder.js` must import `handling-analyzer.js`. Importing
|
|
144
|
+
`calleeDescriptor` the other way would make the two files mutually
|
|
145
|
+
dependent, the exact shape `coverage.js`/`resolve-destination.js` already
|
|
146
|
+
carry and justify carefully (their header comments explain why it's safe
|
|
147
|
+
there). Introducing a SECOND such cycle for a ~10-line helper is not
|
|
148
|
+
worth the added reasoning burden.
|
|
149
|
+
- **Move `calleeDescriptor` into `source-seeding.js`**, which already hosts
|
|
150
|
+
the shared `exprRoots`/`walkExpr` primitives both `graph-builder.js` and
|
|
151
|
+
this module import. Rejected as out of this increment's stated scope —
|
|
152
|
+
`source-seeding.js` is not on the plan's file list, and widening its
|
|
153
|
+
reuse boundary is a real, separate decision this increment should not
|
|
154
|
+
make as a side effect.
|
|
155
|
+
|
|
156
|
+
`handling-analyzer.js` therefore carries its own small, private
|
|
157
|
+
`calleeDescriptorOf`, byte-for-byte the same logic as `graph-builder.js`'s
|
|
158
|
+
own — a disclosed, judgment-call duplicate, not an oversight.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 4. Wiring — inside `graph-builder.js`'s flow-construction loop
|
|
163
|
+
|
|
164
|
+
`flow.handling` is set once, at FLOW-MINT time, inside
|
|
165
|
+
`buildDataFlowGraph`'s `groupsByFlowKey` loop — the same place
|
|
166
|
+
`protection.js`'s `emptyProtection()` is attached to an EDGE object
|
|
167
|
+
one loop up. `classifyHandling(p, callGraph)` is called on `group[0].p`
|
|
168
|
+
(the flow's own representative reconstructed `Path`, already in scope in
|
|
169
|
+
that loop) and its `.handling` string is written to `flow.handling`.
|
|
170
|
+
|
|
171
|
+
Per this increment's own simplification: EVERY flow gets a real
|
|
172
|
+
`flow.handling` value — never `null` — since every sink category
|
|
173
|
+
plausibly has SOME handling answer (even `'raw'` for a field that reached
|
|
174
|
+
it untransformed). A future increment MAY special-case a sink category
|
|
175
|
+
with no natural "handling" concept to `null` instead of a fabricated
|
|
176
|
+
`'raw'`; this increment does not attempt that distinction.
|
|
177
|
+
|
|
178
|
+
`classifyHandling` is computed ONCE, at graph-build time, from the flow's
|
|
179
|
+
OWN reconstructed path — consistent with how every other flow field is
|
|
180
|
+
computed today (per this plan's own "Explicitly deferred" list: no
|
|
181
|
+
re-derivation if a flow's underlying path later changes).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 5. FR-307 multi-path control-credit (increment 2)
|
|
186
|
+
|
|
187
|
+
**Status:** landed as Milestone 2, Sub-project D, increment **2**, per
|
|
188
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-d2-plan.md`.
|
|
189
|
+
Computes `transformation.appliesToAllPaths` — the field `graph-builder.js`
|
|
190
|
+
had stubbed at `null` on every transformation entity since increment 1 (§4's
|
|
191
|
+
"honest absences" note) — and nothing else.
|
|
192
|
+
|
|
193
|
+
### Scope boundary
|
|
194
|
+
|
|
195
|
+
This increment does **NOT** compute `flow.protectionSummary` as
|
|
196
|
+
`mixed`/`unprotected` (AC-12's own literal end-to-end verdict wording) —
|
|
197
|
+
that field stays `'not_assessed'`, exactly as before. Populating it for real
|
|
198
|
+
needs a real protection-verdict analyzer (Sub-project B/C's transit/at-rest
|
|
199
|
+
analyzers, or Sub-project G's policy verdict), none of which exist yet.
|
|
200
|
+
`appliesToAllPaths` is the load-bearing SIGNAL those future analyzers will
|
|
201
|
+
consume, not the verdict itself.
|
|
202
|
+
|
|
203
|
+
### The rule
|
|
204
|
+
|
|
205
|
+
For every **coarse key** `[src.id, snk.id, de.id].join('|')` — deliberately
|
|
206
|
+
coarser than `groupsByFlowKey`'s own key, since it drops `shape`/`grade`/
|
|
207
|
+
`sortedT`, so every flow group reaching the same sink for the same field
|
|
208
|
+
from the same source, regardless of which transforms were found on it,
|
|
209
|
+
collapses into one coarse group:
|
|
210
|
+
|
|
211
|
+
1. Collect every DISTINCT flow group (post-dedup — `groupsByFlowKey`'s own
|
|
212
|
+
entries, not raw reconstructed paths) sharing that coarse key, and each
|
|
213
|
+
one's `sortedT` (its ordered transformation-id array).
|
|
214
|
+
2. For every transformation id `tid` appearing in ANY flow group's
|
|
215
|
+
`sortedT` within that coarse group: `tid` "applies to all paths" for
|
|
216
|
+
THIS coarse group iff `sortedT.includes(tid)` is true for **every** flow
|
|
217
|
+
group in the coarse group — not just the ones where it appears.
|
|
218
|
+
3. Write the result onto `transformsById.get(tid).appliesToAllPaths`.
|
|
219
|
+
|
|
220
|
+
Note that `groupsByFlowKey`'s own key already includes the transformation-id
|
|
221
|
+
set (`sortedT.join(',')`), which was Milestone 1's own flow-key design, not
|
|
222
|
+
something this increment needed to build — two paths to the same
|
|
223
|
+
`(source, sink, dataElement)` that apply DIFFERENT transforms already landed
|
|
224
|
+
in DIFFERENT flow groups before this increment, each with its own
|
|
225
|
+
`flow.handling`. This increment adds the cross-group AGGREGATION on top:
|
|
226
|
+
comparing every flow group in a coarse key against every other one, which
|
|
227
|
+
`groupsByFlowKey`'s own per-group key cannot do by itself.
|
|
228
|
+
|
|
229
|
+
### Conjunction across coarse groups (a disclosed rare-collision case)
|
|
230
|
+
|
|
231
|
+
A transformation's own id (`ids.transformationId(anchor, ...)`) is keyed on
|
|
232
|
+
the SINK node id, not the data-element id — a `transformsById` entry can in
|
|
233
|
+
principle (rare, not observed in any shipped fixture) be referenced by flows
|
|
234
|
+
for more than one data element, if two different fields happen to produce
|
|
235
|
+
byte-identical `(file, line, fromPath, toPath)` at the same sink. If the
|
|
236
|
+
SAME `tid` is relevant to more than one coarse group, the final
|
|
237
|
+
`appliesToAllPaths` is the AND of every coarse group's own answer for that
|
|
238
|
+
`tid` — `true` only if every coarse group where it is relevant judges it
|
|
239
|
+
`true`. This is the conservative (never-overclaim) direction, consistent
|
|
240
|
+
with FR-307's own "insufficient" framing and with Milestone 2's own
|
|
241
|
+
"false-protected" exit-gate concern: a release must never assert credit it
|
|
242
|
+
cannot prove. This case is disclosed but not separately fixture-tested — see
|
|
243
|
+
§5's own "Explicitly deferred" entry below.
|
|
244
|
+
|
|
245
|
+
### Why a truncated/incomplete path needs no special-case code
|
|
246
|
+
|
|
247
|
+
If one path to a sink was cut short by `path-query.js`'s own budget/depth
|
|
248
|
+
limits before reaching a transform call site, that path's own flow-group
|
|
249
|
+
`sortedT` simply won't contain that transform's id — the hop was never
|
|
250
|
+
walked, so `recognizeTransformation` never ran on it. Under the rule above,
|
|
251
|
+
that flow group's absence of `tid` in its own `sortedT` already forces
|
|
252
|
+
`appliesToAllPaths` to `false` for any `tid` present in a SIBLING flow group
|
|
253
|
+
in the same coarse key — the conservative answer falls out of the existing
|
|
254
|
+
conjunction, with no additional "is this path incomplete" branch needed.
|
|
255
|
+
`test/lineage/graph-builder.test.js`'s truncated-path test proves this is
|
|
256
|
+
real, not just argued in prose, by forcing a real `path-query.js` depth
|
|
257
|
+
truncation (via `buildDataFlowGraph`'s existing `opts.budget` passthrough)
|
|
258
|
+
on one branch while a sibling branch's transform completes.
|
|
259
|
+
|
|
260
|
+
### Implementation
|
|
261
|
+
|
|
262
|
+
`graph-builder.js` only — a second pass over data the existing
|
|
263
|
+
flow-construction loop already builds (`groupsByFlowKey`/`transformsById`),
|
|
264
|
+
not new detection. Runs strictly after the flow-construction loop finishes
|
|
265
|
+
populating those maps and before `graph.transformations`/`graph.flows` are
|
|
266
|
+
assigned from them. See that file's own inline comment at the aggregation
|
|
267
|
+
block for the exact code.
|
|
268
|
+
|
|
269
|
+
## 6. Explicitly deferred (named, not silently skipped)
|
|
270
|
+
|
|
271
|
+
- **`aggregate`'s own `'aggregated'` verdict** — needs shape-level
|
|
272
|
+
reasoning about a WHOLE collection, not a single hop. See §2's table.
|
|
273
|
+
Deferred, not scheduled.
|
|
274
|
+
- **Any UI/display of the handling taxonomy.**
|
|
275
|
+
- **Re-deriving `handling` for a flow whose reconstructed path changes**
|
|
276
|
+
after a later Sub-project A/E change — this increment computes it once,
|
|
277
|
+
at graph-build time, same as every other flow field.
|
|
278
|
+
- **Populating `edge.protection.handling`'s own verdict** from this
|
|
279
|
+
taxonomy — a later Milestone 2 analyzer's job, not this increment's
|
|
280
|
+
(§2's naming note).
|
|
281
|
+
- **`flow.protectionSummary` computation** (`mixed`/`unprotected`/
|
|
282
|
+
`protected`) — AC-12's own literal end-to-end verdict wording — stays
|
|
283
|
+
`'not_assessed'`. Needs a real protection-verdict analyzer consuming
|
|
284
|
+
`appliesToAllPaths` as one of its inputs (Sub-project B/C/G, none built
|
|
285
|
+
yet); increment 2 produces the SIGNAL, not the verdict.
|
|
286
|
+
- **The rare same-call-site-different-data-element `t.id` collision** (see
|
|
287
|
+
§5's "Conjunction across coarse groups") — handled conservatively (AND
|
|
288
|
+
across groups) but not separately tested with a dedicated collision
|
|
289
|
+
fixture, since no real catalog/parser shape produces it today.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 7. A second consumer: `edge.protection.atRest` (Milestone 2, Sub-project C, increment 1)
|
|
294
|
+
|
|
295
|
+
**Status:** landed as Milestone 2, Sub-project C, increment **1** (FR-402's
|
|
296
|
+
application-layer at-rest evidence source), per
|
|
297
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-c1-plan.md`
|
|
298
|
+
and its own scoping doc. This is not a new analyzer module — it is a second,
|
|
299
|
+
additive consumer of the SAME `classifyHandling(p, callGraph)` result §4
|
|
300
|
+
already computes and writes to `flow.handling`, wired inline in
|
|
301
|
+
`graph-builder.js`'s flow-construction loop, right next to it.
|
|
302
|
+
|
|
303
|
+
FR-402 asks whether application-layer field encryption evidence sits
|
|
304
|
+
directly on the path to a store write. `classifyHandling`'s own
|
|
305
|
+
`KIND_TO_HANDLING` table (§2) already answers exactly that question for the
|
|
306
|
+
`'encrypted'` outcome — a recognized `transform-catalog.js` `encrypt`-kind
|
|
307
|
+
call found ON THE PATH — so this increment needed no new detection, only a
|
|
308
|
+
second write from the one already-computed result:
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
const handlingResult = classifyHandling(p, callGraph).handling;
|
|
312
|
+
if (handlingResult === 'encrypted' && snk.kind === 'store') {
|
|
313
|
+
const edge = edgesById.get(edgeIdStr);
|
|
314
|
+
if (edge) edge.protection.atRest = { verdict: 'protected', evidenceGrade: 'code' };
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`classifyHandling` is called exactly once per flow — the same call whose
|
|
319
|
+
result feeds `flow.handling` (§4) is reused for this check, never invoked a
|
|
320
|
+
second time.
|
|
321
|
+
|
|
322
|
+
**The gate.** `snk.kind === 'store'` — `sink-registry.js`'s
|
|
323
|
+
`CATEGORY_NODE_KIND` maps `database`/`file`/`object-storage`/`cache`/
|
|
324
|
+
`client-storage`/`backup`/`export` to `kind: 'store'`; `queue` has its own
|
|
325
|
+
distinct `kind: 'queue'` and is deliberately excluded (named as a future
|
|
326
|
+
widening question, not attempted here). Only `'encrypted'` triggers this —
|
|
327
|
+
every other `HANDLING_VALUES` member (`masked`/`hashed`/`tokenized`/`raw`/
|
|
328
|
+
`redacted`/`aggregated`/`unknown`) leaves `edge.protection.atRest` at
|
|
329
|
+
`emptyProtection()`'s own honest `{verdict: 'not_assessed', evidenceGrade:
|
|
330
|
+
'none'}` default. Neither `masking` nor `hashing` nor `tokenization` is
|
|
331
|
+
at-rest PROTECTION evidence in FR-402's own sense — only encryption is.
|
|
332
|
+
|
|
333
|
+
**The anti-pattern guard holds by construction, not by new code.** FR-402
|
|
334
|
+
explicitly warns that "a cipher present anywhere in the same file or
|
|
335
|
+
repository cannot alone establish protection for an unrelated store."
|
|
336
|
+
`classifyHandling` walks `path.hops` — THIS flow's own reconstructed path,
|
|
337
|
+
never "the whole file" or "the whole repo." An `encrypt()`-shaped call that
|
|
338
|
+
exists elsewhere in the same file or function but is not on this specific
|
|
339
|
+
flow's own path to this specific store sink is structurally invisible to
|
|
340
|
+
`classifyHandling`, and therefore invisible to this check too — the same
|
|
341
|
+
precision property §4's own flow.handling already relies on, now reused for
|
|
342
|
+
a security VERDICT rather than a taxonomy label.
|
|
343
|
+
|
|
344
|
+
**What this does NOT do**, all deferred to future, separately-scoped
|
|
345
|
+
increments (C2/C3, per the sub-project's own scoping doc): storage/IaC
|
|
346
|
+
encryption configuration detection (an S3/RDS/Terraform-level
|
|
347
|
+
`ServerSideEncryptionConfiguration`/`StorageEncrypted`/`kms_key_id` signal);
|
|
348
|
+
database column or transparent-encryption configuration; widening the
|
|
349
|
+
`store`-kind filter to `queue`/`vector-store`/`model`/`training`; AC-12's
|
|
350
|
+
at-rest half of the aggregate `protectionSummary` verdict (needs a real
|
|
351
|
+
aggregation rule, the same boundary §5's own scope-boundary note already
|
|
352
|
+
drew for the transit/handling case). The ABSENCE case (no recognized
|
|
353
|
+
encryption, or a non-`store` sink) was already correct with zero new code —
|
|
354
|
+
`emptyProtection()`'s own default — before this increment; this increment's
|
|
355
|
+
own job was only to give the PRESENCE case something real to write.
|