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

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 (162) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +702 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +307 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +770 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/package.json +12 -5
  72. package/src/dataflow/CLAUDE.md +1 -1
  73. package/src/dataflow/catalog.js +42 -0
  74. package/src/dataflow/orm-write-catalog.js +175 -0
  75. package/src/engine.js +92 -1
  76. package/src/ir/CLAUDE.md +1 -0
  77. package/src/ir/chrome-probe.mjs +150 -0
  78. package/src/ir/parser-js.js +94 -7
  79. package/src/lineage/CLAUDE.md +1203 -0
  80. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  81. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  82. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  83. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  84. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  85. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  86. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  87. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  88. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  89. package/src/lineage/classification.js +56 -0
  90. package/src/lineage/coverage.js +658 -0
  91. package/src/lineage/cross-repo-link.js +107 -0
  92. package/src/lineage/dataflow-graph.schema.json +184 -0
  93. package/src/lineage/decision-story.js +206 -0
  94. package/src/lineage/drift-policy.js +279 -0
  95. package/src/lineage/driver.js +135 -0
  96. package/src/lineage/engine.js +992 -0
  97. package/src/lineage/export-briefing.js +628 -0
  98. package/src/lineage/export-csv.js +62 -0
  99. package/src/lineage/export-json.js +238 -0
  100. package/src/lineage/export-privacy.js +258 -0
  101. package/src/lineage/federation-loader.js +111 -0
  102. package/src/lineage/field-identity.js +78 -0
  103. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  104. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  105. package/src/lineage/flow-grade.js +221 -0
  106. package/src/lineage/governance-edit.js +169 -0
  107. package/src/lineage/graph-builder.js +1114 -0
  108. package/src/lineage/graph-diff.js +431 -0
  109. package/src/lineage/graph-snapshot.js +180 -0
  110. package/src/lineage/handling-analyzer.js +168 -0
  111. package/src/lineage/ids.js +349 -0
  112. package/src/lineage/impact-assessment.js +76 -0
  113. package/src/lineage/impact-engine.js +268 -0
  114. package/src/lineage/index.js +281 -0
  115. package/src/lineage/language-coverage-tiers.js +58 -0
  116. package/src/lineage/obligation-mapping.js +126 -0
  117. package/src/lineage/obligation-predicates.js +235 -0
  118. package/src/lineage/observation-adapters.js +282 -0
  119. package/src/lineage/observation-correlation.js +622 -0
  120. package/src/lineage/observation-store.js +497 -0
  121. package/src/lineage/path-query.js +410 -0
  122. package/src/lineage/path-store.js +400 -0
  123. package/src/lineage/protection.js +53 -0
  124. package/src/lineage/recipient-profile.js +192 -0
  125. package/src/lineage/recipient-registry.js +394 -0
  126. package/src/lineage/redact-graph.js +224 -0
  127. package/src/lineage/remediation.js +417 -0
  128. package/src/lineage/resolve-destination.js +91 -0
  129. package/src/lineage/runtime-observation.js +464 -0
  130. package/src/lineage/scenario-diff.js +84 -0
  131. package/src/lineage/scenario-engine.js +251 -0
  132. package/src/lineage/scenario.js +101 -0
  133. package/src/lineage/schema.js +167 -0
  134. package/src/lineage/sink-registry.js +427 -0
  135. package/src/lineage/source-registry.js +357 -0
  136. package/src/lineage/source-seeding.js +212 -0
  137. package/src/lineage/summaries.js +590 -0
  138. package/src/lineage/transform-catalog.js +397 -0
  139. package/src/lineage/transit-protection.js +150 -0
  140. package/src/lineage/validate.js +285 -0
  141. package/src/mcp/CLAUDE.md +7 -1
  142. package/src/mcp/dataflow-tools.js +160 -0
  143. package/src/mcp/server.js +1 -1
  144. package/src/mcp/tools.js +2 -1
  145. package/src/pipeline/scan-health.js +19 -1
  146. package/src/posture/CLAUDE.md +19 -0
  147. package/src/posture/artifact-registry.js +52 -0
  148. package/src/posture/auditor-walkthrough.js +76 -0
  149. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  150. package/src/posture/obligation-evidence-pack.js +202 -0
  151. package/src/posture/remediation-ledger.js +337 -0
  152. package/src/server/CLAUDE.md +47 -0
  153. package/src/server/graph-loader.js +141 -0
  154. package/src/server/http-server.js +325 -0
  155. package/src/server/routes.js +129 -0
  156. package/src/server/security.js +111 -0
  157. package/src/server/static-assets.js +139 -0
  158. package/dist/11.index.js +0 -353
  159. package/dist/259.index.js +0 -975
  160. package/dist/317.index.js +0 -300
  161. package/dist/609.index.js +0 -741
  162. package/dist/838.index.js +0 -152
@@ -0,0 +1,143 @@
1
+ # DESIGN_STORE_DETAIL.md — Sub-project E's binding design record (increment 2)
2
+
3
+ **Status:** landed as Milestone 2, Sub-project E, increment **2** — a
4
+ small, focused slice, per
5
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-e2-plan.md`.
6
+ Binding on later Sub-project E increments the same way `DESIGN_
7
+ DESTINATION_RESOLVER.md` binds Sub-project A — but this record covers its
8
+ own, unrelated field: `node.storeDetail` is specific to database-category
9
+ ORM-write sink nodes, never to be conflated with `node.destination`
10
+ (Sub-project A's own field, for external-api-category nodes).
11
+
12
+ ---
13
+
14
+ ## 1. What this increment actually is
15
+
16
+ Increment 1 (`8e733fe9`) shipped ORM-write SITE RECOGNITION only: a call
17
+ site like `User.create({ email: x })` is recognized as an ORM-write sink
18
+ candidate, but nothing about the call is extracted or retained beyond that
19
+ boolean fact — no table name, no operation, no column list. This increment
20
+ extracts FR-204's structured facts from a site increment 1 already
21
+ recognizes, and attaches them to the sink node as a new field,
22
+ `node.storeDetail`.
23
+
24
+ **PRD text (FR-204, verbatim):** *"When supported evidence exists, a
25
+ database sink must include provider, host/config reference, database,
26
+ schema, table/collection, operation, column/field mapping. Unknown portions
27
+ remain `unknown`; the entire store must not be omitted."*
28
+
29
+ No new detection. Every fact extracted here was already in scope at the
30
+ recognized call site — `resolveOrmWriteAtCallSite` already has, in scope,
31
+ the callee expression (whose `object.name` is the model identifier), the
32
+ matched catalog entry's own `callee` (`create`/`save`/`update`/`upsert`),
33
+ and the confirmed object-literal first argument. This increment reads three
34
+ of those facts it was already holding and never recording.
35
+
36
+ ---
37
+
38
+ ## 2. The `storeDetail` object shape
39
+
40
+ ```
41
+ {
42
+ provider: string | null, // deferred to a later increment — always null in E2
43
+ host: string | null, // deferred — always null in E2
44
+ database: string | null, // deferred — always null in E2 (DB/cluster name, not the table)
45
+ schema: string | null, // deferred — always null in E2 (SQL schema/namespace)
46
+ table: string | null, // the model/receiver identifier name (e.g. 'User'), or null if unavailable
47
+ operation: string | null, // one of schema.js's STORE_OPERATION_VALUES, or null if unavailable
48
+ columns: string[], // real, non-'*' property key names from the object-literal argument; [] if none
49
+ }
50
+ ```
51
+
52
+ `table`/`operation`/`columns` are the only fields this increment ever
53
+ populates, and only from JS/TS ORM-write sites recognized by increment 1's
54
+ own catalog. `provider`/`host`/`database`/`schema` stay `null`
55
+ unconditionally in every case this increment produces — this is FR-204's
56
+ own "unknown portions remain unknown" clause in action, not a gap silently
57
+ left to be filled later without saying so.
58
+
59
+ ## 3. `table` — the receiver identifier
60
+
61
+ `calleeExpr.object.name`, already confirmed a string by
62
+ `_ormReceiverIsCapitalizedIdent`'s own check inside `orm-write-catalog.js`
63
+ before a site is even recognized. Re-verified defensively at the extraction
64
+ site rather than assumed to have survived unchanged — a `typeof` guard, not
65
+ a re-implementation of that check.
66
+
67
+ ## 4. `operation` — the method-name mapping
68
+
69
+ The mapping lives in `graph-builder.js`, not `schema.js` — `schema.js`
70
+ holds enums, not mapping logic, per this package's own established
71
+ separation (the same split `sink-registry.js`'s `CWE_MAP` vs.
72
+ `schema.js`'s `SINK_CATEGORIES` already establishes).
73
+
74
+ | catalog `callee` | `operation` |
75
+ |---|---|
76
+ | `create` | `'create'` |
77
+ | `update` | `'update'` |
78
+ | `upsert` | `'upsert'` |
79
+ | `save` | `'upsert'` |
80
+
81
+ **`save` → `'upsert'`, not `'create'` — a deliberate, disclosed judgment
82
+ call.** Mongoose's `.save()` performs an INSERT on a new document but an
83
+ UPDATE on one loaded from the database, which is genuinely undecidable
84
+ statically from the call site alone; `'upsert'` is the honest umbrella
85
+ covering both, not a guess at which one it is. Any catalog `callee` outside
86
+ this table (there are none today — `ORM_WRITE_CATALOG` has exactly these
87
+ four entries) maps to `null`, never a fabricated guess.
88
+
89
+ ## 5. `columns` — the object-literal property keys
90
+
91
+ `arg0.props`, filtered and mapped:
92
+
93
+ ```js
94
+ [...new Set(
95
+ arg0.props
96
+ .filter((p) => !p.spread && typeof p.key === 'string' && p.key !== '*')
97
+ .map((p) => p.key),
98
+ )]
99
+ ```
100
+
101
+ Two exclusions, both load-bearing (per `parser-js.js`'s real
102
+ `ObjectExpression` lowering, confirmed by direct read, ~line 116-159):
103
+
104
+ - **A spread entry** (`{spread: true, value: <expr>}`, no `key` field at
105
+ all — `{...extra}`) has no key to report. Reporting one would be a
106
+ fabrication.
107
+ - **A `'*'`-keyed entry** (a non-literal computed key, `resolveObjectKey`'s
108
+ established convention — `{[dynamicKey]: x}`) is a genuinely UNKNOWN
109
+ column name, not a literal column named `"*"`. Reporting `'*'` as a real
110
+ column name would be the exact fabrication `field-identity.js`'s own
111
+ `object` case already refuses to commit one level up (see
112
+ `scanner/src/lineage/CLAUDE.md`'s "Round 5" note).
113
+
114
+ Deduplicated via `[...new Set(...)]` since two distinct-value properties
115
+ can share a key in real (if unusual) source — `{email: a, email: b}` — and
116
+ without dedup that would report `'email'` twice, a differently-shaped
117
+ version of the same over-counting bug increment 1's own precision work
118
+ elsewhere in this package has repeatedly guarded against.
119
+
120
+ ## 6. Wiring — mirrors `destination`'s own precedent exactly
121
+
122
+ `mintNode` gains an optional `storeDetail` param (default `null`).
123
+ `sinkNodeFor` passes `site.storeDetail ?? null` through — the exact wiring
124
+ point `destination` already established. `storeDetail` is set once, at node
125
+ MINT time, and is deliberately **NOT** part of the node identity
126
+ discriminator (`ids.nodeId`'s inputs are unchanged) — the same disclosed
127
+ coarsening `destination` already accepted: two sites colliding onto one
128
+ registry-decision node (same `kind`/`subtypeKey`/`coverageStatus`/
129
+ `externality`) still collide onto one node, and that node's `storeDetail`
130
+ is whichever site's resolution landed first, not a set/union of every
131
+ site's own facts.
132
+
133
+ ## 7. Explicitly deferred (named, not silently skipped)
134
+
135
+ - **`provider`/`host`/`database`/`schema` extraction** — needs config-chain
136
+ resolution (Sub-project A's own still-open scope) or a schema-correlation
137
+ mechanism neither this increment nor any prior one builds.
138
+ - **Table/column extraction for raw-SQL `database` sinks** — needs SQL
139
+ parsing, out of scope per the E-scoping doc's own Finding 2. This
140
+ increment is JS/TS ORM-write-site-only.
141
+ - **Python/Java/Go/Ruby/PHP ORM shapes** — increment 1's own JS/TS-only
142
+ scope boundary, unchanged.
143
+ - **Queue/topic mapping** — Sub-project E, increment 3.
@@ -0,0 +1,245 @@
1
+ # DESIGN_TRANSIT_PROTECTION.md — Sub-project B's binding design record (increments 1-2)
2
+
3
+ **Status:** increment **1** (plumbing skeleton) and increment **2** (real
4
+ `edge.protection.transit` verdicts, closing AC-03/AC-04) are both landed.
5
+ Increment 1 shipped per
6
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-b1-plan.md`
7
+ and the scoping correction at
8
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-b-scoping.md`.
9
+ Increment 2 shipped per
10
+ `docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-b2-plan.md`
11
+ and that same scoping doc's own "Correction (post-B1, before B2)" section —
12
+ which overrides §2 below's "separate post-pass, touches neither `coverage.js`
13
+ nor `graph-builder.js`" framing for the VERDICT-COMPUTATION job specifically
14
+ (§2 remains correct and unchanged for increment 1's own plumbing job — see
15
+ §6 for exactly what changed and why). Binding on later Sub-project B
16
+ increments (B3, if anything remains once B2 lands) the same way `DESIGN_
17
+ DESTINATION_RESOLVER.md` binds Sub-project A.
18
+
19
+ ---
20
+
21
+ ## 1. What this increment actually is
22
+
23
+ FR-401 needs a real, computed `edge.protection.transit` verdict for
24
+ network-outbound edges. Before any verdict logic can run, two things had to
25
+ exist that didn't: (1) a way to run `crypto-protocol.js`'s already-proven
26
+ TLS/cipher pattern recognition over the lineage engine's own input, and (2)
27
+ a way for that raw file text to reach the lineage build pipeline at all —
28
+ neither `coverage.js` nor `graph-builder.js` accepts, stores, or threads a
29
+ file's raw source string anywhere in their current signatures (confirmed by
30
+ direct read; see the scoping doc's Finding 2). This increment builds ONLY
31
+ those two things — the plumbing — and proves it end to end. No verdict
32
+ logic. `edge.protection.transit` is not written to anywhere in this
33
+ increment; every edge still gets `emptyProtection()`'s
34
+ `{verdict: 'not_assessed', evidenceGrade: 'none'}` exactly as before.
35
+
36
+ ## 2. The isolation decision (Finding 2's Option 2, confirmed buildable and taken)
37
+
38
+ A **separate post-pass module**, `transit-protection.js`, consuming
39
+ `fileContents` directly — never a change to `coverage.js`/`graph-builder.js`
40
+ (the six-increment-old, real, tested pipeline this whole session has been
41
+ careful never to destabilize). `scanTransitEvidence(fileContents)` runs
42
+ `scanCryptoProtocol` once per file and returns a `Map<file, findings[]>` —
43
+ genuinely reusable here (unlike, say, an ORM-write catalog reuse case)
44
+ because the signal needed is coarser: "was TLS verification disabled
45
+ ANYWHERE in this file" is a defensible question for a whole-file scanner to
46
+ answer, the same reuse shape `posture/network-policy-import.js` already
47
+ established for correlating a whole-file finding to a specific location by
48
+ `(file, line)` proximity — not by re-deriving structural call-site identity.
49
+
50
+ Option 1 (threading raw text into `coverage.js`/`graph-builder.js` directly)
51
+ was rejected for the same reason `index.js` itself already wraps
52
+ `coverage.js` for scan-facing concerns one layer further out: it would
53
+ couple a whole-file regex scanner's raw-text dependency into a pipeline that
54
+ today only ever sees already-parsed IR, for no benefit this increment needs.
55
+
56
+ ## 3. The `fileContents` plumbing path
57
+
58
+ `runFullScan`'s own signature already carries `fileContents` (the real
59
+ `{path: rawSourceString}` map, used pervasively elsewhere in that function
60
+ for `dropGuardedFindings`/`_isInlineSuppressed`/etc.) as a live local in the
61
+ exact scope that calls `buildLineageGraph(callGraph, {...})`. This
62
+ increment adds one field to that call site's opts object —
63
+ `fileContents` — and one corresponding optional parameter,
64
+ `opts.fileContents`, to `buildLineageGraph` itself, mirroring the existing
65
+ `opts.perFile`/`opts.parseFailures` passthrough pattern. `buildLineageGraph`
66
+ calls `scanTransitEvidence(opts.fileContents ?? {})` and attaches the
67
+ result to its own returned status object as a new field, `transitEvidence`
68
+ — a `Map`, alongside the existing `{status, graph, failure, elapsedMs}`
69
+ shape. It is a `Map`, not `Object.fromEntries(...)`, because every consumer
70
+ proven so far (this increment's own test, and B2's future join logic) reads
71
+ it in-process, never serializes it — `bin/agentic-security.js`'s
72
+ JSON-persistence path (`.agentic-security/lineage-graph.json`) only ever
73
+ persists `scan.lineageGraph` (the `graph` field), never the status object
74
+ this field lives on, so no serialization boundary exists yet to force a
75
+ plain-object shape. A future increment that needs to serialize
76
+ `transitEvidence` can convert at that boundary then, not here.
77
+
78
+ **Load-bearing invariant, proven by this increment's own test**: `graph`
79
+ itself is byte-identical whether or not `opts.fileContents` is supplied —
80
+ the same "byte-identical when a hook is omitted" proof every additive hook
81
+ this session has shipped (`opts.resolveSiteDecision`, `opts.resolveDestination`)
82
+ already carries. `scanTransitEvidence`'s own result is attached
83
+ ALONGSIDE `graph`, never folded into it.
84
+
85
+ ## 4. The candidate "network" category list (named, not yet used)
86
+
87
+ `sink-registry.js`'s `CATEGORY_NODE_KIND` maps a `SINK_CATEGORIES` value to
88
+ a node kind of `'external'` for the following categories: `external-api`,
89
+ `webhook`, `email`, `sms`, `push-notification`, `collaboration`,
90
+ `analytics`, `monitoring`, and every `ai-*` category except
91
+ `ai-local-model` (which maps to `'process'`, correctly — a local model is
92
+ not a network call). Of these, `external-api` is the unambiguous case: an
93
+ HTTP/RPC call to an external service. The others are all named here as
94
+ candidates a future increment must decide on, not decided now:
95
+
96
+ - `webhook`, `email`, `sms`, `push-notification` — all genuinely
97
+ network-outbound in every real implementation (an email/SMS/push send is
98
+ itself an API call to a provider); plausibly all "network,
99
+ transit-relevant" the same way `external-api` is.
100
+ - `analytics`, `monitoring`, `collaboration` — also network-outbound in
101
+ practice (a SaaS analytics/monitoring/chat-integration SDK call), but
102
+ named separately here because their `CATEGORY_EXTERNALITY` entry
103
+ (`'external'`) and node kind (`'external'`) are the same signal
104
+ `external-api` carries, so nothing in the registry currently
105
+ distinguishes "this external call matters for transit-protection" from
106
+ "this external call happens to be classified `analytics`" — a future
107
+ increment may find a reason to exclude some of these (e.g. a
108
+ fire-and-forget beacon with no response path) that this increment has no
109
+ basis to decide.
110
+ - Every `ai-*` category except `ai-local-model` — an AI SDK call
111
+ (`ai-model-provider`, `ai-agent`, `ai-tool`, `ai-vector-store`,
112
+ `ai-memory`, `ai-training`, `ai-evaluation`, `ai-telemetry`) is a network
113
+ call to an external service by construction, per Sub-project A's own
114
+ literal-URL AI-provider resolution work — a strong candidate for
115
+ inclusion.
116
+
117
+ **Not** candidates: everything `CATEGORY_NODE_KIND` maps to `'log'`,
118
+ `'sink'`, `'store'`, `'queue'`, or `'process'` — none of those describe a
119
+ genuine outbound network call in the transit-protection sense (a `store`
120
+ category like `database`/`file`/`object-storage` may itself cross a
121
+ network in a real deployment, e.g. a managed database over TLS, but that is
122
+ Sub-project C's at-rest/connection concern, not this one, per the scoping
123
+ doc's own AC-06 exclusion).
124
+
125
+ This list is deliberately a naming exercise only in increment 1 — increment
126
+ 2 (§6 below) decides the filter for THIS increment (`external-api` only) and
127
+ names the rest of this list as still-deferred widening, not silently
128
+ resolved.
129
+
130
+ ## 5. Explicitly deferred as of increment 1 (superseded in part by §6)
131
+
132
+ - ~~The file+line correlation join~~ — **closed by increment 2**, §6 below.
133
+ - ~~Any write to `edge.protection.transit`~~ — **closed by increment 2**,
134
+ for `external-api` sites only; every other category's edges still get
135
+ `emptyProtection()`'s own `not_assessed`/`none` defaults, unchanged.
136
+ - **The final candidate "network category" list's actual USE in filtering
137
+ which edges matter** — increment 2 uses only `external-api` (the
138
+ unambiguous case named in §4); the rest of §4's list remains named,
139
+ deferred widening for a future increment (see §6.2).
140
+ - **AC-03/AC-04 fixtures** — **closed by increment 2**, §6 below.
141
+
142
+ ## 6. Increment 2: the corrected hook point, the verdict logic, and what's still deferred
143
+
144
+ ### 6.1 The correction this increment implements
145
+
146
+ §2 above (increment 1's own design) chose a separate post-pass — a real,
147
+ correct, buildable design for the PLUMBING job (getting raw file text into
148
+ the pipeline). Re-reading `graph-builder.js` directly while scoping the
149
+ verdict-computation job itself found that this does NOT extend to the
150
+ verdict job: `DESIGN_GRAPH_BUILDER.md` §6.1's own rule — "a node is a
151
+ REGISTRY DECISION, not a provenance node and not a call site" — means a
152
+ node's own `location` is unconditionally `null`, and many distinct call
153
+ sites (different files, different lines) can collide onto ONE network sink
154
+ node. The per-site `file`/`line` this correlation needs is available ONLY
155
+ on the `site` object `enumerateSinkSites` builds, and that object is
156
+ consumed and discarded INSIDE `graph-builder.js`'s own edge-construction
157
+ loop — never surfaced onto `graph.edges[]`.
158
+
159
+ **Corrected design, implemented exactly as specified:** a new
160
+ `opts.resolveTransitProtection(site) -> {verdict, evidenceGrade} |
161
+ undefined` hook on `buildDataFlowGraph` (`graph-builder.js`), applied at
162
+ the exact same block that already reads `site.destination` for
163
+ `protocol.destinationResolution` — the same point `opts.resolveDestination`
164
+ (Sub-project A) hooks — composing into
165
+ `protection: { ...emptyProtection(), transit: resolved ??
166
+ emptyProtection().transit }`. `coverage.js`'s `buildGraphWithCoverage`
167
+ gains a new `opts.transitEvidenceByFile` parameter and wires in a DEFAULT
168
+ `resolveTransitProtection` closing over it, composing with a
169
+ caller-supplied override exactly the way `resolveSiteDecision`/
170
+ `resolveDestination` already do (`opts.resolveTransitProtection ?? <default
171
+ built from transitEvidenceByFile>`).
172
+
173
+ ### 6.2 The single-computation requirement
174
+
175
+ `index.js`'s own `buildLineageGraph` is now the ONLY place
176
+ `scanTransitEvidence` is ever called. Its result — a `Map<file,
177
+ findings[]>` — feeds BOTH the existing, unchanged `transitEvidence` return
178
+ field AND the new `opts.transitEvidenceByFile` passed to
179
+ `buildGraphWithCoverage`, the SAME `Map` reference, computed once.
180
+ `coverage.js`'s default hook never calls `scanTransitEvidence`/
181
+ `scanCryptoProtocol` itself — it only reads the pre-computed Map handed to
182
+ it. This is load-bearing, not a nice-to-have: before this change,
183
+ `index.js`'s own `buildGraphWithCoverage(callGraph, {...})` call passed no
184
+ `fileContents` at all, so `scanTransitEvidence` ran exactly once (for the
185
+ `transitEvidence` field alone); a second, independent call inside
186
+ `coverage.js`'s own default hook (re-deriving the Map from raw
187
+ `fileContents` a second time) would have silently doubled the per-file
188
+ regex-scan cost of every scan that supplies `fileContents`. A live test
189
+ (`transit-protection.test.js`, the call-count instrumentation case) proves
190
+ `scanCryptoProtocol` is invoked exactly once per file per
191
+ `buildLineageGraph` call, not merely architected that way.
192
+
193
+ ### 6.3 The verdict logic (`resolveTransitProtectionForSite`)
194
+
195
+ Lives in `transit-protection.js`, exported alongside `scanTransitEvidence`.
196
+ `TRANSIT_PROTECTION_WINDOW_LINES = 10` — a named, disclosed constant, a
197
+ real tuning choice and NOT a calibrated one (no real fixture corpus exists
198
+ yet to tune against): a TLS-config object (`{ rejectUnauthorized: false }`)
199
+ is often on the same line as, or a few lines before, the network call it
200
+ configures. This is a NEW, independently-chosen value for THIS
201
+ correlation, following this codebase's own established line-window
202
+ correlation precedent (`engine.js`'s `dropGuardedFindings` and several
203
+ other detectors use their own independently-chosen windows — measured
204
+ directly at `-2/+3`, `-2/+4`, `+10` lines depending on the detector) — not
205
+ copied from any one existing example.
206
+
207
+ `category !== 'external-api'` is a deliberate, NARROW first slice.
208
+ `webhook`/`email`/`sms`/`push-notification`/`analytics`/`monitoring`/
209
+ `collaboration`/`ai-*` (§4's own candidate list) are all real, plausible
210
+ "also network" categories — widening the filter to include them is
211
+ separate, deliberate scope for a later increment, named here, not silently
212
+ included or silently excluded.
213
+
214
+ Decision table, in order:
215
+
216
+ | # | Condition | Result |
217
+ |---|---|---|
218
+ | 1 | `site.decision.category !== 'external-api'` | `undefined` (not assessed) |
219
+ | 2 | Literal destination starts with `http://` | `{verdict: 'unprotected', evidenceGrade: 'code'}` — the scheme alone is sufficient evidence |
220
+ | 3 | A `crypto-tls-no-verify`/`crypto-tls-version` finding in the site's file within `TRANSIT_PROTECTION_WINDOW_LINES` lines of the site's line | `{verdict: 'unprotected', evidenceGrade: 'code'}` — overrides a literal `https://` scheme; a plain scheme is never sufficient once a nearby finding says verification was disabled (AC-04's own core property: the UI must not award protection based on the scheme alone) |
221
+ | 4 | Literal destination starts with `https://`, no nearby finding | `{verdict: 'protected', evidenceGrade: 'code'}` |
222
+ | 5 | Anything else (dynamic/unresolved destination, or no scheme opinion) | `undefined` — the honest answer; `emptyProtection()`'s own default (`not_assessed`/`none`) already means exactly that, so this function declines to overwrite it rather than manufacturing a fabricated verdict |
223
+
224
+ Returning `undefined` (never a fabricated `unknown`/`none` string) is the
225
+ same discipline `resolveDestination`/`resolveSiteDecision` already
226
+ established: a function that didn't really analyze something must never
227
+ manufacture a verdict that implies it did.
228
+
229
+ ### 6.4 Still explicitly deferred after increment 2
230
+
231
+ - Widening the network-category filter beyond `external-api` (§4's own
232
+ named list) — a later increment's job.
233
+ - AC-05's own dynamic-destination clause beyond "stays `not_assessed`" (the
234
+ PRD's AC-05 wording is about the Unresolved-outbound-destination NODE
235
+ existing, already shipped by Sub-project A/E4 — this increment's own
236
+ transit-verdict contribution to that scenario is exactly "stays
237
+ `not_assessed`," proven by a dedicated test, not more).
238
+ - AC-06 (database encryption / at-rest — Sub-project C, an entirely
239
+ different protection dimension).
240
+ - AC-12's aggregate "mixed" verdict (needs an aggregation rule no increment
241
+ has built yet — this increment populates one INPUT to that future
242
+ aggregation, not the aggregation itself).
243
+ - `atRest`/`handling` protection dimensions — untouched by this increment.
244
+ - Any language beyond JS/TS.
245
+ - `runtime` evidence grade (Milestone 5).
@@ -0,0 +1,56 @@
1
+ //
2
+ // Data classification model (Data Flow Explorer PRD section 9). Reuses
3
+ // scanner/src/dataflow/privacy-taxonomy.js for the PII/PHI/PCI/FIN/
4
+ // CREDENTIALS/GEOLOCATION/DEVICE_ID classes and their versioned,
5
+ // operator-extensible pattern config — PRD section 3 names this module
6
+ // explicitly as reusable, not something to re-implement. This file adds
7
+ // only what privacy-taxonomy.js does not already have: the CONFIDENTIAL
8
+ // class (PRD 9.1's 8th built-in class — proprietary/business-confidential
9
+ // data has no reliable field-NAME pattern, unlike PII/PHI/PCI, so it
10
+ // ships with zero default patterns and is populated entirely through the
11
+ // same operator-config extension mechanism privacy-taxonomy.js already
12
+ // supports) and the AI processing context enum (PRD 9.2), which is
13
+ // DELIBERATELY ORTHOGONAL to data class — see the PRD's explicit warning
14
+ // against modeling AI as a mutually-exclusive label.
15
+
16
+ import { DEFAULT_TAXONOMY, classifyFieldAgainst, compileTaxonomy } from '../dataflow/privacy-taxonomy.js';
17
+
18
+ // PRD section 9.2 — all 15 supported AI processing contexts. "AI" as a
19
+ // filter means "matches ANY of these", never a single flag.
20
+ export const AI_PROCESSING_CONTEXTS = Object.freeze([
21
+ 'ai.system_prompt', 'ai.user_prompt', 'ai.model_input', 'ai.model_output',
22
+ 'ai.rag_context', 'ai.embedding', 'ai.vector_store', 'ai.memory',
23
+ 'ai.tool_argument', 'ai.tool_result', 'ai.training_data',
24
+ 'ai.fine_tuning_data', 'ai.evaluation_data', 'ai.telemetry', 'ai.model_artifact',
25
+ ]);
26
+
27
+ // CONFIDENTIAL ships with no default patterns on purpose — "confidential
28
+ // business data" has no reliable field-name regex the way "ssn" or
29
+ // "diagnosis" does. An operator adds patterns via the SAME
30
+ // .agentic-security/privacy-taxonomy.json extension mechanism
31
+ // privacy-taxonomy.js already documents (a class name not already in
32
+ // DEFAULT_TAXONOMY is accepted as a brand-new organization-defined class).
33
+ const _CONFIDENTIAL_EXTRA = Object.freeze({ severity: 'medium', patterns: [] });
34
+
35
+ export const LINEAGE_DATA_CLASSES = Object.freeze([...Object.keys(DEFAULT_TAXONOMY), 'CONFIDENTIAL']);
36
+
37
+ const _COMPILED_WITH_CONFIDENTIAL = compileTaxonomy({ ...DEFAULT_TAXONOMY, CONFIDENTIAL: _CONFIDENTIAL_EXTRA });
38
+
39
+ export function isAiContext(value) {
40
+ return AI_PROCESSING_CONTEXTS.includes(value);
41
+ }
42
+
43
+ /**
44
+ * Classify a data element's canonical/declared name against the
45
+ * (privacy-taxonomy-plus-CONFIDENTIAL) class list. Returns
46
+ * `{classes: string[], aiContexts: []}` — aiContexts is ALWAYS empty from
47
+ * this function: a name alone can never prove a field reaches an AI
48
+ * processing context (PRD 10.5/FR-205 — that requires actual lineage
49
+ * evidence connecting the field to a model input/prompt/embedding/etc.,
50
+ * which is Milestone 1 scope). Callers must not skip that proof step by
51
+ * reading a non-empty aiContexts here; it is shaped this way specifically
52
+ * so there is nothing to accidentally read.
53
+ */
54
+ export function classifyDataElementName(name, compiled = _COMPILED_WITH_CONFIDENTIAL) {
55
+ return { classes: classifyFieldAgainst(name, compiled), aiContexts: [] };
56
+ }