@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.
Files changed (195) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +779 -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 +379 -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 +841 -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/dist/frontend/index.html +21 -0
  72. package/dist/frontend/src/app.js +176 -0
  73. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  74. package/dist/frontend/src/components/filter-rail.js +119 -0
  75. package/dist/frontend/src/components/query-bar.js +126 -0
  76. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  77. package/dist/frontend/src/export-entry.js +36 -0
  78. package/dist/frontend/src/lib/api-client.js +92 -0
  79. package/dist/frontend/src/lib/contrast.js +34 -0
  80. package/dist/frontend/src/lib/dom.js +24 -0
  81. package/dist/frontend/src/lib/escape-html.js +16 -0
  82. package/dist/frontend/src/lib/flow-path.js +40 -0
  83. package/dist/frontend/src/lib/focus-controls.js +149 -0
  84. package/dist/frontend/src/lib/protection-visual.js +46 -0
  85. package/dist/frontend/src/lib/query-language.js +240 -0
  86. package/dist/frontend/src/lib/row-filters.js +43 -0
  87. package/dist/frontend/src/lib/state.js +84 -0
  88. package/dist/frontend/src/main.js +83 -0
  89. package/dist/frontend/src/shell.js +184 -0
  90. package/dist/frontend/src/views/architecture-view.js +798 -0
  91. package/dist/frontend/src/views/inventory-view.js +292 -0
  92. package/dist/frontend/src/views/privacy-view.js +172 -0
  93. package/dist/frontend/src/views/trace-view.js +206 -0
  94. package/dist/frontend/styles/architecture-view.css +93 -0
  95. package/dist/frontend/styles/filter-rail.css +34 -0
  96. package/dist/frontend/styles/inspector.css +69 -0
  97. package/dist/frontend/styles/inventory-view.css +74 -0
  98. package/dist/frontend/styles/privacy-view.css +86 -0
  99. package/dist/frontend/styles/query-bar.css +107 -0
  100. package/dist/frontend/styles/shell.css +155 -0
  101. package/dist/frontend/styles/tokens.css +128 -0
  102. package/dist/frontend/styles/trace-view.css +95 -0
  103. package/package.json +13 -6
  104. package/src/dataflow/CLAUDE.md +1 -1
  105. package/src/dataflow/catalog.js +42 -0
  106. package/src/dataflow/orm-write-catalog.js +175 -0
  107. package/src/engine.js +92 -1
  108. package/src/ir/CLAUDE.md +1 -0
  109. package/src/ir/chrome-probe.mjs +150 -0
  110. package/src/ir/parser-js.js +94 -7
  111. package/src/lineage/CLAUDE.md +1203 -0
  112. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  113. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  114. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  115. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  116. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  117. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  118. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  119. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  120. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  121. package/src/lineage/classification.js +56 -0
  122. package/src/lineage/coverage.js +658 -0
  123. package/src/lineage/cross-repo-link.js +107 -0
  124. package/src/lineage/dataflow-graph.schema.json +184 -0
  125. package/src/lineage/decision-story.js +206 -0
  126. package/src/lineage/drift-policy.js +279 -0
  127. package/src/lineage/driver.js +135 -0
  128. package/src/lineage/engine.js +992 -0
  129. package/src/lineage/export-briefing.js +628 -0
  130. package/src/lineage/export-csv.js +62 -0
  131. package/src/lineage/export-json.js +238 -0
  132. package/src/lineage/export-privacy.js +258 -0
  133. package/src/lineage/federation-loader.js +111 -0
  134. package/src/lineage/field-identity.js +78 -0
  135. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  136. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  137. package/src/lineage/flow-grade.js +221 -0
  138. package/src/lineage/governance-edit.js +169 -0
  139. package/src/lineage/graph-builder.js +1114 -0
  140. package/src/lineage/graph-diff.js +431 -0
  141. package/src/lineage/graph-snapshot.js +180 -0
  142. package/src/lineage/handling-analyzer.js +168 -0
  143. package/src/lineage/ids.js +349 -0
  144. package/src/lineage/impact-assessment.js +76 -0
  145. package/src/lineage/impact-engine.js +268 -0
  146. package/src/lineage/index.js +281 -0
  147. package/src/lineage/language-coverage-tiers.js +58 -0
  148. package/src/lineage/obligation-mapping.js +126 -0
  149. package/src/lineage/obligation-predicates.js +235 -0
  150. package/src/lineage/observation-adapters.js +282 -0
  151. package/src/lineage/observation-correlation.js +622 -0
  152. package/src/lineage/observation-store.js +497 -0
  153. package/src/lineage/path-query.js +410 -0
  154. package/src/lineage/path-store.js +400 -0
  155. package/src/lineage/protection.js +53 -0
  156. package/src/lineage/recipient-profile.js +192 -0
  157. package/src/lineage/recipient-registry.js +394 -0
  158. package/src/lineage/redact-graph.js +224 -0
  159. package/src/lineage/remediation.js +417 -0
  160. package/src/lineage/resolve-destination.js +91 -0
  161. package/src/lineage/runtime-observation.js +464 -0
  162. package/src/lineage/scenario-diff.js +84 -0
  163. package/src/lineage/scenario-engine.js +251 -0
  164. package/src/lineage/scenario.js +101 -0
  165. package/src/lineage/schema.js +167 -0
  166. package/src/lineage/sink-registry.js +427 -0
  167. package/src/lineage/source-registry.js +357 -0
  168. package/src/lineage/source-seeding.js +212 -0
  169. package/src/lineage/summaries.js +590 -0
  170. package/src/lineage/transform-catalog.js +397 -0
  171. package/src/lineage/transit-protection.js +150 -0
  172. package/src/lineage/validate.js +285 -0
  173. package/src/mcp/CLAUDE.md +7 -1
  174. package/src/mcp/dataflow-tools.js +160 -0
  175. package/src/mcp/server.js +1 -1
  176. package/src/mcp/tools.js +2 -1
  177. package/src/pipeline/scan-health.js +19 -1
  178. package/src/posture/CLAUDE.md +19 -0
  179. package/src/posture/artifact-registry.js +52 -0
  180. package/src/posture/auditor-walkthrough.js +76 -0
  181. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  182. package/src/posture/obligation-evidence-pack.js +202 -0
  183. package/src/posture/remediation-ledger.js +337 -0
  184. package/src/server/CLAUDE.md +47 -0
  185. package/src/server/graph-loader.js +141 -0
  186. package/src/server/http-server.js +325 -0
  187. package/src/server/routes.js +129 -0
  188. package/src/server/security.js +111 -0
  189. package/src/server/static-assets.js +144 -0
  190. package/src/shared/frontend-root.js +52 -0
  191. package/dist/11.index.js +0 -353
  192. package/dist/259.index.js +0 -975
  193. package/dist/317.index.js +0 -300
  194. package/dist/609.index.js +0 -741
  195. package/dist/838.index.js +0 -152
@@ -1,8 +1,8 @@
1
- export const id = 435;
2
- export const ids = [435,752];
1
+ export const id = 9091;
2
+ export const ids = [9091,8218,8752];
3
3
  export const modules = {
4
4
 
5
- /***/ 2435:
5
+ /***/ 9091:
6
6
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
7
 
8
8
 
@@ -102,113 +102,182 @@ var integrity = __webpack_require__(1130);
102
102
  var state_dir = __webpack_require__(1174);
103
103
  // EXTERNAL MODULE: ./src/posture/cache-economics.js
104
104
  var cache_economics = __webpack_require__(8752);
105
- ;// CONCATENATED MODULE: ./src/mcp/redact.js
106
- // Secret redactor for MCP tool outputs and audit log argument summaries.
107
- //
108
- // OWASP MCP01 + MCP10: the scanner reads source code, and findings carry
109
- // `snippet` / `description` / `trace` strings that may contain hardcoded
110
- // credentials, API keys, JWTs, private keys, etc. When those flow back to
111
- // the agent through tools/call responses they land in the agent's context
112
- // exposing the secret to model logs, transcripts, and any downstream tool
113
- // the agent passes them to.
114
- //
115
- // We replace high-confidence secret shapes with [REDACTED:<kind>] before
116
- // emitting them. The original full content is still on disk (scanner
117
- // findings); the MCP surface is the bottleneck we control.
105
+ // EXTERNAL MODULE: ./src/mcp/redact.js
106
+ var redact = __webpack_require__(3468);
107
+ // EXTERNAL MODULE: ./src/report/index.js + 3 modules
108
+ var report = __webpack_require__(457);
109
+ // EXTERNAL MODULE: ./src/posture/provenance/schema.js
110
+ var schema = __webpack_require__(4594);
111
+ // EXTERNAL MODULE: ./src/server/graph-loader.js
112
+ var graph_loader = __webpack_require__(8218);
113
+ // EXTERNAL MODULE: ./src/server/routes.js
114
+ var routes = __webpack_require__(4268);
115
+ // EXTERNAL MODULE: ./src/lineage/redact-graph.js
116
+ var redact_graph = __webpack_require__(334);
117
+ // EXTERNAL MODULE: ./src/lineage/export-json.js
118
+ var export_json = __webpack_require__(859);
119
+ ;// CONCATENATED MODULE: ./src/mcp/dataflow-tools.js
120
+ // dataflow-tools.js — Milestone 4, sub-project MCP tools.
118
121
  //
119
- // Patterns deliberately stay narrow: high-precision so we don't garble
120
- // non-secret long strings (UUIDs, SHAs, base64-encoded scan IDs).
121
-
122
- const PATTERNS = [
123
- // Provider-specific high-entropy keys (anchored prefixes give very low FP)
124
- [/AKIA[0-9A-Z]{16}/g, 'aws-access-key'],
125
- [/ASIA[0-9A-Z]{16}/g, 'aws-temp-key'],
126
- [/gh[pousr]_[A-Za-z0-9]{36,255}/g, 'github-token'],
127
- [/xox[abprs]-[A-Za-z0-9-]{10,}/g, 'slack-token'],
128
- [/sk-ant-[A-Za-z0-9_-]{20,}/g, 'anthropic-key'],
129
- [/sk-proj-[A-Za-z0-9_-]{20,}/g, 'openai-project-key'],
130
- [/sk-[A-Za-z0-9]{32,}/g, 'openai-or-stripe-key'],
131
- [/sk_(?:live|test)_[A-Za-z0-9]{20,}/g, 'stripe-key'],
132
- [/rk_(?:live|test)_[A-Za-z0-9]{20,}/g, 'stripe-restricted-key'],
133
- [/SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, 'sendgrid-key'],
134
- [/AIza[0-9A-Za-z_-]{35}/g, 'google-api-key'],
135
- // Stage 4 correctness audit (coverage breadth, AI security): this list
136
- // only covered a small subset of what the scanner's OWN credential
137
- // detector (engine.js's CREDENTIAL_PATTERNS, 40+ provider shapes) finds
138
- // — a Shopify/Telegram/Twilio/Discord-webhook/Square/Google-OAuth/JDBC
139
- // secret detected and reported by a scan reached explain_finding's
140
- // output completely unredacted, because none of those shapes were in
141
- // THIS separate, narrower list. Reusing the same regex bodies as
142
- // engine.js's CREDENTIAL_PATTERNS for the shapes verified to leak
143
- // (rather than importing engine.js itself, which would pull its entire
144
- // multi-thousand-line module graph into the MCP server's dependency
145
- // surface for a handful of consts).
146
- [/ya29\.[0-9A-Za-z_-]{20,}/g, 'google-oauth-token'],
147
- [/shp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}/g, 'shopify-token'],
148
- [/(?<![0-9])[0-9]{8,10}:AA[0-9A-Za-z_-]{33}(?![A-Za-z0-9_])/g, 'telegram-bot-token'],
149
- [/twilio.{0,20}SK[0-9a-fA-F]{32}/gi, 'twilio-api-key'],
150
- [/sq0atp-[0-9A-Za-z_-]{22}/g, 'square-access-token'],
151
- [/sq0csp-[0-9A-Za-z_-]{43}/g, 'square-oauth-secret'],
152
- [/access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, 'paypal-braintree-token'],
153
- [/https:\/\/(?:discordapp|discord)\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, 'discord-webhook'],
154
- [/https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8}\/B[a-zA-Z0-9_]{8,12}\/[a-zA-Z0-9_]{24}/g, 'slack-webhook'],
155
- [/https:\/\/outlook\.office\.com\/webhook\/[A-Za-z0-9\-@]+\/IncomingWebhook\/[A-Za-z0-9-]+\/[A-Za-z0-9-]+/g, 'teams-webhook'],
156
- [/https:\/\/(?:www\.)?hooks\.zapier\.com\/hooks\/catch\/[A-Za-z0-9]+\/[A-Za-z0-9]+\//g, 'zapier-webhook'],
157
- // JDBC connection string carrying a password: only redact when password
158
- // evidence is actually on the line (matches engine.js's own ctx gate),
159
- // so a credential-free JDBC URL in docs isn't needlessly mangled.
160
- [/jdbc:[a-z:]+:\/\/[A-Za-z0-9.\-_:;=/@?,&]*(?:@|password=|passwd=|pwd=)[A-Za-z0-9.\-_:;=/@?,&]*/gi, 'jdbc-connection-string'],
161
- // JWT — three dot-separated b64url segments starting with eyJ
162
- [/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, 'jwt'],
163
- // PEM-encoded private keys
164
- [/-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g, 'private-key-block'],
165
- // Authorization headers — common copy-paste shape
166
- [/(?:Authorization|authorization)\s*:\s*Bearer\s+[A-Za-z0-9._~+/-]{20,}={0,2}/g, 'bearer-token'],
167
- // Hardcoded password literals — assignment shape with quoted value
168
- [/(password|passwd|secret|api[_-]?key|access[_-]?token)\s*[:=]\s*["'][^"'\n]{6,}["']/gi, 'hardcoded-credential'],
169
- ];
122
+ // Thin, read-only MCP adapter over the DataFlowGraph v1 artifact. Every
123
+ // piece of actual graph-loading and graph-query logic here is REUSED,
124
+ // unmodified, from scanner/src/server/ (built for the `explore` HTTP
125
+ // server, Milestone 3): loadSignedGraph does the signed-artifact
126
+ // load+verify, the four handleX functions do the lookups. This module
127
+ // adds nothing but MCP tool shape (name/description/inputSchema/handler)
128
+ // and MCP-appropriate error handling — no new graph-query logic is
129
+ // written here, on purpose (see this sub-project's own scoping doc).
130
+
170
131
 
171
- const SNIPPET_MAX = 2000;
172
- // OWASP A03 — cap input before running 14 regex patterns over it. A forged
173
- // last-scan.json could plant a 50MB description string; without this cap a
174
- // single explain_finding/query_taint call would peg CPU. After truncation
175
- // the snippet still gets the final SNIPPET_MAX trim downstream.
176
- const INPUT_MAX = 100_000;
177
-
178
- function redactString(s) {
179
- if (typeof s !== 'string') return s;
180
- let out = s;
181
- if (out.length > INPUT_MAX) out = out.slice(0, INPUT_MAX) + `…(+${out.length - INPUT_MAX})`;
182
- for (const [re, kind] of PATTERNS) {
183
- out = out.replace(re, `[REDACTED:${kind}]`);
184
- }
185
- if (out.length > SNIPPET_MAX) out = out.slice(0, SNIPPET_MAX) + `…(+${out.length - SNIPPET_MAX})`;
186
- return out;
187
- }
188
132
 
189
- // Deep-redact every string in a finding-like object (mutates returned copy).
190
- function redactFinding(f) {
191
- if (!f || typeof f !== 'object') return f;
192
- const out = { ...f };
193
- for (const k of ['snippet', 'description', 'remediation', 'title', 'vuln', 'message']) {
194
- if (typeof out[k] === 'string') out[k] = redactString(out[k]);
195
- }
196
- if (out.trace) {
197
- try { out.trace = JSON.parse(redactString(JSON.stringify(out.trace))); }
198
- catch { /* keep as-is if not round-trippable */ }
199
- }
200
- return out;
201
- }
202
133
 
203
- // Redact a freeform JSON-stringified argument blob (used by audit log).
204
- function redactArgsBlob(s) {
205
- return redactString(s);
134
+
135
+
136
+ const META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
137
+
138
+ function _loadOrFailure(sessionRoot) {
139
+ const loaded = (0,graph_loader.loadSignedGraph)(sessionRoot);
140
+ if (loaded.ok) return { graph: loaded.graph };
141
+ return {
142
+ failure: {
143
+ _meta: META,
144
+ hasResult: false,
145
+ reason: loaded.reason,
146
+ message: loaded.message,
147
+ },
148
+ };
206
149
  }
207
150
 
208
- // EXTERNAL MODULE: ./src/report/index.js + 3 modules
209
- var report = __webpack_require__(457);
210
- // EXTERNAL MODULE: ./src/posture/provenance/schema.js
211
- var schema = __webpack_require__(4594);
151
+ // Milestone 5, large-graph pagination: an optional `filter` input narrows the
152
+ // returned graph via the exact same `validateFilterShape`/`_filterGraph`
153
+ // pair the CLI's own `--filter` and the `explore` server's new
154
+ // `POST /api/v1/query` endpoint both already use — one real, shared
155
+ // primitive, not a third drifting copy. KNOWN, DISCLOSED GAP (still open):
156
+ // an OMITTED filter still returns the whole graph inline, with the same
157
+ // stdio.js MAX_LINE_BYTES (4MB) risk on a very large, unfiltered scan as
158
+ // before this change — this increment adds an opt-in capability for a
159
+ // caller that supplies a filter, it does not add a forced fallback/offload
160
+ // for a caller that doesn't. That remains a follow-up increment.
161
+ // Final whole-branch review finding: `filter: {}` is NOT the same as
162
+ // omitting `filter` — `_filterGraph` treats an empty (but well-formed)
163
+ // filter object as "narrow to nothing" (empty nodeIds/edgeIds Sets), so
164
+ // `filter: {}` returns an EMPTY graph (zero nodes/edges/flows), not the
165
+ // whole one. Called out explicitly in this tool's own `description` below
166
+ // so an agent reaching for "no restriction" reaches for OMITTING the
167
+ // argument, never for `{}`.
168
+ const dataflow_get_graph = {
169
+ name: 'dataflow_get_graph',
170
+ description: 'Return the DataFlowGraph v1 artifact from the last signed, verified deep-mode scan: nodes, edges, flows, scope, coverage, and limitations. Requires a prior `AGENTIC_SECURITY_LINEAGE_DEEP=1 agentic-security scan`. Optional `filter: {nodeIds, edgeIds}` narrows the returned nodes/edges/flows/dataElements (same primitive as the CLI\'s `--filter` and the `explore` server\'s `POST /api/v1/query`). IMPORTANT: omit `filter` entirely for the whole graph — passing `filter: {}` returns an EMPTY graph (zero nodes/edges/flows), not the whole one, since an empty filter narrows to nothing rather than meaning "no restriction". KNOWN GAP: an OMITTED filter still returns the whole graph inline with no pagination/offload — may exceed the stdio transport line cap on a very large, unfiltered graph; supply a real, non-empty filter to narrow the response.',
171
+ inputSchema: {
172
+ type: 'object',
173
+ additionalProperties: false,
174
+ properties: {
175
+ filter: {
176
+ type: 'object',
177
+ additionalProperties: false,
178
+ properties: {
179
+ nodeIds: { type: 'array', items: { type: 'string' } },
180
+ edgeIds: { type: 'array', items: { type: 'string' } },
181
+ },
182
+ },
183
+ },
184
+ },
185
+ async handler(args, ctx) {
186
+ const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
187
+ if (failure) return failure;
188
+ // Milestone 5, large-graph pagination: reuses the exact same
189
+ // validateFilterShape/_filterGraph pair the new POST /api/v1/query
190
+ // server endpoint and the CLI's own --filter both use — one real,
191
+ // shared primitive, not a third drifting copy.
192
+ const filterCheck = (0,export_json.validateFilterShape)(args?.filter);
193
+ if (!filterCheck.valid) {
194
+ return { _meta: META, hasResult: false, reason: 'invalid-filter', message: filterCheck.error };
195
+ }
196
+ const { status, body } = (0,routes/* handleGraph */.fn)(graph);
197
+ return {
198
+ _meta: META,
199
+ hasResult: true,
200
+ status,
201
+ data: (0,redact_graph/* _redactGraph */.zl)(args?.filter ? (0,export_json/* _filterGraph */.e)(body.data, args.filter) : body.data),
202
+ digest: body.digest,
203
+ schemaVersion: body.schemaVersion,
204
+ extensions: body.extensions,
205
+ scope: body.scope,
206
+ coverage: body.coverage,
207
+ limitations: body.limitations,
208
+ };
209
+ },
210
+ };
211
+
212
+ const dataflow_get_node = {
213
+ name: 'dataflow_get_node',
214
+ description: 'Look up one node by canonical id in the DataFlowGraph v1 artifact.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ additionalProperties: false,
218
+ properties: { id: { type: 'string', minLength: 1, maxLength: 512 } },
219
+ required: ['id'],
220
+ },
221
+ async handler({ id }, ctx) {
222
+ const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
223
+ if (failure) return failure;
224
+ const { status, body } = (0,routes/* handleNode */.d5)(graph, id);
225
+ return {
226
+ _meta: META,
227
+ hasResult: true,
228
+ notFound: status === 404,
229
+ data: (0,redact_graph/* _redactNode */.T2)(body.data),
230
+ canonicalIds: body.canonicalIds,
231
+ };
232
+ },
233
+ };
234
+
235
+ const dataflow_get_edge = {
236
+ name: 'dataflow_get_edge',
237
+ description: 'Look up one edge by canonical id in the DataFlowGraph v1 artifact.',
238
+ inputSchema: {
239
+ type: 'object',
240
+ additionalProperties: false,
241
+ properties: { id: { type: 'string', minLength: 1, maxLength: 512 } },
242
+ required: ['id'],
243
+ },
244
+ async handler({ id }, ctx) {
245
+ const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
246
+ if (failure) return failure;
247
+ const { status, body } = (0,routes/* handleEdge */.Yu)(graph, id);
248
+ return {
249
+ _meta: META,
250
+ hasResult: true,
251
+ notFound: status === 404,
252
+ data: body.data,
253
+ canonicalIds: body.canonicalIds,
254
+ };
255
+ },
256
+ };
257
+
258
+ const dataflow_get_flow = {
259
+ name: 'dataflow_get_flow',
260
+ description: 'Look up one flow by canonical id in the DataFlowGraph v1 artifact, including its contributing node/edge canonical ids.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ additionalProperties: false,
264
+ properties: { id: { type: 'string', minLength: 1, maxLength: 512 } },
265
+ required: ['id'],
266
+ },
267
+ async handler({ id }, ctx) {
268
+ const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
269
+ if (failure) return failure;
270
+ const { status, body } = (0,routes/* handleFlow */.jg)(graph, id);
271
+ return {
272
+ _meta: META,
273
+ hasResult: true,
274
+ notFound: status === 404,
275
+ data: body.data,
276
+ canonicalIds: body.canonicalIds,
277
+ };
278
+ },
279
+ };
280
+
212
281
  ;// CONCATENATED MODULE: ./src/posture/agents-memory.js
213
282
  // AGENTS.md — writable continual-learning memory (harness-anatomy #2).
214
283
  //
@@ -515,6 +584,7 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
515
584
  // (Sigstore/SLSA attestation) — see report/index.js's import comment.
516
585
 
517
586
 
587
+
518
588
  // Lazy-loaded: these transitively pull in npm packages (@babel/core and
519
589
  // friends) that aren't available in the plugin-cache install path
520
590
  // (no node_modules). Deferring keeps the MCP server bootable everywhere;
@@ -526,14 +596,14 @@ async function getRunScan() {
526
596
  }
527
597
  let _verifyFixCore;
528
598
  async function getVerifyFixCore() {
529
- if (!_verifyFixCore) _verifyFixCore = (await __webpack_require__.e(/* import() */ 526).then(__webpack_require__.bind(__webpack_require__, 3526))).verifyFix;
599
+ if (!_verifyFixCore) _verifyFixCore = (await __webpack_require__.e(/* import() */ 3526).then(__webpack_require__.bind(__webpack_require__, 3526))).verifyFix;
530
600
  return _verifyFixCore;
531
601
  }
532
602
 
533
603
  const MAX_FILES_PER_SCAN = 1024;
534
604
  const MAX_FILE_BYTES = 500_000;
535
605
  const MAX_TOTAL_SCAN_BYTES = 50_000_000;
536
- const META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
606
+ const tools_META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
537
607
 
538
608
  // OWASP A01 — refuse writes to paths that could subvert the security tool
539
609
  // itself or the host's source-control / dependency state. A forged finding
@@ -913,7 +983,7 @@ const scan_diff = {
913
983
  // scanning), left as-is rather than silently claimed fixed here.
914
984
  const findings = (0,report.normalizeFindings)(result.scan)
915
985
  .filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
916
- .map(f => redactFinding({
986
+ .map(f => (0,redact/* redactFinding */.lE)({
917
987
  id: f.id, severity: f.severity, file: f.file, line: f.line,
918
988
  title: f.vuln, cwe: f.cwe,
919
989
  description: f.description, remediation: f.remediation,
@@ -925,7 +995,7 @@ const scan_diff = {
925
995
  const off = _maybeOffload(sessionRoot, 'scan_diff', findings);
926
996
  if (off.offloaded) {
927
997
  return {
928
- _meta: META,
998
+ _meta: tools_META,
929
999
  scannedFiles: Object.keys(fileContents).length,
930
1000
  findingCount: off.total,
931
1001
  offloaded: true,
@@ -935,7 +1005,7 @@ const scan_diff = {
935
1005
  };
936
1006
  }
937
1007
  return {
938
- _meta: META,
1008
+ _meta: tools_META,
939
1009
  scannedFiles: Object.keys(fileContents).length,
940
1010
  findingCount: findings.length,
941
1011
  findings,
@@ -961,7 +1031,7 @@ const query_taint = {
961
1031
  async handler({ source, sink, limit, offset }, ctx) {
962
1032
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
963
1033
  if (!scan) {
964
- return { _meta: META, hasResult: false, status, message: `No usable scan state (${status}).` };
1034
+ return { _meta: tools_META, hasResult: false, status, message: `No usable scan state (${status}).` };
965
1035
  }
966
1036
  const lim = Number.isInteger(limit) ? Math.min(50, Math.max(1, limit)) : 10;
967
1037
  const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
@@ -976,13 +1046,13 @@ const query_taint = {
976
1046
  const hay = [f.description, f.title, f.vuln, f.snippet, JSON.stringify(f.trace || '')].join(' ').toLowerCase();
977
1047
  return hay.includes(srcL) && hay.includes(sinkL);
978
1048
  });
979
- const page = all.slice(off, off + lim).map(f => redactFinding({
1049
+ const page = all.slice(off, off + lim).map(f => (0,redact/* redactFinding */.lE)({
980
1050
  id: f.id, severity: f.severity, file: f.file, line: f.line,
981
1051
  title: f.title || f.vuln, description: f.description,
982
1052
  trace: f.trace || null,
983
1053
  }));
984
1054
  return {
985
- _meta: META,
1055
+ _meta: tools_META,
986
1056
  hasResult: true,
987
1057
  integrity: status,
988
1058
  scanStartedAt: scan.startedAt || scan.meta?.startedAt || null,
@@ -1014,7 +1084,7 @@ const explain_finding = {
1014
1084
  if (!scan) throw new Error(`No usable scan state (${status}).`);
1015
1085
  const f = _findById(scan, finding_id);
1016
1086
  if (!f) throw new Error(`Finding not found: ${finding_id}`);
1017
- const redacted = redactFinding({
1087
+ const redacted = (0,redact/* redactFinding */.lE)({
1018
1088
  id: f.id, severity: f.severity, file: f.file, line: f.line,
1019
1089
  title: f.title || f.vuln, cwe: f.cwe,
1020
1090
  description: f.description, remediation: f.remediation,
@@ -1039,7 +1109,7 @@ const explain_finding = {
1039
1109
  }
1040
1110
  }
1041
1111
  return {
1042
- _meta: META,
1112
+ _meta: tools_META,
1043
1113
  ...redacted,
1044
1114
  trace: traceTrimmed,
1045
1115
  traceOffload: traceMeta,
@@ -1150,16 +1220,16 @@ const apply_fix = {
1150
1220
  },
1151
1221
  async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
1152
1222
  if (confirm !== true) {
1153
- return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
1223
+ return { _meta: tools_META, applied: false, reason: 'apply_fix requires confirm: true.' };
1154
1224
  }
1155
1225
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
1156
1226
  if (!scan) {
1157
- return { _meta: META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
1227
+ return { _meta: tools_META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
1158
1228
  }
1159
1229
  const f = _findById(scan, finding_id);
1160
- if (!f) return { _meta: META, applied: false, reason: `Finding not found: ${finding_id}` };
1230
+ if (!f) return { _meta: tools_META, applied: false, reason: `Finding not found: ${finding_id}` };
1161
1231
  if (f._shadow === true) {
1162
- return { _meta: META, applied: false, reason: 'shadow findings cannot be auto-applied' };
1232
+ return { _meta: tools_META, applied: false, reason: 'shadow findings cannot be auto-applied' };
1163
1233
  }
1164
1234
 
1165
1235
  // #3 — verifier-approved patch path. When the caller supplies `patch` (a
@@ -1174,15 +1244,15 @@ const apply_fix = {
1174
1244
  // slip through — there is no token to replay, the verify runs here and now.
1175
1245
  if (patch && typeof patch === 'object' && Object.keys(patch).length) {
1176
1246
  if (!f.stableId) {
1177
- return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a patch against it' };
1247
+ return { _meta: tools_META, applied: false, reason: 'finding has no stableId — cannot verify a patch against it' };
1178
1248
  }
1179
1249
  const confinedAbs = {};
1180
1250
  for (const [rel, content] of Object.entries(patch)) {
1181
1251
  let abs;
1182
1252
  try { abs = _confine(ctx.sessionRoot, rel, 'patch key'); }
1183
- catch (e) { return { _meta: META, applied: false, reason: `path-escape refused: ${e.message}` }; }
1253
+ catch (e) { return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` }; }
1184
1254
  if (_isReservedWritePath(ctx.sessionRoot, abs)) {
1185
- return { _meta: META, applied: false, reason: `reserved path refused: ${rel}` };
1255
+ return { _meta: tools_META, applied: false, reason: `reserved path refused: ${rel}` };
1186
1256
  }
1187
1257
  confinedAbs[rel] = { abs, content: String(content) };
1188
1258
  }
@@ -1195,7 +1265,7 @@ const apply_fix = {
1195
1265
  // suite as a fourth verification leg (scan + lint + tests). Opt-in
1196
1266
  // because many repos have no runner and we must not fail-closed by
1197
1267
  // default. Normalized to the scan+lint verdict shape used below.
1198
- const { verifyFixWithTests } = await __webpack_require__.e(/* import() */ 113).then(__webpack_require__.bind(__webpack_require__, 4113));
1268
+ const { verifyFixWithTests } = await __webpack_require__.e(/* import() */ 4113).then(__webpack_require__.bind(__webpack_require__, 4113));
1199
1269
  const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
1200
1270
  verdict = { ok: t.ok, summary: t.summary, rescan: t.legs?.scan?.detail, lint: t.legs?.lint?.detail, tests: t.legs?.tests, testVerdict: t.verdict };
1201
1271
  } else {
@@ -1208,11 +1278,11 @@ const apply_fix = {
1208
1278
  });
1209
1279
  }
1210
1280
  } catch (e) {
1211
- return { _meta: META, applied: false, reason: `patch verification failed: ${e.message}` };
1281
+ return { _meta: tools_META, applied: false, reason: `patch verification failed: ${e.message}` };
1212
1282
  }
1213
1283
  if (!verdict.ok) {
1214
1284
  return {
1215
- _meta: META, applied: false,
1285
+ _meta: tools_META, applied: false,
1216
1286
  reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
1217
1287
  verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
1218
1288
  };
@@ -1235,7 +1305,7 @@ const apply_fix = {
1235
1305
  }
1236
1306
  const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
1237
1307
  if (dry_run) {
1238
- return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
1308
+ return { _meta: tools_META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
1239
1309
  }
1240
1310
  if (materialClassification.highImpactCategories.length) {
1241
1311
  const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
@@ -1244,7 +1314,7 @@ const apply_fix = {
1244
1314
  typeof approval.reason === 'string' && approval.reason.trim().length > 0);
1245
1315
  if (!hasApprovalEvidence) {
1246
1316
  return {
1247
- _meta: META, applied: false,
1317
+ _meta: tools_META, applied: false,
1248
1318
  reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
1249
1319
  materialClassification,
1250
1320
  };
@@ -1254,7 +1324,7 @@ const apply_fix = {
1254
1324
  const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
1255
1325
  if (!identityCheck.verified) {
1256
1326
  return {
1257
- _meta: META, applied: false,
1327
+ _meta: tools_META, applied: false,
1258
1328
  reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
1259
1329
  materialClassification,
1260
1330
  };
@@ -1264,7 +1334,7 @@ const apply_fix = {
1264
1334
  const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
1265
1335
  if (!sodCheck.ok) {
1266
1336
  return {
1267
- _meta: META, applied: false,
1337
+ _meta: tools_META, applied: false,
1268
1338
  reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
1269
1339
  materialClassification,
1270
1340
  };
@@ -1291,20 +1361,20 @@ const apply_fix = {
1291
1361
  try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
1292
1362
  }
1293
1363
  if (e && e.name === 'FixAttemptBudgetExceededError') {
1294
- return { _meta: META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
1364
+ return { _meta: tools_META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
1295
1365
  }
1296
1366
  throw e;
1297
1367
  }
1298
1368
  let acceptance = null;
1299
1369
  try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
1300
- return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
1370
+ return { _meta: tools_META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
1301
1371
  }
1302
1372
 
1303
1373
  if (typeof f.fix?.replacement !== 'string') {
1304
1374
  // Premortem #2: templates are patch-shaped text. Same reasoning as
1305
1375
  // the replacement path — do NOT pass through redactString here.
1306
1376
  return {
1307
- _meta: META, applied: false,
1377
+ _meta: tools_META, applied: false,
1308
1378
  reason: 'No full replacement available — only a template. Apply the template manually.',
1309
1379
  template: f.fix?.code || '',
1310
1380
  file: f.file, line: f.line,
@@ -1313,19 +1383,19 @@ const apply_fix = {
1313
1383
  let absFile;
1314
1384
  try { absFile = _confine(ctx.sessionRoot, f.file, 'finding.file'); }
1315
1385
  catch (e) {
1316
- return { _meta: META, applied: false, reason: `path-escape refused: ${e.message}` };
1386
+ return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` };
1317
1387
  }
1318
1388
  if (_isReservedWritePath(ctx.sessionRoot, absFile)) {
1319
- return { _meta: META, applied: false, reason: `reserved path refused: writes to .git/, .agentic-security/, or node_modules/ are not permitted via apply_fix` };
1389
+ return { _meta: tools_META, applied: false, reason: `reserved path refused: writes to .git/, .agentic-security/, or node_modules/ are not permitted via apply_fix` };
1320
1390
  }
1321
1391
  if (!external_node_fs_.existsSync(absFile)) {
1322
- return { _meta: META, applied: false, reason: `File not found: ${absFile}` };
1392
+ return { _meta: tools_META, applied: false, reason: `File not found: ${absFile}` };
1323
1393
  }
1324
1394
  const originalContent = await promises_.readFile(absFile, 'utf8');
1325
1395
 
1326
1396
  if (dry_run) {
1327
1397
  return {
1328
- _meta: META,
1398
+ _meta: tools_META,
1329
1399
  applied: false, dryRun: true,
1330
1400
  file: f.file,
1331
1401
  originalSize: originalContent.length,
@@ -1347,7 +1417,7 @@ const apply_fix = {
1347
1417
  // size-diff shape) but the load-bearing addition is the verification
1348
1418
  // gate before the write.
1349
1419
  if (!f.stableId) {
1350
- return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
1420
+ return { _meta: tools_META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
1351
1421
  }
1352
1422
  const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
1353
1423
  scanRoot: ctx.sessionRoot,
@@ -1357,9 +1427,9 @@ const apply_fix = {
1357
1427
  });
1358
1428
  if (!result.ok) {
1359
1429
  if (result.budgetExceeded) {
1360
- return { _meta: META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
1430
+ return { _meta: tools_META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
1361
1431
  }
1362
- return { _meta: META, applied: false, reason: result.reason, verify: result.verify || null };
1432
+ return { _meta: tools_META, applied: false, reason: result.reason, verify: result.verify || null };
1363
1433
  }
1364
1434
  // R25 (PRD §5): surface the running auto-fix acceptance rate after each
1365
1435
  // applied fix, so the closed loop reports its own success metric.
@@ -1370,7 +1440,7 @@ const apply_fix = {
1370
1440
  // FR-305: verifiedFull distinguishes "every required leg (lint, tests)
1371
1441
  // genuinely ran and passed" from "passed, but a required leg was
1372
1442
  // skipped or unavailable" — verified:true alone conflates them.
1373
- _meta: META, applied: true, verified: true, verifiedFull: result.verifiedFull,
1443
+ _meta: tools_META, applied: true, verified: true, verifiedFull: result.verifiedFull,
1374
1444
  historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
1375
1445
  integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
1376
1446
  verify: result.verify,
@@ -1439,7 +1509,7 @@ const verify_fix = {
1439
1509
  try {
1440
1510
  _confine(ctx.sessionRoot, relPath, 'files key');
1441
1511
  } catch (e) {
1442
- return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` };
1512
+ return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` };
1443
1513
  }
1444
1514
  confined[relPath] = String(content);
1445
1515
  }
@@ -1471,10 +1541,10 @@ const verify_fix = {
1471
1541
  fixMeta,
1472
1542
  });
1473
1543
  return {
1474
- _meta: META,
1544
+ _meta: tools_META,
1475
1545
  ok: r.ok,
1476
1546
  rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
1477
- lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: redactString(r.lint.output || '').slice(0, 1500) },
1547
+ lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: (0,redact/* redactString */.rd)(r.lint.output || '').slice(0, 1500) },
1478
1548
  // verifyFix computes five legs, not two — tests/honesty/poc were
1479
1549
  // being silently dropped here, leaving an agent with no structured
1480
1550
  // way to see WHY verification failed when the failure was in one
@@ -1485,11 +1555,11 @@ const verify_fix = {
1485
1555
  // it can echo proof-harness detail derived from scanned source.
1486
1556
  tests: r.tests,
1487
1557
  honesty: r.honesty,
1488
- poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
1558
+ poc: r.poc ? { ...r.poc, reason: r.poc.reason ? (0,redact/* redactString */.rd)(r.poc.reason) : r.poc.reason } : r.poc,
1489
1559
  summary: r.summary,
1490
1560
  };
1491
1561
  } catch (e) {
1492
- return { _meta: META, ok: false, reason: `verify_fix failed: ${e.message}` };
1562
+ return { _meta: tools_META, ok: false, reason: `verify_fix failed: ${e.message}` };
1493
1563
  }
1494
1564
  },
1495
1565
  };
@@ -1512,11 +1582,11 @@ const synthesize_fix = {
1512
1582
  async handler({ finding_id }, ctx) {
1513
1583
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
1514
1584
  if (!scan) {
1515
- return { _meta: META, ok: false, reason: `last-scan.json failed integrity check: ${status}` };
1585
+ return { _meta: tools_META, ok: false, reason: `last-scan.json failed integrity check: ${status}` };
1516
1586
  }
1517
1587
  const f = _findById(scan, finding_id);
1518
- if (!f) return { _meta: META, ok: false, reason: `Finding not found: ${finding_id}` };
1519
- if (f._shadow === true) return { _meta: META, ok: false, reason: 'shadow findings have no synthesized fix' };
1588
+ if (!f) return { _meta: tools_META, ok: false, reason: `Finding not found: ${finding_id}` };
1589
+ if (f._shadow === true) return { _meta: tools_META, ok: false, reason: 'shadow findings have no synthesized fix' };
1520
1590
  const fix = f.fix || {};
1521
1591
  const hasReplacement = typeof fix.replacement === 'string' && fix.replacement.length > 0;
1522
1592
  // Patch bounds: count files touched + LoC delta.
@@ -1552,7 +1622,7 @@ const synthesize_fix = {
1552
1622
  // verbatim. Snippet/description/etc. continue to be redacted in
1553
1623
  // explain_finding / scan_diff — that's the right surface for redaction.
1554
1624
  return {
1555
- _meta: META,
1625
+ _meta: tools_META,
1556
1626
  ok: true,
1557
1627
  stable_id: f.stableId || null,
1558
1628
  file: f.file, line: f.line,
@@ -1602,14 +1672,14 @@ const find_rule_module = {
1602
1672
  },
1603
1673
  async handler({ cwe, family }, ctx) {
1604
1674
  if (!cwe && !family) {
1605
- return { _meta: META, ok: false, reason: 'provide cwe (e.g. "CWE-89") or family (e.g. "sql-injection")' };
1675
+ return { _meta: tools_META, ok: false, reason: 'provide cwe (e.g. "CWE-89") or family (e.g. "sql-injection")' };
1606
1676
  }
1607
1677
  // Pattern enforcement — the mini-schema validator doesn't do `pattern`.
1608
1678
  if (cwe && !/^CWE-\d+$/.test(cwe)) {
1609
- return { _meta: META, ok: false, reason: 'cwe must match /^CWE-\\d+$/ (e.g. "CWE-89")' };
1679
+ return { _meta: tools_META, ok: false, reason: 'cwe must match /^CWE-\\d+$/ (e.g. "CWE-89")' };
1610
1680
  }
1611
1681
  if (family && !/^[a-z][a-z0-9-]+$/.test(family)) {
1612
- return { _meta: META, ok: false, reason: 'family must match /^[a-z][a-z0-9-]+$/ (e.g. "sql-injection")' };
1682
+ return { _meta: tools_META, ok: false, reason: 'family must match /^[a-z][a-z0-9-]+$/ (e.g. "sql-injection")' };
1613
1683
  }
1614
1684
  const sessionRoot = ctx.sessionRoot;
1615
1685
  const roots = [
@@ -1663,7 +1733,7 @@ const find_rule_module = {
1663
1733
  if (hits.length >= 20) break;
1664
1734
  }
1665
1735
  return {
1666
- _meta: META,
1736
+ _meta: tools_META,
1667
1737
  ok: true,
1668
1738
  query: { cwe: cwe || null, family: family || null },
1669
1739
  hitCount: hits.length,
@@ -1706,14 +1776,14 @@ const append_scratchpad = {
1706
1776
  },
1707
1777
  async handler({ path: relPath, content }, ctx) {
1708
1778
  const v = _validateScratchpadPath(relPath);
1709
- if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1779
+ if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
1710
1780
  let abs;
1711
1781
  try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1712
- catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1782
+ catch (e) { return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1713
1783
  const total = _scratchpadTotalBytes(ctx.sessionRoot);
1714
1784
  if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
1715
1785
  return {
1716
- _meta: META, ok: false,
1786
+ _meta: tools_META, ok: false,
1717
1787
  reason: `scratchpad-total-exceeded: ${total} + ${content.length} > ${SCRATCHPAD_MAX_TOTAL_BYTES}. Clean up via "rm -rf .agentic-security/agent-scratchpad" or rotate sessions.`,
1718
1788
  };
1719
1789
  }
@@ -1721,7 +1791,7 @@ const append_scratchpad = {
1721
1791
  try { if (external_node_fs_.existsSync(abs)) existing = external_node_fs_.statSync(abs).size; } catch {}
1722
1792
  if (existing + content.length > SCRATCHPAD_MAX_FILE_BYTES) {
1723
1793
  return {
1724
- _meta: META, ok: false,
1794
+ _meta: tools_META, ok: false,
1725
1795
  reason: `scratchpad-file-exceeded: ${existing} + ${content.length} > ${SCRATCHPAD_MAX_FILE_BYTES}. Start a new file.`,
1726
1796
  };
1727
1797
  }
@@ -1729,12 +1799,12 @@ const append_scratchpad = {
1729
1799
  external_node_fs_.mkdirSync(external_node_path_.dirname(abs), { recursive: true });
1730
1800
  external_node_fs_.appendFileSync(abs, content);
1731
1801
  return {
1732
- _meta: META, ok: true,
1802
+ _meta: tools_META, ok: true,
1733
1803
  path: relPath, bytesWritten: content.length, fileSize: existing + content.length,
1734
1804
  scratchpadTotal: total + content.length,
1735
1805
  };
1736
1806
  } catch (e) {
1737
- return { _meta: META, ok: false, reason: `write-failed: ${e.message}` };
1807
+ return { _meta: tools_META, ok: false, reason: `write-failed: ${e.message}` };
1738
1808
  }
1739
1809
  },
1740
1810
  };
@@ -1754,14 +1824,14 @@ const read_scratchpad = {
1754
1824
  },
1755
1825
  async handler({ path: relPath, offset, limit }, ctx) {
1756
1826
  const v = _validateScratchpadPath(relPath);
1757
- if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1827
+ if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
1758
1828
  let abs;
1759
1829
  try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1760
- catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1761
- if (!external_node_fs_.existsSync(abs)) return { _meta: META, ok: false, reason: 'not-found' };
1830
+ catch (e) { return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1831
+ if (!external_node_fs_.existsSync(abs)) return { _meta: tools_META, ok: false, reason: 'not-found' };
1762
1832
  let stat;
1763
- try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: META, ok: false, reason: `stat-failed: ${e.message}` }; }
1764
- if (!stat.isFile()) return { _meta: META, ok: false, reason: 'not-a-file' };
1833
+ try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: tools_META, ok: false, reason: `stat-failed: ${e.message}` }; }
1834
+ if (!stat.isFile()) return { _meta: tools_META, ok: false, reason: 'not-a-file' };
1765
1835
  const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
1766
1836
  const lim = Number.isInteger(limit) ? Math.min(64 * 1024, Math.max(1, limit)) : 4096;
1767
1837
  let buf;
@@ -1771,10 +1841,10 @@ const read_scratchpad = {
1771
1841
  const read = external_node_fs_.readSync(fd, tmp, 0, lim, off);
1772
1842
  external_node_fs_.closeSync(fd);
1773
1843
  buf = tmp.slice(0, read);
1774
- } catch (e) { return { _meta: META, ok: false, reason: `read-failed: ${e.message}` }; }
1844
+ } catch (e) { return { _meta: tools_META, ok: false, reason: `read-failed: ${e.message}` }; }
1775
1845
  const text = buf.toString('utf8');
1776
1846
  return {
1777
- _meta: META, ok: true,
1847
+ _meta: tools_META, ok: true,
1778
1848
  path: relPath,
1779
1849
  offset: off, limit: lim, bytesRead: buf.length,
1780
1850
  totalSize: stat.size,
@@ -1806,7 +1876,7 @@ const append_agents_memory = {
1806
1876
  },
1807
1877
  async handler({ agent, body }, ctx) {
1808
1878
  const r = appendAgentsMemory(ctx.sessionRoot, { agent, body });
1809
- return { _meta: META, ...r };
1879
+ return { _meta: tools_META, ...r };
1810
1880
  },
1811
1881
  };
1812
1882
 
@@ -1822,16 +1892,16 @@ const read_agents_memory = {
1822
1892
  },
1823
1893
  async handler({ full }, ctx) {
1824
1894
  const body = readAgentsMemory(ctx.sessionRoot);
1825
- if (!body) return { _meta: META, present: false };
1826
- if (full) return { _meta: META, present: true, length: body.length, content: body };
1895
+ if (!body) return { _meta: tools_META, present: false };
1896
+ if (full) return { _meta: tools_META, present: true, length: body.length, content: body };
1827
1897
  // Tail-only — same logic as summarizeForSession but inlined to avoid a
1828
1898
  // second import surface.
1829
1899
  const limit = 6 * 1024;
1830
- if (body.length <= limit) return { _meta: META, present: true, length: body.length, content: body };
1900
+ if (body.length <= limit) return { _meta: tools_META, present: true, length: body.length, content: body };
1831
1901
  const tail = body.slice(-limit);
1832
1902
  const firstSection = tail.indexOf('\n## ');
1833
1903
  const slice = firstSection >= 0 ? tail.slice(firstSection) : tail;
1834
- return { _meta: META, present: true, length: body.length, truncated: true, content: slice };
1904
+ return { _meta: tools_META, present: true, length: body.length, truncated: true, content: slice };
1835
1905
  },
1836
1906
  };
1837
1907
 
@@ -1865,10 +1935,10 @@ const query_triage_memory = {
1865
1935
  // they landed in, rather than hardcoding a field allowlist that could
1866
1936
  // miss one.
1867
1937
  let results;
1868
- try { results = JSON.parse(redactString(JSON.stringify(raw))); }
1938
+ try { results = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
1869
1939
  catch { results = raw; }
1870
1940
  return {
1871
- _meta: META,
1941
+ _meta: tools_META,
1872
1942
  count: results.length,
1873
1943
  results,
1874
1944
  };
@@ -1893,7 +1963,7 @@ const query_findings_memory = {
1893
1963
  required: ['query'],
1894
1964
  },
1895
1965
  async handler({ query }, ctx) {
1896
- const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 839).then(__webpack_require__.bind(__webpack_require__, 3839));
1966
+ const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 3839).then(__webpack_require__.bind(__webpack_require__, 3839));
1897
1967
  const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
1898
1968
  // Stage 6 correctness audit — same redaction gap and same fix as
1899
1969
  // query_triage_memory just above: this mixes four differently-shaped
@@ -1901,9 +1971,9 @@ const query_findings_memory = {
1901
1971
  // whole-structure redactString round-trip is applied rather than a
1902
1972
  // per-field allowlist that could miss one of the four shapes.
1903
1973
  let body;
1904
- try { body = JSON.parse(redactString(JSON.stringify(raw))); }
1974
+ try { body = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
1905
1975
  catch { body = raw; }
1906
- return { _meta: META, ...body };
1976
+ return { _meta: tools_META, ...body };
1907
1977
  },
1908
1978
  };
1909
1979
 
@@ -1924,7 +1994,7 @@ const lookup_cve = {
1924
1994
  },
1925
1995
  async handler({ cve }, _ctx) {
1926
1996
  const r = lookupCve(cve);
1927
- return { _meta: META, ...r };
1997
+ return { _meta: tools_META, ...r };
1928
1998
  },
1929
1999
  };
1930
2000
 
@@ -1942,9 +2012,9 @@ const query_cache_telemetry = {
1942
2012
  },
1943
2013
  async handler({ transcript_path } = {}, ctx) {
1944
2014
  const result = (0,cache_economics.analyzeTranscript)({ transcriptPath: transcript_path, projectDir: ctx?.sessionRoot || process.cwd() });
1945
- if (!result.ok) return { _meta: META, ok: false, reason: result.reason };
2015
+ if (!result.ok) return { _meta: tools_META, ok: false, reason: result.reason };
1946
2016
  return {
1947
- _meta: META,
2017
+ _meta: tools_META,
1948
2018
  ok: true,
1949
2019
  metrics: result.metrics,
1950
2020
  leaks: result.leaks,
@@ -1960,7 +2030,7 @@ const query_cache_telemetry = {
1960
2030
  // ecosystem's native --dry-run command. Safe to call any number of times.
1961
2031
  let _scaUpgrade;
1962
2032
  async function _getScaUpgrade() {
1963
- if (!_scaUpgrade) _scaUpgrade = await __webpack_require__.e(/* import() */ 333).then(__webpack_require__.bind(__webpack_require__, 5333));
2033
+ if (!_scaUpgrade) _scaUpgrade = await __webpack_require__.e(/* import() */ 5333).then(__webpack_require__.bind(__webpack_require__, 5333));
1964
2034
  return _scaUpgrade;
1965
2035
  }
1966
2036
  const synthesize_sca_upgrade = {
@@ -1980,11 +2050,11 @@ const synthesize_sca_upgrade = {
1980
2050
  const f = _findById(scan, finding_id);
1981
2051
  if (!f) throw new Error(`Finding not found: ${finding_id}`);
1982
2052
  if (f.type !== 'vulnerable_dep') {
1983
- return { _meta: META, ok: false, reason: 'finding is not an SCA vulnerable_dep — use synthesize_fix for SAST findings' };
2053
+ return { _meta: tools_META, ok: false, reason: 'finding is not an SCA vulnerable_dep — use synthesize_fix for SAST findings' };
1984
2054
  }
1985
2055
  const { planScaUpgrade } = await _getScaUpgrade();
1986
2056
  const plan = await planScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f });
1987
- return { _meta: META, ...plan };
2057
+ return { _meta: tools_META, ...plan };
1988
2058
  },
1989
2059
  };
1990
2060
 
@@ -2011,24 +2081,24 @@ const apply_sca_upgrade = {
2011
2081
  },
2012
2082
  async handler({ finding_id, confirm, run_tests = true }, ctx) {
2013
2083
  if (confirm !== true) {
2014
- return { _meta: META, applied: false, reason: 'apply_sca_upgrade requires confirm: true.' };
2084
+ return { _meta: tools_META, applied: false, reason: 'apply_sca_upgrade requires confirm: true.' };
2015
2085
  }
2016
2086
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
2017
2087
  if (!scan) {
2018
- return { _meta: META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
2088
+ return { _meta: tools_META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
2019
2089
  }
2020
2090
  const f = _findById(scan, finding_id);
2021
- if (!f) return { _meta: META, applied: false, reason: `Finding not found: ${finding_id}` };
2091
+ if (!f) return { _meta: tools_META, applied: false, reason: `Finding not found: ${finding_id}` };
2022
2092
  if (f.type !== 'vulnerable_dep') {
2023
- return { _meta: META, applied: false, reason: 'finding is not an SCA vulnerable_dep — use apply_fix for SAST findings' };
2093
+ return { _meta: tools_META, applied: false, reason: 'finding is not an SCA vulnerable_dep — use apply_fix for SAST findings' };
2024
2094
  }
2025
2095
  const { applyScaUpgrade } = await _getScaUpgrade();
2026
2096
  const result = await applyScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f, runTests: run_tests });
2027
- return { _meta: META, ...result };
2097
+ return { _meta: tools_META, ...result };
2028
2098
  },
2029
2099
  };
2030
2100
 
2031
- const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry];
2101
+ const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry, dataflow_get_graph, dataflow_get_node, dataflow_get_edge, dataflow_get_flow];
2032
2102
 
2033
2103
  ;// CONCATENATED MODULE: ./src/mcp/validate.js
2034
2104
  // Minimal JSON Schema validator — just the subset our tool schemas use.
@@ -2126,7 +2196,7 @@ const SESSION_ID = `${process.pid}-${Date.now().toString(36).slice(-6)}`;
2126
2196
  function _summarize(args) {
2127
2197
  let s;
2128
2198
  try { s = JSON.stringify(args); } catch { s = '<unserializable>'; }
2129
- s = redactArgsBlob(s);
2199
+ s = (0,redact/* redactArgsBlob */.MC)(s);
2130
2200
  if (s.length > MAX_ARG_BYTES) s = s.slice(0, MAX_ARG_BYTES) + `…(+${s.length - MAX_ARG_BYTES})`;
2131
2201
  return s;
2132
2202
  }
@@ -2271,7 +2341,7 @@ const TOOLS_BY_NAME = Object.fromEntries(ALL_TOOLS.map(t => [t.name, t]));
2271
2341
  function _codeFingerprint() {
2272
2342
  try {
2273
2343
  const here = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
2274
- const files = ['server.js', 'tools.js', 'stdio.js', 'audit.js', 'validate.js', 'redact.js'];
2344
+ const files = ['server.js', 'tools.js', 'dataflow-tools.js', 'stdio.js', 'audit.js', 'validate.js', 'redact.js'];
2275
2345
  const h = external_node_crypto_.createHash('sha256');
2276
2346
  for (const f of files) {
2277
2347
  try { h.update(f); h.update(external_node_fs_.readFileSync(external_node_path_.join(here, f))); } catch {}
@@ -2759,6 +2829,307 @@ const _internal = {
2759
2829
  };
2760
2830
 
2761
2831
 
2832
+ /***/ }),
2833
+
2834
+ /***/ 8218:
2835
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
2836
+
2837
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2838
+ /* harmony export */ loadFreshLineageGraph: () => (/* binding */ loadFreshLineageGraph),
2839
+ /* harmony export */ loadSignedGraph: () => (/* binding */ loadSignedGraph)
2840
+ /* harmony export */ });
2841
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
2842
+ /* harmony import */ var _posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1174);
2843
+ /* harmony import */ var _posture_integrity_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1130);
2844
+ // graph-loader.js — Milestone 3, sub-project Server, increment 1.
2845
+ //
2846
+ // Reads and VERIFIES the `.agentic-security/lineage-graph.json` artifact
2847
+ // before `explore` is allowed to serve a single byte of it. Reuses
2848
+ // `posture/integrity.js`'s `verifyLastScan` DIRECTLY (per the plan and the
2849
+ // root CLAUDE.md's own instruction) — this module does not implement any
2850
+ // signature comparison of its own. `verifyLastScan` already uses
2851
+ // `crypto.timingSafeEqual` internally.
2852
+ //
2853
+ // Loaded ONCE at server startup (see bin/agentic-security.js's cmdExplore)
2854
+ // and held in memory for the life of the process — this is a read-only,
2855
+ // single-scan-snapshot server; a change to the graph on disk mid-session is
2856
+ // out of scope for this increment (threat-model doc's own "P0 is
2857
+ // read-only" framing).
2858
+
2859
+
2860
+
2861
+
2862
+
2863
+ /**
2864
+ * @param {string} scanRoot
2865
+ * @returns {{ok:true, graph:object} | {ok:false, reason:'missing'|'unsigned'|'tampered'|'malformed', message:string}}
2866
+ *
2867
+ * Four, and only four, distinct failure reasons — each with its own clear
2868
+ * message so an operator knows exactly what to do next:
2869
+ * - 'missing' — no lineage-graph.json at all. Run a scan with
2870
+ * AGENTIC_SECURITY_LINEAGE_DEEP=1 first.
2871
+ * - 'unsigned' — the graph exists but its .sig sibling does not
2872
+ * (verifyLastScan returns null). Refuse to serve an
2873
+ * unverifiable graph.
2874
+ * - 'tampered' — the graph exists and has a .sig, but the signature does
2875
+ * not match the body (verifyLastScan returns false). The
2876
+ * file was modified after signing, or signed under a
2877
+ * different install key.
2878
+ * - 'malformed' — the body passed signature verification but is not
2879
+ * valid JSON. Should not happen from a normal scan; the
2880
+ * file may be corrupted on disk after signing.
2881
+ */
2882
+ function loadSignedGraph(scanRoot) {
2883
+ const graphPath = (0,_posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, 'lineage-graph.json');
2884
+ const sigPath = graphPath + '.sig';
2885
+
2886
+ if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(graphPath)) {
2887
+ return {
2888
+ ok: false,
2889
+ reason: 'missing',
2890
+ message: `No lineage graph found at ${graphPath}. Run a scan with AGENTIC_SECURITY_LINEAGE_DEEP=1 first (e.g. \`AGENTIC_SECURITY_LINEAGE_DEEP=1 agentic-security scan\`), then re-run \`agentic-security explore\`.`,
2891
+ };
2892
+ }
2893
+
2894
+ let body;
2895
+ try {
2896
+ body = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(graphPath, 'utf8');
2897
+ } catch (e) {
2898
+ return {
2899
+ ok: false,
2900
+ reason: 'missing',
2901
+ message: `Lineage graph found at ${graphPath} but could not be read: ${e && e.message ? e.message : e}.`,
2902
+ };
2903
+ }
2904
+
2905
+ const verified = (0,_posture_integrity_js__WEBPACK_IMPORTED_MODULE_2__/* .verifyLastScan */ .Ef)(body, sigPath);
2906
+ if (verified === null) {
2907
+ return {
2908
+ ok: false,
2909
+ reason: 'unsigned',
2910
+ message: `Lineage graph at ${graphPath} has no signature file (${sigPath} is missing). Refusing to serve an unverifiable graph. Re-run the scan (AGENTIC_SECURITY_LINEAGE_DEEP=1) to regenerate both files together.`,
2911
+ };
2912
+ }
2913
+ if (verified === false) {
2914
+ return {
2915
+ ok: false,
2916
+ reason: 'tampered',
2917
+ message: `Lineage graph at ${graphPath} FAILED signature verification — its contents do not match ${sigPath}. The file may have been modified after the scan, or signed under a different install key. Refusing to serve a tampered graph. Re-run the scan to regenerate it.`,
2918
+ };
2919
+ }
2920
+
2921
+ let graph;
2922
+ try {
2923
+ graph = JSON.parse(body);
2924
+ } catch (e) {
2925
+ return {
2926
+ ok: false,
2927
+ reason: 'malformed',
2928
+ message: `Lineage graph at ${graphPath} passed signature verification but is not valid JSON (${e && e.message ? e.message : e}). This should not happen from a normal scan — the file may be corrupted. Re-run the scan to regenerate it.`,
2929
+ };
2930
+ }
2931
+
2932
+ return { ok: true, graph };
2933
+ }
2934
+
2935
+ /**
2936
+ * Load .agentic-security/lineage-graph.json ONLY when it is genuinely
2937
+ * fresh for THIS scan — never merely because a file happens to exist on
2938
+ * disk. Shared by every caller that signs or narrates a graph:-derived
2939
+ * compliance claim (M4 sub-project 6c's final whole-branch review found
2940
+ * the identical staleness gap independently reachable from
2941
+ * `attest --obligations` AND `compliance --walkthrough`, and required
2942
+ * this predicate to live in exactly one place rather than being
2943
+ * copy-pasted per caller — a safety check that drifts between two
2944
+ * near-identical inline copies is worse than one shared bug).
2945
+ *
2946
+ * `.agentic-security/lineage-graph.json` is only rewritten when a scan
2947
+ * actually finishes building a graph (`if (scan.lineageGraph)` in
2948
+ * bin/agentic-security.js's persistence code) — an ordinary non-deep
2949
+ * rescan, or a deep scan whose lineage build fails, leaves whatever file
2950
+ * was there from an earlier successful deep scan untouched. Loading that
2951
+ * stale graph and joining it to the CURRENT scan's other data would let a
2952
+ * caller assert a graph-derived fact (e.g. "transit protected") about
2953
+ * code that has since changed.
2954
+ *
2955
+ * `enabled: true` in `scan.scanHealth.lineageAnalysis` does NOT by itself
2956
+ * mean the build succeeded — engine.js sets it the moment
2957
+ * AGENTIC_SECURITY_LINEAGE_DEEP=1 is read, before the build even starts,
2958
+ * and leaves it `true` even when the build later throws (only `failure`
2959
+ * gets set in that case). `requested && enabled` alone therefore still
2960
+ * accepts a stale graph after a failed rebuild — reproduced live via the
2961
+ * scan's own already-shipped fault-injection fixture
2962
+ * (test/lineage-fault-injection.test.js) before this `failure === null`
2963
+ * check was added.
2964
+ *
2965
+ * @param {string} scanRoot
2966
+ * @param {object} scan - the parsed last-scan.json for the CURRENT scan
2967
+ * @returns {{graph:object|null, fresh:boolean, loaded:ReturnType<typeof loadSignedGraph>}}
2968
+ * `fresh` is true only when a signed graph loaded successfully AND this
2969
+ * scan's own scanHealth confirms lineage analysis was requested,
2970
+ * enabled, and did not fail. `graph` is `loaded.graph` when fresh, else
2971
+ * `null` — never the stale file, even when one exists on disk.
2972
+ * `loaded` is the raw `loadSignedGraph` result, so a caller can still
2973
+ * distinguish "no file at all" from "a file exists but isn't fresh" for
2974
+ * its own disclosure message.
2975
+ */
2976
+ function loadFreshLineageGraph(scanRoot, scan) {
2977
+ const la = scan?.scanHealth?.lineageAnalysis;
2978
+ const requested = la?.requested === true;
2979
+ const enabled = la?.enabled === true;
2980
+ const failure = la?.failure ?? null;
2981
+ const loaded = loadSignedGraph(scanRoot);
2982
+ const fresh = loaded.ok && requested && enabled && failure === null;
2983
+ return { graph: fresh ? loaded.graph : null, fresh, loaded };
2984
+ }
2985
+
2986
+
2987
+ /***/ }),
2988
+
2989
+ /***/ 4268:
2990
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
2991
+
2992
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2993
+ /* harmony export */ Yo: () => (/* binding */ handleScan),
2994
+ /* harmony export */ Yu: () => (/* binding */ handleEdge),
2995
+ /* harmony export */ d5: () => (/* binding */ handleNode),
2996
+ /* harmony export */ fn: () => (/* binding */ handleGraph),
2997
+ /* harmony export */ jg: () => (/* binding */ handleFlow),
2998
+ /* harmony export */ rR: () => (/* binding */ handleQuery)
2999
+ /* harmony export */ });
3000
+ /* unused harmony export wrapResponse */
3001
+ /* harmony import */ var _lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(859);
3002
+ // routes.js — Milestone 3, sub-project Server, increment 1.
3003
+ //
3004
+ // Five pure GET-endpoint handlers, each `(graph, ...) -> {status, body}`.
3005
+ // No req/res access anywhere in this file — that is what makes these
3006
+ // handlers unit-testable without an HTTP layer at all. http-server.js is
3007
+ // the only module that touches node:http and calls into these.
3008
+ //
3009
+ // Every response body is wrapped in `wrapResponse`, which adds the exact
3010
+ // envelope fields PRD line 1326 names (quoted in the implementation plan):
3011
+ // "base graph/snapshot digest, schema/extension versions, scope, coverage,
3012
+ // limitations, and contributing canonical IDs."
3013
+
3014
+
3015
+
3016
+ /**
3017
+ * Shared response envelope. Maps PRD line 1326's required fields onto the
3018
+ * graph's own real fields:
3019
+ * - digest -> graph.graphId (the base graph/snapshot digest)
3020
+ * - schemaVersion -> graph.schemaVersion
3021
+ * - extensions -> graph.extensions (schema/extension versions —
3022
+ * today always `{}`; see schema.js)
3023
+ * - scope -> graph.scope
3024
+ * - coverage -> graph.coverage
3025
+ * - limitations -> graph.limitations
3026
+ * - canonicalIds -> see the design note below
3027
+ *
3028
+ * "contributing canonical IDs" design decision (disclosed per the plan):
3029
+ * for `handleScan`/`handleGraph`, which describe the WHOLE graph rather
3030
+ * than one entity, `canonicalIds` is `null` — the response body for
3031
+ * `handleGraph` already IS the full nodes/edges/flows arrays, so echoing
3032
+ * every id again here would be pure duplication with no informational
3033
+ * gain, and for a large graph would materially bloat the response for
3034
+ * zero benefit. For `handleNode`/`handleEdge`, `canonicalIds` is the
3035
+ * single id the response is about. For `handleFlow`, `canonicalIds` is
3036
+ * the flow's own id PLUS the node/edge ids that flow's evidence draws
3037
+ * from (source, sink, edgeIds) — a flow is a derived record referencing
3038
+ * several underlying entities, and naming all of them here is genuinely
3039
+ * useful metadata a client would otherwise have to re-derive from the
3040
+ * flow body itself.
3041
+ */
3042
+ function wrapResponse(data, graph, { canonicalIds = null } = {}) {
3043
+ return {
3044
+ digest: graph?.graphId ?? null,
3045
+ schemaVersion: graph?.schemaVersion ?? null,
3046
+ extensions: graph?.extensions ?? {},
3047
+ scope: graph?.scope ?? null,
3048
+ coverage: graph?.coverage ?? null,
3049
+ limitations: graph?.limitations ?? [],
3050
+ canonicalIds,
3051
+ data,
3052
+ };
3053
+ }
3054
+
3055
+ function _findById(list, id) {
3056
+ if (!Array.isArray(list)) return null;
3057
+ return list.find((item) => item && item.id === id) ?? null;
3058
+ }
3059
+
3060
+ /** Scan/graph metadata — NOT the full node/edge arrays. */
3061
+ function handleScan(graph) {
3062
+ const data = {
3063
+ schemaVersion: graph?.schemaVersion ?? null,
3064
+ graphId: graph?.graphId ?? null,
3065
+ generatedAt: graph?.generatedAt ?? null,
3066
+ scope: graph?.scope ?? null,
3067
+ scanHealth: graph?.scanHealth ?? null,
3068
+ coverage: graph?.coverage ?? null,
3069
+ };
3070
+ return { status: 200, body: wrapResponse(data, graph, { canonicalIds: null }) };
3071
+ }
3072
+
3073
+ /** The full graph document, unfiltered. For a scoped/narrowed projection, use `handleQuery` (`POST /api/v1/query`, Milestone 5) below instead. */
3074
+ function handleGraph(graph) {
3075
+ return { status: 200, body: wrapResponse(graph, graph, { canonicalIds: null }) };
3076
+ }
3077
+
3078
+ /**
3079
+ * A deterministic typed projection query — Milestone 5's own
3080
+ * `POST /api/v1/query`, the S2 endpoint `handleGraph`'s own header
3081
+ * comment named and deferred. `filter` is the exact `{nodeIds, edgeIds}`
3082
+ * shape `dataflow export --filter`/`exportGraphJSON` already use — reused
3083
+ * via `_filterGraph`, never reimplemented. Final whole-branch review
3084
+ * finding: `undefined` (filter omitted entirely) returns the WHOLE graph,
3085
+ * identical to `handleGraph` — but `{}` (an empty, well-formed filter
3086
+ * object) is NOT the same thing, and does NOT mean "no restriction": both
3087
+ * `nodeIds`/`edgeIds` default to empty Sets inside `_filterGraph`, so `{}`
3088
+ * narrows the graph down to EMPTY node/edge/flow/dataElement arrays. A
3089
+ * caller that wants the whole graph must omit `filter` entirely, never
3090
+ * pass `{}` meaning "everything." A malformed filter is a 400, never a
3091
+ * thrown exception reaching the caller.
3092
+ */
3093
+ function handleQuery(graph, filter) {
3094
+ const check = (0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__.validateFilterShape)(filter);
3095
+ if (!check.valid) {
3096
+ return { status: 400, body: { error: check.error } };
3097
+ }
3098
+ return { status: 200, body: wrapResponse((0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__/* ._filterGraph */ .e)(graph, filter), graph, { canonicalIds: null }) };
3099
+ }
3100
+
3101
+ /** Look up one node by id. 404 with a clear body if not found. */
3102
+ function handleNode(graph, id) {
3103
+ const node = _findById(graph?.nodes, id);
3104
+ if (!node) {
3105
+ return { status: 404, body: wrapResponse({ error: `node not found: ${id}` }, graph, { canonicalIds: [] }) };
3106
+ }
3107
+ return { status: 200, body: wrapResponse(node, graph, { canonicalIds: [id] }) };
3108
+ }
3109
+
3110
+ /** Look up one edge by id. 404 with a clear body if not found. */
3111
+ function handleEdge(graph, id) {
3112
+ const edge = _findById(graph?.edges, id);
3113
+ if (!edge) {
3114
+ return { status: 404, body: wrapResponse({ error: `edge not found: ${id}` }, graph, { canonicalIds: [] }) };
3115
+ }
3116
+ return { status: 200, body: wrapResponse(edge, graph, { canonicalIds: [id] }) };
3117
+ }
3118
+
3119
+ /** Look up one flow by id. 404 with a clear body if not found. */
3120
+ function handleFlow(graph, id) {
3121
+ const flow = _findById(graph?.flows, id);
3122
+ if (!flow) {
3123
+ return { status: 404, body: wrapResponse({ error: `flow not found: ${id}` }, graph, { canonicalIds: [] }) };
3124
+ }
3125
+ const contributing = new Set([id]);
3126
+ if (flow.source) contributing.add(flow.source);
3127
+ if (flow.sink) contributing.add(flow.sink);
3128
+ for (const eid of (flow.edgeIds || [])) contributing.add(eid);
3129
+ return { status: 200, body: wrapResponse(flow, graph, { canonicalIds: [...contributing] }) };
3130
+ }
3131
+
3132
+
2762
3133
  /***/ })
2763
3134
 
2764
3135
  };