@clear-capabilities/agentic-security-scanner 0.144.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 (209) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/bin/agentic-security.js +3813 -83
  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} +13 -4
  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/2271.index.js +165 -0
  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} +14 -6
  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} +14 -6
  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} +67 -3
  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} +29 -7
  42. package/dist/{449.index.js → 5830.index.js} +78 -14
  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} +26 -8
  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} +580 -187
  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 +23 -10
  72. package/src/compare.js +6 -1
  73. package/src/dataflow/CLAUDE.md +2 -2
  74. package/src/dataflow/catalog.js +42 -0
  75. package/src/dataflow/orm-write-catalog.js +175 -0
  76. package/src/engine.js +580 -30
  77. package/src/fix/apply-fix-service.js +1 -0
  78. package/src/history-scan.js +22 -5
  79. package/src/ir/CLAUDE.md +2 -1
  80. package/src/ir/chrome-probe.mjs +150 -0
  81. package/src/ir/parser-js.js +94 -7
  82. package/src/lineage/CLAUDE.md +1203 -0
  83. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  84. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  85. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  86. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  87. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  88. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  89. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  90. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  91. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  92. package/src/lineage/classification.js +56 -0
  93. package/src/lineage/coverage.js +658 -0
  94. package/src/lineage/cross-repo-link.js +107 -0
  95. package/src/lineage/dataflow-graph.schema.json +184 -0
  96. package/src/lineage/decision-story.js +206 -0
  97. package/src/lineage/drift-policy.js +279 -0
  98. package/src/lineage/driver.js +135 -0
  99. package/src/lineage/engine.js +992 -0
  100. package/src/lineage/export-briefing.js +628 -0
  101. package/src/lineage/export-csv.js +62 -0
  102. package/src/lineage/export-json.js +238 -0
  103. package/src/lineage/export-privacy.js +258 -0
  104. package/src/lineage/federation-loader.js +111 -0
  105. package/src/lineage/field-identity.js +78 -0
  106. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  107. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  108. package/src/lineage/flow-grade.js +221 -0
  109. package/src/lineage/governance-edit.js +169 -0
  110. package/src/lineage/graph-builder.js +1114 -0
  111. package/src/lineage/graph-diff.js +431 -0
  112. package/src/lineage/graph-snapshot.js +180 -0
  113. package/src/lineage/handling-analyzer.js +168 -0
  114. package/src/lineage/ids.js +349 -0
  115. package/src/lineage/impact-assessment.js +76 -0
  116. package/src/lineage/impact-engine.js +268 -0
  117. package/src/lineage/index.js +281 -0
  118. package/src/lineage/language-coverage-tiers.js +58 -0
  119. package/src/lineage/obligation-mapping.js +126 -0
  120. package/src/lineage/obligation-predicates.js +235 -0
  121. package/src/lineage/observation-adapters.js +282 -0
  122. package/src/lineage/observation-correlation.js +622 -0
  123. package/src/lineage/observation-store.js +497 -0
  124. package/src/lineage/path-query.js +410 -0
  125. package/src/lineage/path-store.js +400 -0
  126. package/src/lineage/protection.js +53 -0
  127. package/src/lineage/recipient-profile.js +192 -0
  128. package/src/lineage/recipient-registry.js +394 -0
  129. package/src/lineage/redact-graph.js +224 -0
  130. package/src/lineage/remediation.js +417 -0
  131. package/src/lineage/resolve-destination.js +91 -0
  132. package/src/lineage/runtime-observation.js +464 -0
  133. package/src/lineage/scenario-diff.js +84 -0
  134. package/src/lineage/scenario-engine.js +251 -0
  135. package/src/lineage/scenario.js +101 -0
  136. package/src/lineage/schema.js +167 -0
  137. package/src/lineage/sink-registry.js +427 -0
  138. package/src/lineage/source-registry.js +357 -0
  139. package/src/lineage/source-seeding.js +212 -0
  140. package/src/lineage/summaries.js +590 -0
  141. package/src/lineage/transform-catalog.js +397 -0
  142. package/src/lineage/transit-protection.js +150 -0
  143. package/src/lineage/validate.js +285 -0
  144. package/src/lsp/server.js +49 -2
  145. package/src/mcp/CLAUDE.md +7 -1
  146. package/src/mcp/dataflow-tools.js +160 -0
  147. package/src/mcp/server.js +1 -1
  148. package/src/mcp/tools.js +22 -1
  149. package/src/pipeline/assurance-mode.js +64 -1
  150. package/src/pipeline/finding-schema.js +8 -1
  151. package/src/pipeline/scan-health.js +19 -1
  152. package/src/posture/CLAUDE.md +140 -0
  153. package/src/posture/accuracy-scorecard.js +60 -0
  154. package/src/posture/artifact-registry.js +76 -0
  155. package/src/posture/auditor-walkthrough.js +192 -13
  156. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  157. package/src/posture/compliance-policy.js +12 -2
  158. package/src/posture/cross-repo-memory.js +7 -2
  159. package/src/posture/fix-history.js +25 -2
  160. package/src/posture/fix-verify.js +9 -1
  161. package/src/posture/fleet.js +0 -0
  162. package/src/posture/git-history.js +13 -5
  163. package/src/posture/material-change.js +21 -2
  164. package/src/posture/mttr.js +75 -12
  165. package/src/posture/obligation-evidence-pack.js +202 -0
  166. package/src/posture/pre-incident-archaeology.js +39 -7
  167. package/src/posture/privacy-framework.js +14 -0
  168. package/src/posture/provenance/ai-authorship.js +68 -0
  169. package/src/posture/provenance/branch-entry.js +80 -0
  170. package/src/posture/provenance/cache.js +143 -0
  171. package/src/posture/provenance/confidence.js +36 -0
  172. package/src/posture/provenance/coordinator.js +786 -0
  173. package/src/posture/provenance/dag-walk.js +249 -0
  174. package/src/posture/provenance/evidence-attribution.js +59 -0
  175. package/src/posture/provenance/git-evidence.js +310 -0
  176. package/src/posture/provenance/lifecycle.js +208 -0
  177. package/src/posture/provenance/missing-control-resolver.js +137 -0
  178. package/src/posture/provenance/origin-resolver.js +342 -0
  179. package/src/posture/provenance/predicate-replay.js +133 -0
  180. package/src/posture/provenance/providers/config.js +39 -0
  181. package/src/posture/provenance/providers/github.js +62 -0
  182. package/src/posture/provenance/providers/gitlab.js +58 -0
  183. package/src/posture/provenance/repo-lineage.js +74 -0
  184. package/src/posture/provenance/sca-origin.js +139 -0
  185. package/src/posture/provenance/schema.js +255 -0
  186. package/src/posture/provenance/transitive-sca.js +147 -0
  187. package/src/posture/provenance/validate.js +30 -0
  188. package/src/posture/provenance-evidence-bundle.js +144 -0
  189. package/src/posture/remediation-ledger.js +337 -0
  190. package/src/posture/sbom-diff.js +15 -2
  191. package/src/posture/secret-history.js +10 -2
  192. package/src/posture/state-dir.js +38 -14
  193. package/src/posture/vuln-archaeology.js +8 -2
  194. package/src/pr-delta.js +25 -4
  195. package/src/report/index.js +197 -3
  196. package/src/runScan.js +34 -5
  197. package/src/sast/rate-limit.js +33 -3
  198. package/src/server/CLAUDE.md +47 -0
  199. package/src/server/graph-loader.js +141 -0
  200. package/src/server/http-server.js +325 -0
  201. package/src/server/routes.js +129 -0
  202. package/src/server/security.js +111 -0
  203. package/src/server/static-assets.js +139 -0
  204. package/src/util/git-hardening.js +128 -0
  205. package/dist/11.index.js +0 -353
  206. package/dist/259.index.js +0 -975
  207. package/dist/317.index.js +0 -300
  208. package/dist/609.index.js +0 -741
  209. package/dist/838.index.js +0 -152
@@ -0,0 +1,357 @@
1
+ //
2
+ // source-registry.js — Data Flow Explorer, Sub-project D, increment D2.
3
+ //
4
+ // Reclassifies scanner/src/dataflow/catalog.js's `kind: 'source'` entries
5
+ // (180 as of this writing — re-derive via `CATALOG.filter(e =>
6
+ // e.kind === 'source').length` rather than trusting this comment; the
7
+ // dataflow/CLAUDE.md count has already drifted twice) into DataFlowGraph
8
+ // v1's `SOURCE_CATEGORIES` / `coverageStatus` vocabulary (`schema.js`).
9
+ //
10
+ // This module is the MECHANICAL implementation of a binding, independently
11
+ // reviewed design — it is not a fresh design decision. Every mapping row
12
+ // below traces to `DESIGN_REGISTRIES.md`:
13
+ // §4 — PROVENANCE_MAP, the primary mapping (keyed on the entry's
14
+ // declared `provenance` field).
15
+ // §4.1 — AGENT_TOOL_REFINEMENT: `agent-tool` provenance splits per-entry
16
+ // between model-produced tool arguments and results/resources
17
+ // flowing back to the model.
18
+ // §4.2 — the `language === 'cpp'` descriptor-generic-I/O refinement:
19
+ // `recv`/`recvfrom`/`read`/`fread`/`fgets` cannot say whether
20
+ // they read a file, socket, pipe or stdin, so their C entries
21
+ // demote to `partial`.
22
+ // §4.3 — NO_PROVENANCE_OVERRIDES: the 82 entries with no declared
23
+ // `provenance` at all, resolved by a per-entry-id override table.
24
+ // Per §9.1, THIS module is the table's permanent home.
25
+ // §6 — the `coverageStatus` decision procedure (modeled/partial/
26
+ // candidate/unsupported — never `manual`, §6.5).
27
+ // §7.1 — node kind is always `'source'` for a reclassified source entry
28
+ // (checked by this module's own tests, not assumed).
29
+ // §9.0 — the shared `{kind, category, coverageStatus, externality,
30
+ // reason}` decision shape.
31
+ //
32
+ // Pure reclassification layer: reads CATALOG entries as DATA and maps them
33
+ // onto the target vocabulary. It never re-derives what a call site
34
+ // matches — `matchSource()` in dataflow/catalog.js already owns that, and
35
+ // duplicating it here would fork a matcher the corpus proves against one
36
+ // that nothing proves (DESIGN_REGISTRIES.md §1).
37
+ //
38
+ // Isolation: imports ONLY `CATALOG` from ../dataflow/catalog.js. Never
39
+ // dataflow/engine.js, never dataflow/summaries.js, never matcher internals
40
+ // (this PRD's §18.1 isolation principle, same basis Sub-project A/B/C use
41
+ // for their own `dataflow/` reuse).
42
+ //
43
+ // The field this module emits is `category` — NOT `subtype`. On a
44
+ // DataFlowGraph v1 node, `category` becomes `node.subtype` (§9.0). A
45
+ // registry must never itself emit a field literally called `subtype`: the
46
+ // two are the same vocabulary at different layers, and conflating them
47
+ // would let a registry decision validate as a real graph node without ever
48
+ // passing through Sub-project E's future graph builder.
49
+
50
+ import { CATALOG } from '../dataflow/catalog.js';
51
+
52
+ // ─────────────────────────────────────────────────────────────────────────
53
+ // §4 — PROVENANCE_MAP: the primary source-side mapping, keyed on the
54
+ // entry's declared `provenance` field (12 rows).
55
+ // ─────────────────────────────────────────────────────────────────────────
56
+
57
+ export const PROVENANCE_MAP = Object.freeze({
58
+ 'http-body': Object.freeze({ category: 'http-body', status: 'modeled', why: 'exact vocabulary match' }),
59
+ 'url-param': Object.freeze({ category: 'http-query', status: 'modeled', why: 'pure rename; the query string' }),
60
+ 'path-param': Object.freeze({ category: 'http-route', status: 'modeled', why: 'pure rename; a route/path segment' }),
61
+ 'header': Object.freeze({ category: 'http-header', status: 'modeled', why: 'pure rename' }),
62
+ 'cookie': Object.freeze({ category: 'http-cookie', status: 'modeled', why: 'pure rename' }),
63
+ 'env': Object.freeze({ category: 'env-value', status: 'modeled', why: 'pure rename' }),
64
+ 'cli': Object.freeze({ category: 'cli-argument', status: 'modeled', why: 'pure rename' }),
65
+ // Both rows below carry a `language === 'cpp'` refinement (§4.2) — C's
66
+ // I/O primitives are descriptor-generic, so their C entries demote to
67
+ // `partial` (see CPP_DESCRIPTOR_GENERIC_PROVENANCE below).
68
+ 'network': Object.freeze({ category: 'external-api-response', status: 'modeled', why: 'reads an outbound response body (fetch/axios/requests/urlopen)' }),
69
+ 'file-read': Object.freeze({ category: 'storage-read', status: 'modeled', why: "FR-101 groups 'files and object storage reads'; storage-read is its only encoding" }),
70
+ 'url-fragment': Object.freeze({ category: 'http-query', status: 'partial', why: 'LOSSY: a fragment is URL-borne but never transmitted to the server; SOURCE_CATEGORIES has no http-fragment value' }),
71
+ 'stdin': Object.freeze({ category: 'user-input', status: 'partial', why: 'LOSSY: broadens to the generic category; no stdin/console value exists' }),
72
+ // `agent-tool` is the one provenance value that does NOT resolve on the
73
+ // provenance key alone — its 8 entries split directionally between
74
+ // model-produced tool ARGUMENTS and tool RESULTS/resources flowing back
75
+ // (§4.1). This row exists so the completeness guard still sees the key
76
+ // covered, and records WHY it needs refinement. `status: 'split'` is an
77
+ // INTERNAL marker only — reclassifySource() intercepts it before it can
78
+ // ever reach a caller as a coverageStatus.
79
+ 'agent-tool': Object.freeze({ category: null, status: 'split', why: 'REFINED PER ENTRY: arguments (model-produced) vs results/resources (returned to the model) are different SOURCE_CATEGORIES' }),
80
+ });
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────
83
+ // §4.1 — agent-tool splits directionally. 8 rows.
84
+ // ─────────────────────────────────────────────────────────────────────────
85
+
86
+ export const AGENT_TOOL_REFINEMENT = Object.freeze({
87
+ 'py-mcp-tool': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'the tool parameter is whatever the model chose to pass; ai-model-output normally denotes a completion, so this is an approximation' }),
88
+ 'py-mcp-server-tool': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
89
+ 'js-mcp-call-args': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
90
+ 'js-mcp-request-params': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
91
+ 'js-mcp-extra-args': Object.freeze({ category: 'ai-model-output', status: 'partial', why: 'as py-mcp-tool' }),
92
+ 'js-mcp-tool-result': Object.freeze({ category: 'ai-tool-result', status: 'modeled', why: 'literally a tool result' }),
93
+ 'py-mcp-tool-result': Object.freeze({ category: 'ai-tool-result', status: 'modeled', why: 'literally a tool result' }),
94
+ 'js-mcp-resource-contents': Object.freeze({ category: 'ai-retrieved-document', status: 'modeled', why: 'an MCP resource IS a retrieved document' }),
95
+ });
96
+
97
+ // ─────────────────────────────────────────────────────────────────────────
98
+ // §4.2 — C's I/O primitives are descriptor-generic. Within these two
99
+ // provenance buckets, a `cpp` entry cannot say whether it read a file, a
100
+ // socket, a pipe or stdin, so it is refined on top of PROVENANCE_MAP:
101
+ // category unchanged, status demoted to `partial`. This is a REFINEMENT of
102
+ // a declared value, not a fallback for a missing one (that's §4.3, a
103
+ // different mechanism entirely).
104
+ // ─────────────────────────────────────────────────────────────────────────
105
+
106
+ export const CPP_DESCRIPTOR_GENERIC_PROVENANCE = new Set(['network', 'file-read']);
107
+
108
+ const CPP_DESCRIPTOR_GENERIC_WHY = Object.freeze({
109
+ 'network': 'LOSSY: a raw socket receive is directionally ambiguous — on a client socket this is an API response, on a listening socket it is an inbound client request, which external-api-response mis-describes',
110
+ 'file-read': 'LOSSY: a C file descriptor / FILE* is equally a file, socket, pipe or stdin; the entry cannot say which',
111
+ });
112
+
113
+ // ─────────────────────────────────────────────────────────────────────────
114
+ // §4.3 — the 82 source entries carrying NO `provenance` field at all. The
115
+ // category comes from the entry's own descriptive metadata (id/label/
116
+ // framework), never from a field its author set for classification — hence
117
+ // `candidate` for every row, per §6.3's decision procedure. Per §9.1, THIS
118
+ // table is the permanent home for these mappings (the design-phase PoC's
119
+ // own copy is redundant now that this module ships). Keys must equal
120
+ // EXACTLY the set of source entries with no declared `provenance` — see
121
+ // this file's own test suite for the completeness guard that enforces
122
+ // that, in both directions, against the live catalog.
123
+ // ─────────────────────────────────────────────────────────────────────────
124
+
125
+ export const NO_PROVENANCE_OVERRIDES = Object.freeze({
126
+ // Python / Flask
127
+ 'py-flask-request-args': 'http-query',
128
+ 'py-flask-request-form': 'http-body',
129
+ 'py-flask-request-json': 'http-body',
130
+ 'py-flask-request-values': 'http-query', // merged query+form bag
131
+ 'py-flask-request-cookies': 'http-cookie',
132
+ 'py-flask-request-headers': 'http-header',
133
+ 'py-flask-request-data': 'http-body',
134
+ // Python / FastAPI (call form; the annotation form DOES carry provenance)
135
+ 'py-fastapi-request-query': 'http-query',
136
+ 'py-fastapi-request-body': 'http-body',
137
+ 'py-fastapi-form': 'http-body',
138
+ // Python / Django
139
+ 'py-django-request-GET': 'http-query',
140
+ 'py-django-request-POST': 'http-body',
141
+ 'py-django-request-FILES': 'http-upload',
142
+ 'py-django-request-META': 'http-header',
143
+ // Python / stdlib
144
+ 'py-os-getenv': 'env-value',
145
+ 'py-os-environ': 'env-value',
146
+ 'py-input': 'user-input',
147
+ // Java / Servlet
148
+ 'java-request-getParameter': 'http-query',
149
+ 'java-request-getHeader': 'http-header',
150
+ 'java-request-getCookies': 'http-cookie',
151
+ 'java-request-getInputStream': 'http-body',
152
+ 'java-request-getReader': 'http-body',
153
+ // Java / stdlib
154
+ 'java-system-getenv': 'env-value',
155
+ 'java-system-getProperty': 'env-value', // JVM system properties, env-adjacent
156
+ // Java / Spring annotations
157
+ 'java-spring-requestparam': 'http-query',
158
+ 'java-spring-pathvariable': 'http-route',
159
+ 'java-spring-requestbody': 'http-body',
160
+ 'java-spring-requestheader': 'http-header',
161
+ // C# / ASP.NET Core annotations
162
+ 'cs-aspnet-fromquery': 'http-query',
163
+ 'cs-aspnet-frombody': 'http-body',
164
+ 'cs-aspnet-fromform': 'http-body',
165
+ 'cs-aspnet-fromroute': 'http-route',
166
+ 'cs-aspnet-fromheader': 'http-header',
167
+ 'cs-request-params': 'http-query', // merged bag
168
+ // Go / net/http
169
+ 'go-r-form': 'http-body', // r.Form merges URL query + POST form
170
+ 'go-r-postform': 'http-body',
171
+ 'go-r-body': 'http-body',
172
+ 'go-r-formvalue': 'http-body',
173
+ 'go-r-postformvalue': 'http-body',
174
+ 'go-r-uquery': 'http-query',
175
+ 'go-r-uquery-get': 'http-query',
176
+ // Go / gin
177
+ 'go-gin-query': 'http-query',
178
+ 'go-gin-bindjson': 'http-body',
179
+ 'go-gin-postform': 'http-body',
180
+ 'go-gin-shouldbind': 'http-body',
181
+ 'go-gin-shouldbindjson': 'http-body',
182
+ // Go / echo
183
+ 'go-echo-param': 'http-route',
184
+ 'go-echo-formvalue': 'http-body',
185
+ 'go-echo-queryparam': 'http-query',
186
+ 'go-echo-bind': 'http-body',
187
+ // Go / chi, fiber, buffalo, gorilla
188
+ 'go-chi-urlparam': 'http-route',
189
+ 'go-fiber-body': 'http-body',
190
+ 'go-fiber-query': 'http-query',
191
+ 'go-fiber-params': 'http-route',
192
+ 'go-fiber-formvalue': 'http-body',
193
+ 'go-fiber-cookies': 'http-cookie',
194
+ 'go-fiber-bodyparser': 'http-body',
195
+ 'go-buffalo-param': 'http-route',
196
+ 'go-buffalo-request': 'http-body',
197
+ 'go-gorilla-vars': 'http-route',
198
+ // Ruby
199
+ 'rb-rails-params': 'http-body', // merged query+body+route bag
200
+ 'rb-rails-cookies': 'http-cookie',
201
+ 'rb-rails-session': 'http-cookie', // Rails sessions are cookie-backed by default
202
+ 'rb-env': 'env-value',
203
+ 'rb-sinatra-request-body': 'http-body',
204
+ 'rb-sinatra-request-env': 'http-header', // the Rack env hash carries request headers
205
+ 'rb-sinatra-request-params': 'http-query', // merged bag
206
+ // PHP
207
+ 'php-request': 'http-body', // $_REQUEST merges GET+POST+COOKIE
208
+ 'php-get': 'http-query',
209
+ 'php-post': 'http-body',
210
+ 'php-cookie': 'http-cookie',
211
+ 'php-server': 'http-header',
212
+ 'php-symfony-query': 'http-query',
213
+ 'php-symfony-request': 'http-body',
214
+ 'php-symfony-cookies': 'http-cookie',
215
+ 'php-symfony-headers': 'http-header',
216
+ 'php-symfony-files': 'http-upload',
217
+ 'php-symfony-content': 'http-body',
218
+ 'php-symfony-get': 'http-query', // merged bag
219
+ // Kotlin
220
+ 'kt-request-param': 'http-query',
221
+ 'kt-request-header': 'http-header',
222
+ 'kt-ktor-parameters': 'http-query',
223
+ });
224
+
225
+ // ─────────────────────────────────────────────────────────────────────────
226
+ // Externality (§9.0's decision shape requires it on every returned
227
+ // decision). DESIGN_REGISTRIES.md §7.5's `CATEGORY_EXTERNALITY` table is
228
+ // SINK-focused only — D1's binding design does not specify a source-side
229
+ // equivalent, and the design-phase PoC's own `reclassifySource()` never
230
+ // computed one at all. This module must still emit the field (§9.0), so
231
+ // the value below is this increment's own resolution of that gap, derived
232
+ // from the one shipped precedent that exists: the flagship fixture's `web`
233
+ // node (`fixtures/build-flagship-fixture.mjs`) — `kind: 'source'`,
234
+ // `subtype: 'web-app'`, `externality: 'internal'`. That fixture models the
235
+ // checkout/registration form collection point as INTERNAL even though the
236
+ // person submitting the form is an anonymous public user — establishing
237
+ // that a source's externality reads the SAME counterparty rule §7.5 already
238
+ // states for sinks: *is the counterparty on the other side of this data
239
+ // exchange a party outside this program?* An end user hitting your own
240
+ // HTTP/GraphQL/gRPC/CLI endpoint, or your own process's env/argv/stdin, is
241
+ // your front door, not an outside party — so the analyzed system's own code
242
+ // is the counterparty, giving `internal`. (This is NOT "is the collection
243
+ // point itself part of the analyzed system" — every catalog source entry's
244
+ // collection point is trivially part of the analyzed system, or there
245
+ // would be no call site to catalog at all, so that framing collapses to
246
+ // "always true" and predicts nothing; it is specifically the identity of
247
+ // the COUNTERPARTY, symmetric with the sink-side rule, that this table
248
+ // encodes.) Nor is it "is the eventual human origin of the bytes untrusted"
249
+ // — every catalog source entry answers yes to that by construction, which
250
+ // is why it's a taint source at all, so that question cannot be what
251
+ // externality encodes either, or every source would trivially be
252
+ // `external` and the field would carry no information.
253
+ //
254
+ // This table mirrors CATEGORY_EXTERNALITY's own counterparty-rule reasoning,
255
+ // one class at a time:
256
+ // - ordinary in-app collection points (HTTP shapes, CLI, env, stdin /
257
+ // user-input) → `internal`, matching the flagship precedent directly:
258
+ // the counterparty is this program's own front door, not an outside
259
+ // party.
260
+ // - genuinely third-party-origin categories (an external API's response
261
+ // body, a webhook payload, anything AI-model/tool/resource-sourced) →
262
+ // `external`, matching CATEGORY_EXTERNALITY's identical treatment of
263
+ // the sink-side `external-api`/`webhook`/`ai-*` categories.
264
+ // - store-shaped categories (a file/object-storage read, a DB read, a
265
+ // queue message) → `unknown`, matching CATEGORY_EXTERNALITY's identical
266
+ // reasoning for `database`/`object-storage`/`cache`/`queue`: the entry
267
+ // gives no way to tell whether the store is local or third-party
268
+ // managed.
269
+ // - `declared` (the unreached operator-declaration path, §6.5) →
270
+ // `unknown`, matching the sink table's own choice for that category.
271
+ //
272
+ // Evidence-grade consequence (§7.5, carried over unchanged for the source
273
+ // side): the accompanying evidence grade for any of the above is at best
274
+ // `declared` — it comes from this table, not from executed code — never
275
+ // `code`. Real externality resolution is FR-202's job, landing in Milestone 2.
276
+ // ─────────────────────────────────────────────────────────────────────────
277
+
278
+ export const SOURCE_CATEGORY_EXTERNALITY = Object.freeze({
279
+ 'http-body': 'internal', 'http-query': 'internal', 'http-route': 'internal',
280
+ 'http-header': 'internal', 'http-cookie': 'internal', 'http-upload': 'internal',
281
+ 'graphql-argument': 'internal', 'grpc-field': 'internal',
282
+ 'cli-argument': 'internal', 'env-value': 'internal', 'user-input': 'internal',
283
+ 'queue-message': 'unknown', 'database-read': 'unknown', 'storage-read': 'unknown',
284
+ 'external-api-response': 'external', 'webhook-payload': 'external',
285
+ 'ai-model-output': 'external', 'ai-tool-result': 'external',
286
+ 'ai-retrieved-document': 'external', 'ai-memory': 'external',
287
+ 'declared': 'unknown',
288
+ });
289
+
290
+ /**
291
+ * Reclassify a single `kind: 'source'` catalog entry (from CATALOG) into
292
+ * DataFlowGraph v1's vocabulary. Total: every entry gets a decision, none
293
+ * throws, none is silently dropped (§6, §9's D2 item). Node kind is always
294
+ * `'source'` (§7.1). `coverageStatus` is never `'manual'` (§6.5) and never
295
+ * the internal `'split'` marker (intercepted before it can escape).
296
+ *
297
+ * @param {object} entry a CATALOG entry with `kind === 'source'`
298
+ * @returns {{kind: 'source', category: string|null, coverageStatus: string, externality: string, reason: string}}
299
+ */
300
+ export function reclassifySource(entry) {
301
+ if (entry.provenance) {
302
+ const row = PROVENANCE_MAP[entry.provenance];
303
+ if (!row) {
304
+ return {
305
+ kind: 'source', category: null, coverageStatus: 'unsupported',
306
+ externality: 'unknown', reason: `unmapped provenance ${entry.provenance}`,
307
+ };
308
+ }
309
+ if (entry.language === 'cpp' && CPP_DESCRIPTOR_GENERIC_PROVENANCE.has(entry.provenance)) {
310
+ const category = row.category;
311
+ return {
312
+ kind: 'source', category, coverageStatus: 'partial',
313
+ externality: SOURCE_CATEGORY_EXTERNALITY[category] ?? 'unknown',
314
+ reason: CPP_DESCRIPTOR_GENERIC_WHY[entry.provenance],
315
+ };
316
+ }
317
+ if (row.status === 'split') {
318
+ const ref = AGENT_TOOL_REFINEMENT[entry.id];
319
+ if (!ref) {
320
+ return {
321
+ kind: 'source', category: null, coverageStatus: 'unsupported',
322
+ externality: 'unknown', reason: `unrefined ${entry.provenance} entry ${entry.id}`,
323
+ };
324
+ }
325
+ return {
326
+ kind: 'source', category: ref.category, coverageStatus: ref.status,
327
+ externality: SOURCE_CATEGORY_EXTERNALITY[ref.category] ?? 'unknown',
328
+ reason: ref.why,
329
+ };
330
+ }
331
+ return {
332
+ kind: 'source', category: row.category, coverageStatus: row.status,
333
+ externality: SOURCE_CATEGORY_EXTERNALITY[row.category] ?? 'unknown',
334
+ reason: row.why,
335
+ };
336
+ }
337
+ const category = NO_PROVENANCE_OVERRIDES[entry.id];
338
+ if (!category) {
339
+ return {
340
+ kind: 'source', category: null, coverageStatus: 'unsupported',
341
+ externality: 'unknown', reason: `no provenance field and no override for ${entry.id}`,
342
+ };
343
+ }
344
+ // No declared classification field: inferred from the entry's own
345
+ // descriptive metadata. Plausible, reviewed, but not author-declared —
346
+ // hence `candidate`, never `modeled`/`partial` (§6.3).
347
+ return {
348
+ kind: 'source', category, coverageStatus: 'candidate',
349
+ externality: SOURCE_CATEGORY_EXTERNALITY[category] ?? 'unknown',
350
+ reason: 'inferred from entry id/label/framework; catalog.js declares no provenance for this entry',
351
+ };
352
+ }
353
+
354
+ // Re-exported for callers that want the raw source-entry slice without
355
+ // re-filtering CATALOG themselves (the completeness guards in this
356
+ // module's own test suite are the primary consumer).
357
+ export const SOURCE_ENTRIES = Object.freeze(CATALOG.filter((e) => e.kind === 'source'));
@@ -0,0 +1,212 @@
1
+ //
2
+ // source-seeding.js — Sub-project E, increment 2 (E2).
3
+ //
4
+ // Extracts the already-designed-and-proven source-seeding mechanism out of
5
+ // `DESIGN_GRAPH_BUILDER.md` §3 (binding ADR) and its throwaway
6
+ // proof-of-concept (`test/lineage/graph-builder-poc.test.js`'s
7
+ // `exprRoots`/`exprChildren`/`walkExpr`/`seedPathFor`/`planSeeds`/
8
+ // `seedEntryStateFactory`, `E1/1`-`E1/5`, `E1/14`) into a real, permanent,
9
+ // shipped module. This closes the measured "0 hops on real code" gap: the
10
+ // shipped `driver.js` (Sub-project E, increment 1) hardcodes `emptyState()`
11
+ // and produces exactly zero hops on real code with no seeding hook — see
12
+ // `DESIGN_GRAPH_BUILDER.md` §2.1. This module is that hook's first real
13
+ // consumer.
14
+ //
15
+ // This is mechanical porting of an already-reviewed design, not a redesign
16
+ // — see `DESIGN_GRAPH_BUILDER.md` §3 in full for the binding rules this
17
+ // file implements, and its own header for why the PoC (not this document)
18
+ // is authoritative if the two ever disagree.
19
+ //
20
+ // Reuse boundary (§12, confirmed against the source): imports ONLY
21
+ // `matchSource` from `../dataflow/catalog.js` and `accessPathOf` from
22
+ // `../dataflow/access-paths.js` — never `dataflow/engine.js`'s live taint
23
+ // state, never `dataflow/summaries.js`'s SummaryCache, never
24
+ // `dataflow/index.js`'s `runDeepAnalysis` (PRD §18.1). Everything else this
25
+ // module needs comes from already-shipped `src/lineage/` modules.
26
+
27
+ import { matchSource } from '../dataflow/catalog.js';
28
+ import { accessPathOf } from '../dataflow/access-paths.js';
29
+ import { emptyState, addIdentity } from './field-identity.js';
30
+ import { reclassifySource } from './source-registry.js';
31
+ import { classifyDataElementName } from './classification.js';
32
+ import { dataElementId } from './ids.js';
33
+
34
+ // ── §3.1: where the matched expressions come from ──────────────────────────
35
+ //
36
+ // Walk `fn.cfg.nodes` and, per node, the SAME expression roots `engine.js`'s
37
+ // own `step()` switch reads: `assign` → `source`, `call` → `callee` + `args`,
38
+ // `return` → `value`. Then recurse into every sub-expression. Deliberately
39
+ // NOT `fn.reads`/`fn.calls`: Sub-project D5 already measured that a call
40
+ // used as an assignment RHS never reaches `fn.calls[]` at all, so those
41
+ // side-channels are incomplete for this purpose. `fn.cfg.nodes` is a plain
42
+ // `Object` at runtime, not a `Map` (also D5's finding).
43
+
44
+ /**
45
+ * The expression roots a single CFG node carries, mirroring `engine.js`'s
46
+ * own `step()` switch exactly. Exported: this walking primitive is not
47
+ * seeding-specific — E3's projection (sink enumeration, transformation
48
+ * attribution) needs the identical CFG-node → expression-roots traversal,
49
+ * and a second, independently-drifting copy is exactly the duplication
50
+ * this package avoids everywhere else (see `DESIGN_REGISTRIES.md` §1).
51
+ */
52
+ export function exprRoots(node) {
53
+ const r = [];
54
+ if (node.kind === 'assign' && node.source) r.push(node.source);
55
+ if (node.kind === 'call') {
56
+ if (node.callee) r.push(node.callee);
57
+ for (const a of node.args ?? []) r.push(a);
58
+ }
59
+ if (node.kind === 'return' && node.value) r.push(node.value);
60
+ return r;
61
+ }
62
+
63
+ /** The sub-expressions of `e`, for recursive descent during the walk. */
64
+ function exprChildren(e) {
65
+ switch (e.kind) {
66
+ case 'member': return [e.object];
67
+ case 'call': return [e.callee, ...(e.args ?? [])];
68
+ case 'tpl': return e.parts ?? [];
69
+ case 'binary': case 'logical': return [e.left, e.right];
70
+ case 'union': return e.options ?? [];
71
+ case 'array': return e.elements ?? [];
72
+ case 'object': return (e.props ?? []).map((p) => p.value);
73
+ case 'assign-expr': return [e.source ?? e.value];
74
+ default: return [];
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Depth-first walk over an expression tree, visiting every node once.
80
+ * Exported for the same reuse reason as `exprRoots` above.
81
+ */
82
+ export function walkExpr(e, visit, parent = null) {
83
+ if (!e || typeof e !== 'object') return;
84
+ visit(e, parent);
85
+ for (const c of exprChildren(e)) walkExpr(c, visit, e);
86
+ }
87
+
88
+ // ── §3.2: the seed-path rule — seed the FIELD, not the container ───────────
89
+ //
90
+ // `matchSource` matches the CONTAINER (`req.body`), but the thing that has
91
+ // a field identity is the FIELD (`req.body.card_number`). Extend the
92
+ // matched expression outward through every enclosing pure-member access,
93
+ // then take `accessPathOf` of the outermost node. Falls back to the matched
94
+ // expression's own path when it is not the object of a member access
95
+ // (`User.create(req.body)`) — that container-level seed is the honest
96
+ // answer for that shape. §3.2's own text: "Classification is impossible
97
+ // without this rule." This must be ported EXACTLY as the PoC implements it
98
+ // — the single most consequential rule in the whole design.
99
+
100
+ /**
101
+ * Extend `expr` outward through every enclosing pure-member access (using
102
+ * `parentOf`, a map from expression node to its immediate parent expression
103
+ * within the same CFG-node walk), then return `accessPathOf` of the
104
+ * outermost such node — the FIELD path, not the container path.
105
+ */
106
+ function seedPathFor(expr, parentOf) {
107
+ let cur = expr;
108
+ for (;;) {
109
+ const p = parentOf.get(cur);
110
+ if (p && p.kind === 'member' && p.object === cur && typeof p.prop === 'string') { cur = p; continue; }
111
+ break;
112
+ }
113
+ return accessPathOf(cur);
114
+ }
115
+ // task review N-1: `p.prop === 'string'` is also true for the parser's own
116
+ // computed-key-unknown marker `'*'` (`req.body[k]`), so this rule can seed
117
+ // a path like `req.body.*` or `req.body.*.name`, minting a data element
118
+ // literally named `*`. This is NOT a correctness bug in seeding itself —
119
+ // `engine.js`'s `definitePrefixBeforeWildcard` already keeps every hop
120
+ // path at the DEFINITE prefix (`req.body`), so the provenance DAG is
121
+ // unaffected (verified: `function h(req,k,db){const v=req.body[k];
122
+ // db.query(v);}` still produces the correct hop/node/edge shape). It DOES
123
+ // mean a `*`-named seed can reach `planSeeds`' own output — E3's own
124
+ // projection module, which mints real `DataFlowGraph v1` dataElements
125
+ // from seed records, must not surface a literal `*` name to a user
126
+ // without translating it into an honest "dynamic key" disclosure.
127
+
128
+ // ── §3.3/§3.4: planSeeds ────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Plan the seeds for a whole project. Pure: runs no field-identity analysis
132
+ * itself, only walks IR expressions and consults the source registry.
133
+ *
134
+ * `callGraph` must be a real object from
135
+ * `scanner/src/ir/callgraph.js#buildCallGraph` (`{functions, ...}`) or an
136
+ * equivalent hand-built fixture exposing the same shape.
137
+ *
138
+ * Returns `{seeds, unseedable}`. Each seed record carries exactly the
139
+ * §3.3 shape:
140
+ * { file, qid, nodeId, line, entryId, seedPath, canonicalName,
141
+ * category, coverageStatus, externality, reason, // <- reclassifySource(entry)
142
+ * dataElementId, dataClasses } // <- ids.js + classification.js
143
+ *
144
+ * A matched expression with NO access path (`accessPathOf` → `null`) is
145
+ * recorded in `unseedable[]` — never silently dropped (§3.3).
146
+ */
147
+ export function planSeeds(callGraph, { repository } = {}) {
148
+ const seeds = [];
149
+ const unseedable = [];
150
+ for (const fn of callGraph.functions.values()) {
151
+ for (const [nid, node] of Object.entries(fn.cfg?.nodes ?? {})) {
152
+ const parentOf = new Map();
153
+ for (const root of exprRoots(node)) walkExpr(root, (e, p) => { if (p) parentOf.set(e, p); });
154
+ for (const root of exprRoots(node)) {
155
+ walkExpr(root, (e) => {
156
+ const entry = matchSource(e, fn.file);
157
+ if (!entry) return;
158
+ const decision = reclassifySource(entry);
159
+ const seedPath = seedPathFor(e, parentOf);
160
+ if (!seedPath) {
161
+ unseedable.push({
162
+ file: fn.file, qid: fn.qid, nodeId: nid, line: node.line ?? null,
163
+ entryId: entry.id, reason: 'accessPathOf returned null for the matched expression',
164
+ });
165
+ return;
166
+ }
167
+ const canonicalName = seedPath.slice(seedPath.lastIndexOf('.') + 1);
168
+ seeds.push({
169
+ file: fn.file, qid: fn.qid, nodeId: nid, line: node.line ?? null,
170
+ entryId: entry.id, seedPath, canonicalName,
171
+ category: decision.category, coverageStatus: decision.coverageStatus,
172
+ externality: decision.externality, reason: decision.reason,
173
+ // §3.4's minting rule. `canonicalName` alone is forbidden by
174
+ // PRD §10.4; the discriminator carries the system proxy
175
+ // (repository + file), the access path, and the category.
176
+ // Function scope (`qid`) is deliberately NOT in the
177
+ // discriminator (§3.4).
178
+ dataElementId: dataElementId(canonicalName, [repository, fn.file, seedPath, decision.category ?? '']),
179
+ dataClasses: classifyDataElementName(canonicalName).classes,
180
+ });
181
+ });
182
+ }
183
+ }
184
+ }
185
+ return { seeds, unseedable };
186
+ }
187
+
188
+ // ── §3.5: seedEntryStateFactory — the driver.js opts.seedEntryState hook ──
189
+
190
+ /**
191
+ * Builds the `opts.seedEntryState(fn) -> state | undefined` function
192
+ * `driver.js`'s `runFieldIdentityAnalysis` expects (Sub-project E,
193
+ * increment 1's additive hook). Groups `seeds` by function (`qid`) and
194
+ * returns, for a given function, a `field-identity.js` state carrying every
195
+ * data-element identity seeded at a matched call site inside that function
196
+ * — or `undefined` for a function with no seeds, so `driver.js`'s own
197
+ * `opts.seedEntryState(fn) || emptyState()` fallback applies unchanged.
198
+ */
199
+ export function seedEntryStateFactory(seeds) {
200
+ const byQid = new Map();
201
+ for (const s of seeds) {
202
+ if (!byQid.has(s.qid)) byQid.set(s.qid, []);
203
+ byQid.get(s.qid).push(s);
204
+ }
205
+ return (fn) => {
206
+ const list = byQid.get(fn.qid);
207
+ if (!list) return undefined;
208
+ let st = emptyState();
209
+ for (const s of list) st = addIdentity(st, s.seedPath, s.dataElementId);
210
+ return st;
211
+ };
212
+ }