@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
@@ -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,111 +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
+
131
+
132
+
170
133
 
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
134
 
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
135
 
203
- // Redact a freeform JSON-stringified argument blob (used by audit log).
204
- function redactArgsBlob(s) {
205
- return redactString(s);
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);
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
+
210
281
  ;// CONCATENATED MODULE: ./src/posture/agents-memory.js
211
282
  // AGENTS.md — writable continual-learning memory (harness-anatomy #2).
212
283
  //
@@ -508,6 +579,11 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
508
579
 
509
580
 
510
581
 
582
+ // Git-origin provenance (Finding Provenance M0/M1). Distinct from
583
+ // `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
584
+ // (Sigstore/SLSA attestation) — see report/index.js's import comment.
585
+
586
+
511
587
 
512
588
  // Lazy-loaded: these transitively pull in npm packages (@babel/core and
513
589
  // friends) that aren't available in the plugin-cache install path
@@ -520,14 +596,14 @@ async function getRunScan() {
520
596
  }
521
597
  let _verifyFixCore;
522
598
  async function getVerifyFixCore() {
523
- 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;
524
600
  return _verifyFixCore;
525
601
  }
526
602
 
527
603
  const MAX_FILES_PER_SCAN = 1024;
528
604
  const MAX_FILE_BYTES = 500_000;
529
605
  const MAX_TOTAL_SCAN_BYTES = 50_000_000;
530
- const META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
606
+ const tools_META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
531
607
 
532
608
  // OWASP A01 — refuse writes to paths that could subvert the security tool
533
609
  // itself or the host's source-control / dependency state. A forged finding
@@ -907,7 +983,7 @@ const scan_diff = {
907
983
  // scanning), left as-is rather than silently claimed fixed here.
908
984
  const findings = (0,report.normalizeFindings)(result.scan)
909
985
  .filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
910
- .map(f => redactFinding({
986
+ .map(f => (0,redact/* redactFinding */.lE)({
911
987
  id: f.id, severity: f.severity, file: f.file, line: f.line,
912
988
  title: f.vuln, cwe: f.cwe,
913
989
  description: f.description, remediation: f.remediation,
@@ -919,7 +995,7 @@ const scan_diff = {
919
995
  const off = _maybeOffload(sessionRoot, 'scan_diff', findings);
920
996
  if (off.offloaded) {
921
997
  return {
922
- _meta: META,
998
+ _meta: tools_META,
923
999
  scannedFiles: Object.keys(fileContents).length,
924
1000
  findingCount: off.total,
925
1001
  offloaded: true,
@@ -929,7 +1005,7 @@ const scan_diff = {
929
1005
  };
930
1006
  }
931
1007
  return {
932
- _meta: META,
1008
+ _meta: tools_META,
933
1009
  scannedFiles: Object.keys(fileContents).length,
934
1010
  findingCount: findings.length,
935
1011
  findings,
@@ -955,7 +1031,7 @@ const query_taint = {
955
1031
  async handler({ source, sink, limit, offset }, ctx) {
956
1032
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
957
1033
  if (!scan) {
958
- 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}).` };
959
1035
  }
960
1036
  const lim = Number.isInteger(limit) ? Math.min(50, Math.max(1, limit)) : 10;
961
1037
  const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
@@ -970,13 +1046,13 @@ const query_taint = {
970
1046
  const hay = [f.description, f.title, f.vuln, f.snippet, JSON.stringify(f.trace || '')].join(' ').toLowerCase();
971
1047
  return hay.includes(srcL) && hay.includes(sinkL);
972
1048
  });
973
- const page = all.slice(off, off + lim).map(f => redactFinding({
1049
+ const page = all.slice(off, off + lim).map(f => (0,redact/* redactFinding */.lE)({
974
1050
  id: f.id, severity: f.severity, file: f.file, line: f.line,
975
1051
  title: f.title || f.vuln, description: f.description,
976
1052
  trace: f.trace || null,
977
1053
  }));
978
1054
  return {
979
- _meta: META,
1055
+ _meta: tools_META,
980
1056
  hasResult: true,
981
1057
  integrity: status,
982
1058
  scanStartedAt: scan.startedAt || scan.meta?.startedAt || null,
@@ -1008,7 +1084,7 @@ const explain_finding = {
1008
1084
  if (!scan) throw new Error(`No usable scan state (${status}).`);
1009
1085
  const f = _findById(scan, finding_id);
1010
1086
  if (!f) throw new Error(`Finding not found: ${finding_id}`);
1011
- const redacted = redactFinding({
1087
+ const redacted = (0,redact/* redactFinding */.lE)({
1012
1088
  id: f.id, severity: f.severity, file: f.file, line: f.line,
1013
1089
  title: f.title || f.vuln, cwe: f.cwe,
1014
1090
  description: f.description, remediation: f.remediation,
@@ -1033,7 +1109,7 @@ const explain_finding = {
1033
1109
  }
1034
1110
  }
1035
1111
  return {
1036
- _meta: META,
1112
+ _meta: tools_META,
1037
1113
  ...redacted,
1038
1114
  trace: traceTrimmed,
1039
1115
  traceOffload: traceMeta,
@@ -1053,6 +1129,21 @@ const explain_finding = {
1053
1129
  epssScore: typeof f.epssScore === 'number' ? f.epssScore : null,
1054
1130
  epssPercentile: typeof f.epssPercentile === 'number' ? f.epssPercentile : null,
1055
1131
  exploitedNow: !!f.exploitedNow,
1132
+ // Which commit introduced this finding. `includeEmail` stays at its
1133
+ // DEFAULT (false) unconditionally — unlike the JSON report there is no
1134
+ // operator-set env escape for it here, because the consumer is an
1135
+ // agent that has no business receiving a committer's email address.
1136
+ // `pseudonymize`, by contrast, IS read back from the same env var
1137
+ // report/index.js's `_normalizedProvenance` reads
1138
+ // (AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1 / --pseudonymize-authors) —
1139
+ // fix-round item 4: an operator who set that policy was still getting
1140
+ // raw committer names (and, via providerEnrichment, raw reviewer
1141
+ // logins/CODEOWNERS lines) through this MCP surface because this call
1142
+ // passed no options object at all, silently defeating their policy at
1143
+ // this one output boundary while report/index.js honoured it.
1144
+ findingProvenance: f.findingProvenance ? (0,schema/* redactFindingProvenance */.As)(f.findingProvenance, {
1145
+ pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
1146
+ }) : null,
1056
1147
  };
1057
1148
  },
1058
1149
  };
@@ -1129,16 +1220,16 @@ const apply_fix = {
1129
1220
  },
1130
1221
  async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
1131
1222
  if (confirm !== true) {
1132
- return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
1223
+ return { _meta: tools_META, applied: false, reason: 'apply_fix requires confirm: true.' };
1133
1224
  }
1134
1225
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
1135
1226
  if (!scan) {
1136
- 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.` };
1137
1228
  }
1138
1229
  const f = _findById(scan, finding_id);
1139
- 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}` };
1140
1231
  if (f._shadow === true) {
1141
- 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' };
1142
1233
  }
1143
1234
 
1144
1235
  // #3 — verifier-approved patch path. When the caller supplies `patch` (a
@@ -1153,15 +1244,15 @@ const apply_fix = {
1153
1244
  // slip through — there is no token to replay, the verify runs here and now.
1154
1245
  if (patch && typeof patch === 'object' && Object.keys(patch).length) {
1155
1246
  if (!f.stableId) {
1156
- 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' };
1157
1248
  }
1158
1249
  const confinedAbs = {};
1159
1250
  for (const [rel, content] of Object.entries(patch)) {
1160
1251
  let abs;
1161
1252
  try { abs = _confine(ctx.sessionRoot, rel, 'patch key'); }
1162
- 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}` }; }
1163
1254
  if (_isReservedWritePath(ctx.sessionRoot, abs)) {
1164
- return { _meta: META, applied: false, reason: `reserved path refused: ${rel}` };
1255
+ return { _meta: tools_META, applied: false, reason: `reserved path refused: ${rel}` };
1165
1256
  }
1166
1257
  confinedAbs[rel] = { abs, content: String(content) };
1167
1258
  }
@@ -1174,7 +1265,7 @@ const apply_fix = {
1174
1265
  // suite as a fourth verification leg (scan + lint + tests). Opt-in
1175
1266
  // because many repos have no runner and we must not fail-closed by
1176
1267
  // default. Normalized to the scan+lint verdict shape used below.
1177
- 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));
1178
1269
  const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
1179
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 };
1180
1271
  } else {
@@ -1187,11 +1278,11 @@ const apply_fix = {
1187
1278
  });
1188
1279
  }
1189
1280
  } catch (e) {
1190
- return { _meta: META, applied: false, reason: `patch verification failed: ${e.message}` };
1281
+ return { _meta: tools_META, applied: false, reason: `patch verification failed: ${e.message}` };
1191
1282
  }
1192
1283
  if (!verdict.ok) {
1193
1284
  return {
1194
- _meta: META, applied: false,
1285
+ _meta: tools_META, applied: false,
1195
1286
  reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
1196
1287
  verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
1197
1288
  };
@@ -1214,7 +1305,7 @@ const apply_fix = {
1214
1305
  }
1215
1306
  const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
1216
1307
  if (dry_run) {
1217
- 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 };
1218
1309
  }
1219
1310
  if (materialClassification.highImpactCategories.length) {
1220
1311
  const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
@@ -1223,7 +1314,7 @@ const apply_fix = {
1223
1314
  typeof approval.reason === 'string' && approval.reason.trim().length > 0);
1224
1315
  if (!hasApprovalEvidence) {
1225
1316
  return {
1226
- _meta: META, applied: false,
1317
+ _meta: tools_META, applied: false,
1227
1318
  reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
1228
1319
  materialClassification,
1229
1320
  };
@@ -1233,7 +1324,7 @@ const apply_fix = {
1233
1324
  const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
1234
1325
  if (!identityCheck.verified) {
1235
1326
  return {
1236
- _meta: META, applied: false,
1327
+ _meta: tools_META, applied: false,
1237
1328
  reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
1238
1329
  materialClassification,
1239
1330
  };
@@ -1243,7 +1334,7 @@ const apply_fix = {
1243
1334
  const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
1244
1335
  if (!sodCheck.ok) {
1245
1336
  return {
1246
- _meta: META, applied: false,
1337
+ _meta: tools_META, applied: false,
1247
1338
  reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
1248
1339
  materialClassification,
1249
1340
  };
@@ -1257,6 +1348,7 @@ const apply_fix = {
1257
1348
  const entry = await (0,fix_history/* applyFix */.oM)({
1258
1349
  scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
1259
1350
  findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
1351
+ findingProvenance: f.findingProvenance || null,
1260
1352
  });
1261
1353
  written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
1262
1354
  }
@@ -1269,20 +1361,20 @@ const apply_fix = {
1269
1361
  try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
1270
1362
  }
1271
1363
  if (e && e.name === 'FixAttemptBudgetExceededError') {
1272
- 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 };
1273
1365
  }
1274
1366
  throw e;
1275
1367
  }
1276
1368
  let acceptance = null;
1277
1369
  try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
1278
- 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 };
1279
1371
  }
1280
1372
 
1281
1373
  if (typeof f.fix?.replacement !== 'string') {
1282
1374
  // Premortem #2: templates are patch-shaped text. Same reasoning as
1283
1375
  // the replacement path — do NOT pass through redactString here.
1284
1376
  return {
1285
- _meta: META, applied: false,
1377
+ _meta: tools_META, applied: false,
1286
1378
  reason: 'No full replacement available — only a template. Apply the template manually.',
1287
1379
  template: f.fix?.code || '',
1288
1380
  file: f.file, line: f.line,
@@ -1291,19 +1383,19 @@ const apply_fix = {
1291
1383
  let absFile;
1292
1384
  try { absFile = _confine(ctx.sessionRoot, f.file, 'finding.file'); }
1293
1385
  catch (e) {
1294
- return { _meta: META, applied: false, reason: `path-escape refused: ${e.message}` };
1386
+ return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` };
1295
1387
  }
1296
1388
  if (_isReservedWritePath(ctx.sessionRoot, absFile)) {
1297
- 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` };
1298
1390
  }
1299
1391
  if (!external_node_fs_.existsSync(absFile)) {
1300
- return { _meta: META, applied: false, reason: `File not found: ${absFile}` };
1392
+ return { _meta: tools_META, applied: false, reason: `File not found: ${absFile}` };
1301
1393
  }
1302
1394
  const originalContent = await promises_.readFile(absFile, 'utf8');
1303
1395
 
1304
1396
  if (dry_run) {
1305
1397
  return {
1306
- _meta: META,
1398
+ _meta: tools_META,
1307
1399
  applied: false, dryRun: true,
1308
1400
  file: f.file,
1309
1401
  originalSize: originalContent.length,
@@ -1325,7 +1417,7 @@ const apply_fix = {
1325
1417
  // size-diff shape) but the load-bearing addition is the verification
1326
1418
  // gate before the write.
1327
1419
  if (!f.stableId) {
1328
- 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' };
1329
1421
  }
1330
1422
  const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
1331
1423
  scanRoot: ctx.sessionRoot,
@@ -1335,9 +1427,9 @@ const apply_fix = {
1335
1427
  });
1336
1428
  if (!result.ok) {
1337
1429
  if (result.budgetExceeded) {
1338
- 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 };
1339
1431
  }
1340
- 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 };
1341
1433
  }
1342
1434
  // R25 (PRD §5): surface the running auto-fix acceptance rate after each
1343
1435
  // applied fix, so the closed loop reports its own success metric.
@@ -1348,7 +1440,7 @@ const apply_fix = {
1348
1440
  // FR-305: verifiedFull distinguishes "every required leg (lint, tests)
1349
1441
  // genuinely ran and passed" from "passed, but a required leg was
1350
1442
  // skipped or unavailable" — verified:true alone conflates them.
1351
- _meta: META, applied: true, verified: true, verifiedFull: result.verifiedFull,
1443
+ _meta: tools_META, applied: true, verified: true, verifiedFull: result.verifiedFull,
1352
1444
  historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
1353
1445
  integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
1354
1446
  verify: result.verify,
@@ -1417,7 +1509,7 @@ const verify_fix = {
1417
1509
  try {
1418
1510
  _confine(ctx.sessionRoot, relPath, 'files key');
1419
1511
  } catch (e) {
1420
- return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` };
1512
+ return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` };
1421
1513
  }
1422
1514
  confined[relPath] = String(content);
1423
1515
  }
@@ -1449,10 +1541,10 @@ const verify_fix = {
1449
1541
  fixMeta,
1450
1542
  });
1451
1543
  return {
1452
- _meta: META,
1544
+ _meta: tools_META,
1453
1545
  ok: r.ok,
1454
1546
  rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
1455
- 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) },
1456
1548
  // verifyFix computes five legs, not two — tests/honesty/poc were
1457
1549
  // being silently dropped here, leaving an agent with no structured
1458
1550
  // way to see WHY verification failed when the failure was in one
@@ -1463,11 +1555,11 @@ const verify_fix = {
1463
1555
  // it can echo proof-harness detail derived from scanned source.
1464
1556
  tests: r.tests,
1465
1557
  honesty: r.honesty,
1466
- 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,
1467
1559
  summary: r.summary,
1468
1560
  };
1469
1561
  } catch (e) {
1470
- return { _meta: META, ok: false, reason: `verify_fix failed: ${e.message}` };
1562
+ return { _meta: tools_META, ok: false, reason: `verify_fix failed: ${e.message}` };
1471
1563
  }
1472
1564
  },
1473
1565
  };
@@ -1490,11 +1582,11 @@ const synthesize_fix = {
1490
1582
  async handler({ finding_id }, ctx) {
1491
1583
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
1492
1584
  if (!scan) {
1493
- 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}` };
1494
1586
  }
1495
1587
  const f = _findById(scan, finding_id);
1496
- if (!f) return { _meta: META, ok: false, reason: `Finding not found: ${finding_id}` };
1497
- 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' };
1498
1590
  const fix = f.fix || {};
1499
1591
  const hasReplacement = typeof fix.replacement === 'string' && fix.replacement.length > 0;
1500
1592
  // Patch bounds: count files touched + LoC delta.
@@ -1530,7 +1622,7 @@ const synthesize_fix = {
1530
1622
  // verbatim. Snippet/description/etc. continue to be redacted in
1531
1623
  // explain_finding / scan_diff — that's the right surface for redaction.
1532
1624
  return {
1533
- _meta: META,
1625
+ _meta: tools_META,
1534
1626
  ok: true,
1535
1627
  stable_id: f.stableId || null,
1536
1628
  file: f.file, line: f.line,
@@ -1580,14 +1672,14 @@ const find_rule_module = {
1580
1672
  },
1581
1673
  async handler({ cwe, family }, ctx) {
1582
1674
  if (!cwe && !family) {
1583
- 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")' };
1584
1676
  }
1585
1677
  // Pattern enforcement — the mini-schema validator doesn't do `pattern`.
1586
1678
  if (cwe && !/^CWE-\d+$/.test(cwe)) {
1587
- 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")' };
1588
1680
  }
1589
1681
  if (family && !/^[a-z][a-z0-9-]+$/.test(family)) {
1590
- 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")' };
1591
1683
  }
1592
1684
  const sessionRoot = ctx.sessionRoot;
1593
1685
  const roots = [
@@ -1641,7 +1733,7 @@ const find_rule_module = {
1641
1733
  if (hits.length >= 20) break;
1642
1734
  }
1643
1735
  return {
1644
- _meta: META,
1736
+ _meta: tools_META,
1645
1737
  ok: true,
1646
1738
  query: { cwe: cwe || null, family: family || null },
1647
1739
  hitCount: hits.length,
@@ -1684,14 +1776,14 @@ const append_scratchpad = {
1684
1776
  },
1685
1777
  async handler({ path: relPath, content }, ctx) {
1686
1778
  const v = _validateScratchpadPath(relPath);
1687
- if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1779
+ if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
1688
1780
  let abs;
1689
1781
  try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1690
- 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}` }; }
1691
1783
  const total = _scratchpadTotalBytes(ctx.sessionRoot);
1692
1784
  if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
1693
1785
  return {
1694
- _meta: META, ok: false,
1786
+ _meta: tools_META, ok: false,
1695
1787
  reason: `scratchpad-total-exceeded: ${total} + ${content.length} > ${SCRATCHPAD_MAX_TOTAL_BYTES}. Clean up via "rm -rf .agentic-security/agent-scratchpad" or rotate sessions.`,
1696
1788
  };
1697
1789
  }
@@ -1699,7 +1791,7 @@ const append_scratchpad = {
1699
1791
  try { if (external_node_fs_.existsSync(abs)) existing = external_node_fs_.statSync(abs).size; } catch {}
1700
1792
  if (existing + content.length > SCRATCHPAD_MAX_FILE_BYTES) {
1701
1793
  return {
1702
- _meta: META, ok: false,
1794
+ _meta: tools_META, ok: false,
1703
1795
  reason: `scratchpad-file-exceeded: ${existing} + ${content.length} > ${SCRATCHPAD_MAX_FILE_BYTES}. Start a new file.`,
1704
1796
  };
1705
1797
  }
@@ -1707,12 +1799,12 @@ const append_scratchpad = {
1707
1799
  external_node_fs_.mkdirSync(external_node_path_.dirname(abs), { recursive: true });
1708
1800
  external_node_fs_.appendFileSync(abs, content);
1709
1801
  return {
1710
- _meta: META, ok: true,
1802
+ _meta: tools_META, ok: true,
1711
1803
  path: relPath, bytesWritten: content.length, fileSize: existing + content.length,
1712
1804
  scratchpadTotal: total + content.length,
1713
1805
  };
1714
1806
  } catch (e) {
1715
- return { _meta: META, ok: false, reason: `write-failed: ${e.message}` };
1807
+ return { _meta: tools_META, ok: false, reason: `write-failed: ${e.message}` };
1716
1808
  }
1717
1809
  },
1718
1810
  };
@@ -1732,14 +1824,14 @@ const read_scratchpad = {
1732
1824
  },
1733
1825
  async handler({ path: relPath, offset, limit }, ctx) {
1734
1826
  const v = _validateScratchpadPath(relPath);
1735
- if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1827
+ if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
1736
1828
  let abs;
1737
1829
  try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1738
- catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1739
- 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' };
1740
1832
  let stat;
1741
- try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: META, ok: false, reason: `stat-failed: ${e.message}` }; }
1742
- 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' };
1743
1835
  const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
1744
1836
  const lim = Number.isInteger(limit) ? Math.min(64 * 1024, Math.max(1, limit)) : 4096;
1745
1837
  let buf;
@@ -1749,10 +1841,10 @@ const read_scratchpad = {
1749
1841
  const read = external_node_fs_.readSync(fd, tmp, 0, lim, off);
1750
1842
  external_node_fs_.closeSync(fd);
1751
1843
  buf = tmp.slice(0, read);
1752
- } 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}` }; }
1753
1845
  const text = buf.toString('utf8');
1754
1846
  return {
1755
- _meta: META, ok: true,
1847
+ _meta: tools_META, ok: true,
1756
1848
  path: relPath,
1757
1849
  offset: off, limit: lim, bytesRead: buf.length,
1758
1850
  totalSize: stat.size,
@@ -1784,7 +1876,7 @@ const append_agents_memory = {
1784
1876
  },
1785
1877
  async handler({ agent, body }, ctx) {
1786
1878
  const r = appendAgentsMemory(ctx.sessionRoot, { agent, body });
1787
- return { _meta: META, ...r };
1879
+ return { _meta: tools_META, ...r };
1788
1880
  },
1789
1881
  };
1790
1882
 
@@ -1800,16 +1892,16 @@ const read_agents_memory = {
1800
1892
  },
1801
1893
  async handler({ full }, ctx) {
1802
1894
  const body = readAgentsMemory(ctx.sessionRoot);
1803
- if (!body) return { _meta: META, present: false };
1804
- 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 };
1805
1897
  // Tail-only — same logic as summarizeForSession but inlined to avoid a
1806
1898
  // second import surface.
1807
1899
  const limit = 6 * 1024;
1808
- 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 };
1809
1901
  const tail = body.slice(-limit);
1810
1902
  const firstSection = tail.indexOf('\n## ');
1811
1903
  const slice = firstSection >= 0 ? tail.slice(firstSection) : tail;
1812
- 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 };
1813
1905
  },
1814
1906
  };
1815
1907
 
@@ -1843,10 +1935,10 @@ const query_triage_memory = {
1843
1935
  // they landed in, rather than hardcoding a field allowlist that could
1844
1936
  // miss one.
1845
1937
  let results;
1846
- try { results = JSON.parse(redactString(JSON.stringify(raw))); }
1938
+ try { results = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
1847
1939
  catch { results = raw; }
1848
1940
  return {
1849
- _meta: META,
1941
+ _meta: tools_META,
1850
1942
  count: results.length,
1851
1943
  results,
1852
1944
  };
@@ -1871,7 +1963,7 @@ const query_findings_memory = {
1871
1963
  required: ['query'],
1872
1964
  },
1873
1965
  async handler({ query }, ctx) {
1874
- 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));
1875
1967
  const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
1876
1968
  // Stage 6 correctness audit — same redaction gap and same fix as
1877
1969
  // query_triage_memory just above: this mixes four differently-shaped
@@ -1879,9 +1971,9 @@ const query_findings_memory = {
1879
1971
  // whole-structure redactString round-trip is applied rather than a
1880
1972
  // per-field allowlist that could miss one of the four shapes.
1881
1973
  let body;
1882
- try { body = JSON.parse(redactString(JSON.stringify(raw))); }
1974
+ try { body = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
1883
1975
  catch { body = raw; }
1884
- return { _meta: META, ...body };
1976
+ return { _meta: tools_META, ...body };
1885
1977
  },
1886
1978
  };
1887
1979
 
@@ -1902,7 +1994,7 @@ const lookup_cve = {
1902
1994
  },
1903
1995
  async handler({ cve }, _ctx) {
1904
1996
  const r = lookupCve(cve);
1905
- return { _meta: META, ...r };
1997
+ return { _meta: tools_META, ...r };
1906
1998
  },
1907
1999
  };
1908
2000
 
@@ -1920,9 +2012,9 @@ const query_cache_telemetry = {
1920
2012
  },
1921
2013
  async handler({ transcript_path } = {}, ctx) {
1922
2014
  const result = (0,cache_economics.analyzeTranscript)({ transcriptPath: transcript_path, projectDir: ctx?.sessionRoot || process.cwd() });
1923
- if (!result.ok) return { _meta: META, ok: false, reason: result.reason };
2015
+ if (!result.ok) return { _meta: tools_META, ok: false, reason: result.reason };
1924
2016
  return {
1925
- _meta: META,
2017
+ _meta: tools_META,
1926
2018
  ok: true,
1927
2019
  metrics: result.metrics,
1928
2020
  leaks: result.leaks,
@@ -1938,7 +2030,7 @@ const query_cache_telemetry = {
1938
2030
  // ecosystem's native --dry-run command. Safe to call any number of times.
1939
2031
  let _scaUpgrade;
1940
2032
  async function _getScaUpgrade() {
1941
- 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));
1942
2034
  return _scaUpgrade;
1943
2035
  }
1944
2036
  const synthesize_sca_upgrade = {
@@ -1958,11 +2050,11 @@ const synthesize_sca_upgrade = {
1958
2050
  const f = _findById(scan, finding_id);
1959
2051
  if (!f) throw new Error(`Finding not found: ${finding_id}`);
1960
2052
  if (f.type !== 'vulnerable_dep') {
1961
- 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' };
1962
2054
  }
1963
2055
  const { planScaUpgrade } = await _getScaUpgrade();
1964
2056
  const plan = await planScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f });
1965
- return { _meta: META, ...plan };
2057
+ return { _meta: tools_META, ...plan };
1966
2058
  },
1967
2059
  };
1968
2060
 
@@ -1989,24 +2081,24 @@ const apply_sca_upgrade = {
1989
2081
  },
1990
2082
  async handler({ finding_id, confirm, run_tests = true }, ctx) {
1991
2083
  if (confirm !== true) {
1992
- 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.' };
1993
2085
  }
1994
2086
  const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
1995
2087
  if (!scan) {
1996
- 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.` };
1997
2089
  }
1998
2090
  const f = _findById(scan, finding_id);
1999
- 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}` };
2000
2092
  if (f.type !== 'vulnerable_dep') {
2001
- 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' };
2002
2094
  }
2003
2095
  const { applyScaUpgrade } = await _getScaUpgrade();
2004
2096
  const result = await applyScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f, runTests: run_tests });
2005
- return { _meta: META, ...result };
2097
+ return { _meta: tools_META, ...result };
2006
2098
  },
2007
2099
  };
2008
2100
 
2009
- 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];
2010
2102
 
2011
2103
  ;// CONCATENATED MODULE: ./src/mcp/validate.js
2012
2104
  // Minimal JSON Schema validator — just the subset our tool schemas use.
@@ -2104,7 +2196,7 @@ const SESSION_ID = `${process.pid}-${Date.now().toString(36).slice(-6)}`;
2104
2196
  function _summarize(args) {
2105
2197
  let s;
2106
2198
  try { s = JSON.stringify(args); } catch { s = '<unserializable>'; }
2107
- s = redactArgsBlob(s);
2199
+ s = (0,redact/* redactArgsBlob */.MC)(s);
2108
2200
  if (s.length > MAX_ARG_BYTES) s = s.slice(0, MAX_ARG_BYTES) + `…(+${s.length - MAX_ARG_BYTES})`;
2109
2201
  return s;
2110
2202
  }
@@ -2249,7 +2341,7 @@ const TOOLS_BY_NAME = Object.fromEntries(ALL_TOOLS.map(t => [t.name, t]));
2249
2341
  function _codeFingerprint() {
2250
2342
  try {
2251
2343
  const here = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
2252
- 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'];
2253
2345
  const h = external_node_crypto_.createHash('sha256');
2254
2346
  for (const f of files) {
2255
2347
  try { h.update(f); h.update(external_node_fs_.readFileSync(external_node_path_.join(here, f))); } catch {}
@@ -2737,6 +2829,307 @@ const _internal = {
2737
2829
  };
2738
2830
 
2739
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
+
2740
3133
  /***/ })
2741
3134
 
2742
3135
  };