trace-sec 0.0.0-stage → 2.1.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 (249) hide show
  1. package/README.md +210 -2
  2. package/bin/trace.js +112 -0
  3. package/package.json +44 -4
  4. package/pyproject.toml +39 -0
  5. package/src/trace_engine/__init__.py +8 -0
  6. package/src/trace_engine/__pycache__/__init__.cpython-311.pyc +0 -0
  7. package/src/trace_engine/__pycache__/cli.cpython-311.pyc +0 -0
  8. package/src/trace_engine/__pycache__/doctor.cpython-311.pyc +0 -0
  9. package/src/trace_engine/__pycache__/interactive.cpython-311.pyc +0 -0
  10. package/src/trace_engine/__pycache__/verify.cpython-311.pyc +0 -0
  11. package/src/trace_engine/ai/__init__.py +7 -0
  12. package/src/trace_engine/ai/__pycache__/__init__.cpython-311.pyc +0 -0
  13. package/src/trace_engine/ai/__pycache__/base.cpython-311.pyc +0 -0
  14. package/src/trace_engine/ai/__pycache__/ollama.cpython-311.pyc +0 -0
  15. package/src/trace_engine/ai/__pycache__/planner.cpython-311.pyc +0 -0
  16. package/src/trace_engine/ai/base.py +23 -0
  17. package/src/trace_engine/ai/ollama.py +50 -0
  18. package/src/trace_engine/ai/planner.py +40 -0
  19. package/src/trace_engine/apm/__init__.py +18 -0
  20. package/src/trace_engine/apm/__pycache__/__init__.cpython-311.pyc +0 -0
  21. package/src/trace_engine/apm/__pycache__/builder.cpython-311.pyc +0 -0
  22. package/src/trace_engine/apm/__pycache__/edges.cpython-311.pyc +0 -0
  23. package/src/trace_engine/apm/__pycache__/model.cpython-311.pyc +0 -0
  24. package/src/trace_engine/apm/__pycache__/nodes.cpython-311.pyc +0 -0
  25. package/src/trace_engine/apm/__pycache__/serialization.cpython-311.pyc +0 -0
  26. package/src/trace_engine/apm/builder.py +208 -0
  27. package/src/trace_engine/apm/edges.py +25 -0
  28. package/src/trace_engine/apm/model.py +107 -0
  29. package/src/trace_engine/apm/nodes.py +27 -0
  30. package/src/trace_engine/apm/serialization.py +105 -0
  31. package/src/trace_engine/benchmark/__init__.py +5 -0
  32. package/src/trace_engine/benchmark/__pycache__/__init__.cpython-311.pyc +0 -0
  33. package/src/trace_engine/benchmark/__pycache__/owasp.cpython-311.pyc +0 -0
  34. package/src/trace_engine/benchmark/owasp.py +183 -0
  35. package/src/trace_engine/cli.py +1184 -0
  36. package/src/trace_engine/config/__init__.py +35 -0
  37. package/src/trace_engine/config/__pycache__/__init__.cpython-311.pyc +0 -0
  38. package/src/trace_engine/config/__pycache__/defaults.cpython-311.pyc +0 -0
  39. package/src/trace_engine/config/__pycache__/loader.cpython-311.pyc +0 -0
  40. package/src/trace_engine/config/__pycache__/settings.cpython-311.pyc +0 -0
  41. package/src/trace_engine/config/defaults.py +48 -0
  42. package/src/trace_engine/config/loader.py +64 -0
  43. package/src/trace_engine/config/settings.py +72 -0
  44. package/src/trace_engine/doctor.py +250 -0
  45. package/src/trace_engine/findings/__init__.py +15 -0
  46. package/src/trace_engine/findings/__pycache__/__init__.cpython-311.pyc +0 -0
  47. package/src/trace_engine/findings/__pycache__/correlate.cpython-311.pyc +0 -0
  48. package/src/trace_engine/findings/__pycache__/model.cpython-311.pyc +0 -0
  49. package/src/trace_engine/findings/__pycache__/recommendations.cpython-311.pyc +0 -0
  50. package/src/trace_engine/findings/__pycache__/store.cpython-311.pyc +0 -0
  51. package/src/trace_engine/findings/correlate.py +103 -0
  52. package/src/trace_engine/findings/model.py +40 -0
  53. package/src/trace_engine/findings/recommendations.py +35 -0
  54. package/src/trace_engine/findings/store.py +39 -0
  55. package/src/trace_engine/framework/__init__.py +58 -0
  56. package/src/trace_engine/framework/__pycache__/__init__.cpython-311.pyc +0 -0
  57. package/src/trace_engine/framework/__pycache__/base.cpython-311.pyc +0 -0
  58. package/src/trace_engine/framework/__pycache__/csharp.cpython-311.pyc +0 -0
  59. package/src/trace_engine/framework/__pycache__/dart.cpython-311.pyc +0 -0
  60. package/src/trace_engine/framework/__pycache__/django.cpython-311.pyc +0 -0
  61. package/src/trace_engine/framework/__pycache__/express.cpython-311.pyc +0 -0
  62. package/src/trace_engine/framework/__pycache__/fastapi.cpython-311.pyc +0 -0
  63. package/src/trace_engine/framework/__pycache__/flask.cpython-311.pyc +0 -0
  64. package/src/trace_engine/framework/__pycache__/go.cpython-311.pyc +0 -0
  65. package/src/trace_engine/framework/__pycache__/nextjs.cpython-311.pyc +0 -0
  66. package/src/trace_engine/framework/__pycache__/php.cpython-311.pyc +0 -0
  67. package/src/trace_engine/framework/__pycache__/react_router.cpython-311.pyc +0 -0
  68. package/src/trace_engine/framework/__pycache__/ruby.cpython-311.pyc +0 -0
  69. package/src/trace_engine/framework/__pycache__/rust.cpython-311.pyc +0 -0
  70. package/src/trace_engine/framework/__pycache__/springboot.cpython-311.pyc +0 -0
  71. package/src/trace_engine/framework/base.py +49 -0
  72. package/src/trace_engine/framework/csharp.py +111 -0
  73. package/src/trace_engine/framework/dart.py +152 -0
  74. package/src/trace_engine/framework/django.py +188 -0
  75. package/src/trace_engine/framework/express.py +82 -0
  76. package/src/trace_engine/framework/fastapi.py +135 -0
  77. package/src/trace_engine/framework/flask.py +108 -0
  78. package/src/trace_engine/framework/go.py +94 -0
  79. package/src/trace_engine/framework/nextjs.py +200 -0
  80. package/src/trace_engine/framework/php.py +98 -0
  81. package/src/trace_engine/framework/react_router.py +331 -0
  82. package/src/trace_engine/framework/ruby.py +69 -0
  83. package/src/trace_engine/framework/rust.py +101 -0
  84. package/src/trace_engine/framework/springboot.py +145 -0
  85. package/src/trace_engine/harness/__init__.py +19 -0
  86. package/src/trace_engine/harness/__pycache__/__init__.cpython-311.pyc +0 -0
  87. package/src/trace_engine/harness/__pycache__/benchmark.cpython-311.pyc +0 -0
  88. package/src/trace_engine/harness/__pycache__/context.cpython-311.pyc +0 -0
  89. package/src/trace_engine/harness/__pycache__/engine.cpython-311.pyc +0 -0
  90. package/src/trace_engine/harness/__pycache__/patcher.cpython-311.pyc +0 -0
  91. package/src/trace_engine/harness/__pycache__/remediators.cpython-311.pyc +0 -0
  92. package/src/trace_engine/harness/benchmark.py +55 -0
  93. package/src/trace_engine/harness/context.py +49 -0
  94. package/src/trace_engine/harness/engine.py +187 -0
  95. package/src/trace_engine/harness/patcher.py +143 -0
  96. package/src/trace_engine/harness/remediators.py +307 -0
  97. package/src/trace_engine/ingest/__init__.py +15 -0
  98. package/src/trace_engine/ingest/__pycache__/__init__.cpython-311.pyc +0 -0
  99. package/src/trace_engine/ingest/__pycache__/files.cpython-311.pyc +0 -0
  100. package/src/trace_engine/ingest/__pycache__/hashing.cpython-311.pyc +0 -0
  101. package/src/trace_engine/ingest/__pycache__/ignore.cpython-311.pyc +0 -0
  102. package/src/trace_engine/ingest/__pycache__/repository.cpython-311.pyc +0 -0
  103. package/src/trace_engine/ingest/files.py +60 -0
  104. package/src/trace_engine/ingest/hashing.py +21 -0
  105. package/src/trace_engine/ingest/ignore.py +108 -0
  106. package/src/trace_engine/ingest/repository.py +50 -0
  107. package/src/trace_engine/intelligence/__init__.py +15 -0
  108. package/src/trace_engine/intelligence/__pycache__/__init__.cpython-311.pyc +0 -0
  109. package/src/trace_engine/intelligence/__pycache__/evaluation.cpython-311.pyc +0 -0
  110. package/src/trace_engine/intelligence/__pycache__/orchestrator.cpython-311.pyc +0 -0
  111. package/src/trace_engine/intelligence/__pycache__/tracebench.cpython-311.pyc +0 -0
  112. package/src/trace_engine/intelligence/evaluation.py +828 -0
  113. package/src/trace_engine/intelligence/laya/__init__.py +19 -0
  114. package/src/trace_engine/intelligence/laya/__pycache__/__init__.cpython-311.pyc +0 -0
  115. package/src/trace_engine/intelligence/laya/__pycache__/prompts.cpython-311.pyc +0 -0
  116. package/src/trace_engine/intelligence/laya/__pycache__/router.cpython-311.pyc +0 -0
  117. package/src/trace_engine/intelligence/laya/__pycache__/schemas.cpython-311.pyc +0 -0
  118. package/src/trace_engine/intelligence/laya/__pycache__/telemetry.cpython-311.pyc +0 -0
  119. package/src/trace_engine/intelligence/laya/__pycache__/thresholds.cpython-311.pyc +0 -0
  120. package/src/trace_engine/intelligence/laya/prompts.py +67 -0
  121. package/src/trace_engine/intelligence/laya/router.py +352 -0
  122. package/src/trace_engine/intelligence/laya/schemas.py +56 -0
  123. package/src/trace_engine/intelligence/laya/telemetry.py +48 -0
  124. package/src/trace_engine/intelligence/laya/thresholds.py +12 -0
  125. package/src/trace_engine/intelligence/orchestrator.py +130 -0
  126. package/src/trace_engine/intelligence/securebert/__init__.py +6 -0
  127. package/src/trace_engine/intelligence/securebert/__pycache__/__init__.cpython-311.pyc +0 -0
  128. package/src/trace_engine/intelligence/securebert/__pycache__/cache.cpython-311.pyc +0 -0
  129. package/src/trace_engine/intelligence/securebert/__pycache__/classifier.cpython-311.pyc +0 -0
  130. package/src/trace_engine/intelligence/securebert/cache.py +37 -0
  131. package/src/trace_engine/intelligence/securebert/classifier.py +240 -0
  132. package/src/trace_engine/intelligence/tracebench.py +61 -0
  133. package/src/trace_engine/intelligence/training/__init__.py +21 -0
  134. package/src/trace_engine/intelligence/training/__pycache__/__init__.cpython-311.pyc +0 -0
  135. package/src/trace_engine/intelligence/training/__pycache__/dataset.cpython-311.pyc +0 -0
  136. package/src/trace_engine/intelligence/training/__pycache__/dataset_importers.cpython-311.pyc +0 -0
  137. package/src/trace_engine/intelligence/training/__pycache__/laya_trainer.cpython-311.pyc +0 -0
  138. package/src/trace_engine/intelligence/training/__pycache__/lora_system2.cpython-311.pyc +0 -0
  139. package/src/trace_engine/intelligence/training/__pycache__/morefixes_pipeline.cpython-311.pyc +0 -0
  140. package/src/trace_engine/intelligence/training/__pycache__/slicer.cpython-311.pyc +0 -0
  141. package/src/trace_engine/intelligence/training/__pycache__/train_all.cpython-311.pyc +0 -0
  142. package/src/trace_engine/intelligence/training/__pycache__/trainer.cpython-311.pyc +0 -0
  143. package/src/trace_engine/intelligence/training/dataset.py +320 -0
  144. package/src/trace_engine/intelligence/training/dataset_importers.py +349 -0
  145. package/src/trace_engine/intelligence/training/laya_trainer.py +678 -0
  146. package/src/trace_engine/intelligence/training/lora_system2.py +162 -0
  147. package/src/trace_engine/intelligence/training/morefixes_pipeline.py +463 -0
  148. package/src/trace_engine/intelligence/training/slicer.py +129 -0
  149. package/src/trace_engine/intelligence/training/train_all.py +1009 -0
  150. package/src/trace_engine/intelligence/training/trainer.py +321 -0
  151. package/src/trace_engine/interactive.py +623 -0
  152. package/src/trace_engine/mcp/__init__.py +6 -0
  153. package/src/trace_engine/mcp/__pycache__/__init__.cpython-311.pyc +0 -0
  154. package/src/trace_engine/mcp/__pycache__/config.cpython-311.pyc +0 -0
  155. package/src/trace_engine/mcp/__pycache__/server.cpython-311.pyc +0 -0
  156. package/src/trace_engine/mcp/config.py +42 -0
  157. package/src/trace_engine/mcp/server.py +398 -0
  158. package/src/trace_engine/output/__init__.py +28 -0
  159. package/src/trace_engine/output/__pycache__/__init__.cpython-311.pyc +0 -0
  160. package/src/trace_engine/output/__pycache__/html.cpython-311.pyc +0 -0
  161. package/src/trace_engine/output/__pycache__/markdown.cpython-311.pyc +0 -0
  162. package/src/trace_engine/output/__pycache__/sarif.cpython-311.pyc +0 -0
  163. package/src/trace_engine/output/__pycache__/terminal.cpython-311.pyc +0 -0
  164. package/src/trace_engine/output/html.py +142 -0
  165. package/src/trace_engine/output/markdown.py +65 -0
  166. package/src/trace_engine/output/sarif.py +180 -0
  167. package/src/trace_engine/output/terminal.py +537 -0
  168. package/src/trace_engine/parsing/__init__.py +26 -0
  169. package/src/trace_engine/parsing/__pycache__/__init__.cpython-311.pyc +0 -0
  170. package/src/trace_engine/parsing/__pycache__/calls.cpython-311.pyc +0 -0
  171. package/src/trace_engine/parsing/__pycache__/language.cpython-311.pyc +0 -0
  172. package/src/trace_engine/parsing/__pycache__/locations.cpython-311.pyc +0 -0
  173. package/src/trace_engine/parsing/__pycache__/parser.cpython-311.pyc +0 -0
  174. package/src/trace_engine/parsing/__pycache__/symbols.cpython-311.pyc +0 -0
  175. package/src/trace_engine/parsing/calls.py +14 -0
  176. package/src/trace_engine/parsing/language.py +24 -0
  177. package/src/trace_engine/parsing/locations.py +17 -0
  178. package/src/trace_engine/parsing/parser.py +465 -0
  179. package/src/trace_engine/parsing/symbols.py +45 -0
  180. package/src/trace_engine/plugin/__init__.py +157 -0
  181. package/src/trace_engine/plugin/__pycache__/__init__.cpython-311.pyc +0 -0
  182. package/src/trace_engine/plugin/__pycache__/evaluator.cpython-311.pyc +0 -0
  183. package/src/trace_engine/plugin/__pycache__/swebench_adapter.cpython-311.pyc +0 -0
  184. package/src/trace_engine/plugin/__pycache__/task.cpython-311.pyc +0 -0
  185. package/src/trace_engine/plugin/bundle/hooks.json +24 -0
  186. package/src/trace_engine/plugin/bundle/mcp_config.json +11 -0
  187. package/src/trace_engine/plugin/bundle/plugin.json +20 -0
  188. package/src/trace_engine/plugin/bundle/rules/security_remediation.md +40 -0
  189. package/src/trace_engine/plugin/bundle/skills/trace-security-harness/SKILL.md +118 -0
  190. package/src/trace_engine/plugin/evaluator.py +105 -0
  191. package/src/trace_engine/plugin/swebench_adapter.py +96 -0
  192. package/src/trace_engine/plugin/task.py +48 -0
  193. package/src/trace_engine/policy/__init__.py +5 -0
  194. package/src/trace_engine/policy/__pycache__/__init__.cpython-311.pyc +0 -0
  195. package/src/trace_engine/policy/__pycache__/scope.cpython-311.pyc +0 -0
  196. package/src/trace_engine/policy/scope.py +80 -0
  197. package/src/trace_engine/runtime/__init__.py +7 -0
  198. package/src/trace_engine/runtime/__pycache__/__init__.cpython-311.pyc +0 -0
  199. package/src/trace_engine/runtime/__pycache__/client.cpython-311.pyc +0 -0
  200. package/src/trace_engine/runtime/__pycache__/observations.cpython-311.pyc +0 -0
  201. package/src/trace_engine/runtime/__pycache__/target.cpython-311.pyc +0 -0
  202. package/src/trace_engine/runtime/client.py +81 -0
  203. package/src/trace_engine/runtime/observations.py +18 -0
  204. package/src/trace_engine/runtime/target.py +23 -0
  205. package/src/trace_engine/security/__init__.py +16 -0
  206. package/src/trace_engine/security/__pycache__/__init__.cpython-311.pyc +0 -0
  207. package/src/trace_engine/security/__pycache__/fusion.cpython-311.pyc +0 -0
  208. package/src/trace_engine/security/__pycache__/hypotheses.cpython-311.pyc +0 -0
  209. package/src/trace_engine/security/__pycache__/signals.cpython-311.pyc +0 -0
  210. package/src/trace_engine/security/__pycache__/timing.cpython-311.pyc +0 -0
  211. package/src/trace_engine/security/fusion.py +126 -0
  212. package/src/trace_engine/security/hypotheses.py +251 -0
  213. package/src/trace_engine/security/signals.py +28 -0
  214. package/src/trace_engine/security/timing.py +132 -0
  215. package/src/trace_engine/testpacks/__init__.py +18 -0
  216. package/src/trace_engine/testpacks/__pycache__/__init__.cpython-311.pyc +0 -0
  217. package/src/trace_engine/testpacks/__pycache__/authentication.cpython-311.pyc +0 -0
  218. package/src/trace_engine/testpacks/__pycache__/base.cpython-311.pyc +0 -0
  219. package/src/trace_engine/testpacks/__pycache__/bfla.cpython-311.pyc +0 -0
  220. package/src/trace_engine/testpacks/__pycache__/bola.cpython-311.pyc +0 -0
  221. package/src/trace_engine/testpacks/__pycache__/cors.cpython-311.pyc +0 -0
  222. package/src/trace_engine/testpacks/__pycache__/deserialization.cpython-311.pyc +0 -0
  223. package/src/trace_engine/testpacks/__pycache__/injection.cpython-311.pyc +0 -0
  224. package/src/trace_engine/testpacks/__pycache__/mass_assignment.cpython-311.pyc +0 -0
  225. package/src/trace_engine/testpacks/__pycache__/path_traversal.cpython-311.pyc +0 -0
  226. package/src/trace_engine/testpacks/__pycache__/registry.cpython-311.pyc +0 -0
  227. package/src/trace_engine/testpacks/__pycache__/ssrf.cpython-311.pyc +0 -0
  228. package/src/trace_engine/testpacks/__pycache__/ssti.cpython-311.pyc +0 -0
  229. package/src/trace_engine/testpacks/authentication.py +60 -0
  230. package/src/trace_engine/testpacks/base.py +63 -0
  231. package/src/trace_engine/testpacks/bfla.py +70 -0
  232. package/src/trace_engine/testpacks/bola.py +94 -0
  233. package/src/trace_engine/testpacks/cors.py +85 -0
  234. package/src/trace_engine/testpacks/deserialization.py +86 -0
  235. package/src/trace_engine/testpacks/injection.py +179 -0
  236. package/src/trace_engine/testpacks/mass_assignment.py +70 -0
  237. package/src/trace_engine/testpacks/path_traversal.py +117 -0
  238. package/src/trace_engine/testpacks/registry.py +44 -0
  239. package/src/trace_engine/testpacks/ssrf.py +85 -0
  240. package/src/trace_engine/testpacks/ssti.py +96 -0
  241. package/src/trace_engine/tools/__init__.py +6 -0
  242. package/src/trace_engine/tools/__pycache__/__init__.cpython-311.pyc +0 -0
  243. package/src/trace_engine/tools/__pycache__/adapters.cpython-311.pyc +0 -0
  244. package/src/trace_engine/tools/__pycache__/base.cpython-311.pyc +0 -0
  245. package/src/trace_engine/tools/__pycache__/registry.cpython-311.pyc +0 -0
  246. package/src/trace_engine/tools/adapters.py +111 -0
  247. package/src/trace_engine/tools/base.py +68 -0
  248. package/src/trace_engine/tools/registry.py +36 -0
  249. package/src/trace_engine/verify.py +156 -0
@@ -0,0 +1,39 @@
1
+ """Finding store for saving and loading correlated security results."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+ from trace_engine.findings.model import Finding
7
+
8
+
9
+ class FindingStore:
10
+ """Manages persistence of correlated findings in the .trace directory."""
11
+
12
+ def __init__(self, trace_dir: Path):
13
+ self.trace_dir = trace_dir
14
+ self.store_file = trace_dir / "findings.json"
15
+
16
+ def save_findings(self, findings: List[Finding]) -> None:
17
+ """Save a list of findings to JSON."""
18
+ self.trace_dir.mkdir(parents=True, exist_ok=True)
19
+ data = [f.model_dump() for f in findings]
20
+ with open(self.store_file, "w", encoding="utf-8") as f:
21
+ json.dump(data, f, indent=2)
22
+
23
+ def load_findings(self) -> List[Finding]:
24
+ """Load findings from JSON file."""
25
+ if not self.store_file.exists():
26
+ return []
27
+ try:
28
+ with open(self.store_file, "r", encoding="utf-8") as f:
29
+ data = json.load(f)
30
+ return [Finding.model_validate(item) for item in data]
31
+ except Exception:
32
+ return []
33
+
34
+ def get_finding_by_id(self, finding_id: str) -> Optional[Finding]:
35
+ """Retrieve finding by ID."""
36
+ for f in self.load_findings():
37
+ if f.id.lower() == finding_id.lower():
38
+ return f
39
+ return None
@@ -0,0 +1,58 @@
1
+ """Framework adapters for web frameworks and API route extraction."""
2
+
3
+ from typing import List
4
+ from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
5
+ from trace_engine.framework.fastapi import FastAPIFrameworkAdapter
6
+ from trace_engine.framework.flask import FlaskFrameworkAdapter
7
+ from trace_engine.framework.django import DjangoFrameworkAdapter
8
+ from trace_engine.framework.express import ExpressFrameworkAdapter
9
+ from trace_engine.framework.react_router import ReactRouterFrameworkAdapter
10
+ from trace_engine.framework.nextjs import NextJSFrameworkAdapter
11
+ from trace_engine.framework.dart import DartFrameworkAdapter
12
+ from trace_engine.framework.springboot import SpringBootFrameworkAdapter
13
+ from trace_engine.framework.go import GoFrameworkAdapter
14
+ from trace_engine.framework.php import PHPFrameworkAdapter
15
+ from trace_engine.framework.ruby import RubyFrameworkAdapter
16
+ from trace_engine.framework.rust import RustFrameworkAdapter
17
+ from trace_engine.framework.csharp import CSharpFrameworkAdapter
18
+
19
+ _ADAPTERS: List[FrameworkAdapter] = [
20
+ FastAPIFrameworkAdapter(),
21
+ FlaskFrameworkAdapter(),
22
+ DjangoFrameworkAdapter(),
23
+ ExpressFrameworkAdapter(),
24
+ ReactRouterFrameworkAdapter(),
25
+ NextJSFrameworkAdapter(),
26
+ DartFrameworkAdapter(),
27
+ SpringBootFrameworkAdapter(),
28
+ GoFrameworkAdapter(),
29
+ PHPFrameworkAdapter(),
30
+ RubyFrameworkAdapter(),
31
+ RustFrameworkAdapter(),
32
+ CSharpFrameworkAdapter(),
33
+ ]
34
+
35
+
36
+ def get_adapters() -> List[FrameworkAdapter]:
37
+ return _ADAPTERS
38
+
39
+
40
+ __all__ = [
41
+ "FrameworkAdapter",
42
+ "Endpoint",
43
+ "EndpointParameter",
44
+ "FastAPIFrameworkAdapter",
45
+ "FlaskFrameworkAdapter",
46
+ "DjangoFrameworkAdapter",
47
+ "ExpressFrameworkAdapter",
48
+ "ReactRouterFrameworkAdapter",
49
+ "NextJSFrameworkAdapter",
50
+ "DartFrameworkAdapter",
51
+ "SpringBootFrameworkAdapter",
52
+ "GoFrameworkAdapter",
53
+ "PHPFrameworkAdapter",
54
+ "RubyFrameworkAdapter",
55
+ "RustFrameworkAdapter",
56
+ "CSharpFrameworkAdapter",
57
+ "get_adapters",
58
+ ]
@@ -0,0 +1,49 @@
1
+ """Normalized API endpoint models and framework adapter base class."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Optional, Dict, Any
5
+ from pydantic import BaseModel, Field
6
+ from trace_engine.parsing.locations import SourceLocation
7
+ from trace_engine.parsing.parser import ParsedFile
8
+
9
+
10
+ class EndpointParameter(BaseModel):
11
+ name: str
12
+ location: str = "query" # "path", "query", "body", "header"
13
+ param_type: Optional[str] = "string"
14
+ required: bool = True
15
+ user_controlled: bool = True
16
+
17
+
18
+ class Endpoint(BaseModel):
19
+ """Normalized API endpoint representation according to TRACE specification."""
20
+ id: str
21
+ method: str
22
+ path: str
23
+ handler_name: str
24
+ auth_required: bool = False
25
+ roles: List[str] = Field(default_factory=list)
26
+ parameters: List[EndpointParameter] = Field(default_factory=list)
27
+ database_access: bool = False
28
+ object_identifier: bool = False
29
+ state_changing: bool = False
30
+ external_network: bool = False
31
+ sensitive_data: bool = False
32
+ source: SourceLocation
33
+
34
+ def display_name(self) -> str:
35
+ return f"{self.method.upper()} {self.path}"
36
+
37
+
38
+ class FrameworkAdapter(ABC):
39
+ """Abstract adapter for extracting endpoints from parsed ASTs."""
40
+
41
+ @abstractmethod
42
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
43
+ """Determines if this adapter applies to the given file."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
48
+ """Extracts normalized endpoints from the file."""
49
+ pass
@@ -0,0 +1,111 @@
1
+ """C# and ASP.NET Core framework adapter for controller and minimal API route extraction."""
2
+
3
+ import re
4
+ from typing import List, Optional
5
+ from trace_engine.parsing.parser import ParsedFile
6
+ from trace_engine.parsing.locations import SourceLocation
7
+ from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
8
+
9
+
10
+ class CSharpFrameworkAdapter(FrameworkAdapter):
11
+ """Detects and extracts C# ASP.NET Core endpoints, controller actions, and minimal APIs."""
12
+
13
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
14
+ if parsed_file.language != "csharp":
15
+ norm = parsed_file.file_path.replace("\\", "/").lower()
16
+ return norm.endswith(".cs")
17
+ return True
18
+
19
+ def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
20
+ endpoints: List[Endpoint] = []
21
+ norm_file = parsed_file.file_path.replace("\\", "/")
22
+ content_lower = content.lower()
23
+
24
+ has_db_sink = any(
25
+ term in content_lower
26
+ for term in ("fromsqlraw", "executesqlraw", "dbcontext", "_context.", "sqlconnection", "dapper", "queryasync")
27
+ )
28
+ has_ext_sink = any(
29
+ term in content_lower
30
+ for term in ("httpclient", "getasync", "postasync", "restsharp")
31
+ )
32
+
33
+ # 1. Controller HTTP action attributes: [HttpGet("api/v1/users/{id}")] or [HttpPost("orders")]
34
+ attr_pattern = re.compile(
35
+ r"""\[Http(Get|Post|Put|Delete|Patch)\s*(?:\(\s*["']([^"']*)["']\s*\))?\]\s*(?:\[[^\]]+\]\s*)*public\s+(?:async\s+)?(?:Task<[a-zA-Z0-9_<>]+>|[a-zA-Z0-9_<>]+)\s+([a-zA-Z0-9_]+)""",
36
+ re.MULTILINE
37
+ )
38
+
39
+ # Extract base route from [Route("api/[controller]")] if present
40
+ base_route_match = re.search(r"""\[Route\s*\(\s*["']([^"']+)["']\s*\)\]""", content)
41
+ base_route = base_route_match.group(1).rstrip("/") if base_route_match else ""
42
+
43
+ for match in attr_pattern.finditer(content):
44
+ method = match.group(1).upper()
45
+ action_route = match.group(2) or ""
46
+ handler = match.group(3)
47
+ line_no = content[:match.start()].count("\n") + 1
48
+
49
+ if action_route.startswith("/"):
50
+ raw_path = action_route
51
+ elif base_route:
52
+ raw_path = f"/{base_route}/{action_route}".rstrip("/")
53
+ else:
54
+ raw_path = f"/{action_route}" if action_route else f"/{handler}"
55
+
56
+ raw_path = "/" + raw_path.lstrip("/")
57
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', raw_path)
58
+
59
+ ep_id = f"ep_cs_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
60
+ endpoints.append(
61
+ Endpoint(
62
+ id=ep_id,
63
+ method=method,
64
+ path=raw_path,
65
+ handler_name=handler,
66
+ auth_required="[authorize]" in content_lower or "authorize" in content_lower,
67
+ roles=["admin"] if "admin" in raw_path.lower() else [],
68
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
69
+ database_access=has_db_sink,
70
+ object_identifier=bool(path_params),
71
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
72
+ external_network=has_ext_sink,
73
+ sensitive_data="admin" in raw_path.lower() or "user" in raw_path.lower(),
74
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
75
+ )
76
+ )
77
+
78
+ # 2. Minimal APIs: app.MapGet("/api/v1/users/{id}", ...)
79
+ map_pattern = re.compile(
80
+ r"""app\.Map(Get|Post|Put|Delete)\s*\(\s*["']([^"']+)["']\s*,\s*([a-zA-Z0-9_\.]+|async|\()""",
81
+ re.IGNORECASE,
82
+ )
83
+ for match in map_pattern.finditer(content):
84
+ method = match.group(1).upper()
85
+ raw_path = match.group(2).strip()
86
+ handler = "minimal_api_handler"
87
+ line_no = content[:match.start()].count("\n") + 1
88
+
89
+ norm_path = raw_path if raw_path.startswith("/") else f"/{raw_path}"
90
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
91
+
92
+ ep_id = f"ep_cs_map_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
93
+ endpoints.append(
94
+ Endpoint(
95
+ id=ep_id,
96
+ method=method,
97
+ path=norm_path,
98
+ handler_name=handler,
99
+ auth_required="requireauthorization" in content_lower,
100
+ roles=["admin"] if "admin" in norm_path.lower() else [],
101
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
102
+ database_access=has_db_sink,
103
+ object_identifier=bool(path_params),
104
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
105
+ external_network=has_ext_sink,
106
+ sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
107
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
108
+ )
109
+ )
110
+
111
+ return endpoints
@@ -0,0 +1,152 @@
1
+ """Dart framework adapter for Shelf, Dart Frog, Angel, and Flutter API routes."""
2
+
3
+ import re
4
+ from typing import List, Dict, Optional
5
+ from trace_engine.parsing.parser import ParsedFile
6
+ from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
7
+ from trace_engine.parsing.locations import SourceLocation
8
+
9
+
10
+ class DartFrameworkAdapter(FrameworkAdapter):
11
+ """Detects and extracts Dart API endpoints, Shelf routes, and Dart Frog handlers."""
12
+
13
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
14
+ if parsed_file.language != "dart":
15
+ return False
16
+
17
+ norm_path = parsed_file.file_path.replace("\\", "/").lower()
18
+
19
+ # If in routes, controllers, or api directory
20
+ if any(f"/{d}/" in norm_path or norm_path.startswith(f"{d}/") for d in ("routes", "controllers", "api", "endpoints")):
21
+ return True
22
+
23
+ # Check imports
24
+ for imp in parsed_file.imports:
25
+ mod_low = imp.module.lower()
26
+ if any(term in mod_low for term in ("shelf", "dart_frog", "angel", "conduit")):
27
+ return True
28
+
29
+ return False
30
+
31
+ def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
32
+ endpoints: List[Endpoint] = []
33
+ norm_file_path = parsed_file.file_path.replace("\\", "/")
34
+ content_lower = content.lower()
35
+
36
+ # Database sinks
37
+ has_db_sink = any(
38
+ term in content_lower
39
+ for term in (
40
+ "postgres", "mysql", "sqlite", "drift", "sembast", "mongo",
41
+ "supabase", "db.query", "db.execute", "findbyid", "findone",
42
+ "repository", "table("
43
+ )
44
+ )
45
+
46
+ # External network sinks
47
+ has_ext_sink = any(
48
+ term in content_lower
49
+ for term in (
50
+ "http.get", "http.post", "http.put", "http.delete",
51
+ "client.get", "client.post", "dio.get", "dio.post"
52
+ )
53
+ )
54
+
55
+ # Authentication detection
56
+ auth_signals = (
57
+ "requireauth", "verifytoken", "jwt", "bearer", "authmiddleware",
58
+ "isauthenticated", "currentuser", "admin", "hasrole"
59
+ )
60
+ auth_required = any(sig in content_lower for sig in auth_signals) or "admin" in norm_file_path.lower()
61
+ roles = ["admin"] if "admin" in content_lower or "admin" in norm_file_path.lower() else []
62
+
63
+ # 1. Shelf Router: router.get('/path', _handler) or router.get('/path', (req) => ...)
64
+ shelf_pattern = re.compile(
65
+ r"""(?:router|app)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]\s*,\s*([a-zA-Z0-9_$]+|\([^\)]*\))""",
66
+ re.IGNORECASE
67
+ )
68
+ for match in shelf_pattern.finditer(content):
69
+ method = match.group(1).upper()
70
+ raw_path = match.group(2)
71
+ raw_handler = match.group(3)
72
+ handler_name = raw_handler if not raw_handler.startswith("(") else "closure_handler"
73
+ line_no = content[:match.start()].count("\n") + 1
74
+
75
+ # Convert Shelf <param> and :param to {param}
76
+ norm_path = re.sub(r'<([a-zA-Z0-9_]+)>', r'{\1}', raw_path)
77
+ norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', norm_path)
78
+ norm_path = "/" + norm_path.lstrip("/")
79
+
80
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
81
+ ep_id = f"ep_dart_{norm_file_path}_{line_no}_{method}".replace("/", "_").replace(".", "_")
82
+
83
+ sensitive = any(
84
+ term in norm_path.lower() or term in content_lower
85
+ for term in ("admin", "user", "auth", "billing", "payment", "order", "token", "secret", "webhook")
86
+ )
87
+
88
+ endpoints.append(
89
+ Endpoint(
90
+ id=ep_id,
91
+ method=method,
92
+ path=norm_path,
93
+ handler_name=handler_name,
94
+ auth_required=auth_required or ("admin" in norm_path.lower()),
95
+ roles=roles if "admin" not in norm_path.lower() else ["admin"],
96
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
97
+ database_access=has_db_sink,
98
+ object_identifier=bool(path_params),
99
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
100
+ external_network=has_ext_sink,
101
+ sensitive_data=sensitive,
102
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
103
+ )
104
+ )
105
+
106
+ # 2. Dart Frog: routes/api/v1/projects/[id].dart
107
+ if not endpoints and "routes/" in norm_file_path:
108
+ clean = norm_file_path
109
+ idx = clean.find("routes/")
110
+ clean = clean[idx + 7:]
111
+ clean = re.sub(r'\.dart$', '', clean)
112
+ clean = re.sub(r'\[([a-zA-Z0-9_]+)\]', r'{\1}', clean)
113
+ clean = re.sub(r'(?:^|/)index$', '', clean)
114
+ norm_path = "/" + clean.lstrip("/")
115
+ if not norm_path:
116
+ norm_path = "/"
117
+
118
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
119
+ sensitive = any(
120
+ term in norm_path.lower() or term in content_lower
121
+ for term in ("admin", "user", "auth", "billing", "payment", "token", "webhook")
122
+ )
123
+
124
+ # Look for onRequest or method checks
125
+ methods = []
126
+ for m in re.finditer(r'''HttpMethod\.(get|post|put|delete)''', content, re.IGNORECASE):
127
+ methods.append(m.group(1).upper())
128
+
129
+ if not methods:
130
+ methods = ["GET", "POST"]
131
+
132
+ for meth in set(methods):
133
+ ep_id = f"ep_dart_frog_{norm_file_path}_{meth}".replace("/", "_").replace(".", "_")
134
+ endpoints.append(
135
+ Endpoint(
136
+ id=ep_id,
137
+ method=meth,
138
+ path=norm_path,
139
+ handler_name="onRequest",
140
+ auth_required=auth_required or ("admin" in norm_path.lower()),
141
+ roles=roles if "admin" not in norm_path.lower() else ["admin"],
142
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
143
+ database_access=has_db_sink,
144
+ object_identifier=bool(path_params),
145
+ state_changing=meth in ("POST", "PUT", "DELETE", "PATCH"),
146
+ external_network=has_ext_sink,
147
+ sensitive_data=sensitive,
148
+ source=SourceLocation(file=parsed_file.file_path, line_start=1, line_end=1),
149
+ )
150
+ )
151
+
152
+ return endpoints
@@ -0,0 +1,188 @@
1
+ """Django and Django REST Framework adapter for endpoint and route discovery."""
2
+
3
+ import re
4
+ from typing import List, Set, Optional
5
+ from trace_engine.parsing.parser import ParsedFile
6
+ from trace_engine.parsing.locations import SourceLocation
7
+ from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
8
+
9
+
10
+ class DjangoFrameworkAdapter(FrameworkAdapter):
11
+ """Detects and extracts Django and Django REST Framework (DRF) endpoints, URL routing, and security decorators."""
12
+
13
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
14
+ if parsed_file.language != "python":
15
+ return False
16
+ # Check imports for Django or DRF
17
+ for imp in parsed_file.imports:
18
+ mod = imp.module.lower()
19
+ if "django" in mod or "rest_framework" in mod:
20
+ return True
21
+ # Check file naming conventions or patterns
22
+ if "urls.py" in parsed_file.file_path.lower() or "views.py" in parsed_file.file_path.lower():
23
+ return True
24
+ return False
25
+
26
+ def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
27
+ endpoints: List[Endpoint] = []
28
+
29
+ # 1. Parse urls.py patterns: path('api/v1/orders/<int:order_id>/', views.get_order)
30
+ endpoints.extend(self._extract_from_urlpatterns(parsed_file, content))
31
+
32
+ # 2. Parse views.py: Class-Based Views (APIView, View, ViewSet) and function views (@api_view)
33
+ endpoints.extend(self._extract_from_views(parsed_file, content))
34
+
35
+ return endpoints
36
+
37
+ def _extract_from_urlpatterns(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
38
+ endpoints: List[Endpoint] = []
39
+ if "urlpatterns" not in content and "path(" not in content and "re_path(" not in content:
40
+ return endpoints
41
+
42
+ # Pattern: path('orders/<int:order_id>/', views.order_detail, name='order-detail')
43
+ # Pattern: re_path(r'^api/v1/export/?$', ExportView.as_view())
44
+ path_pattern = re.compile(
45
+ r"""(?:path|re_path)\s*\(\s*r?["']([^"']*)["']\s*,\s*([a-zA-Z0-9_\.]+)(?:\.as_view\s*\(\s*\))?""",
46
+ re.MULTILINE,
47
+ )
48
+
49
+ for match in path_pattern.finditer(content):
50
+ raw_path = match.group(1).strip()
51
+ handler_name = match.group(2).strip()
52
+
53
+ # Normalize Django path converters: <int:id>, <str:slug>, <uuid:pk> -> {id}, {slug}, {pk}
54
+ path_params = re.findall(r"<(?:[a-zA-Z0-9_]+:)?([a-zA-Z0-9_]+)>", raw_path)
55
+ clean_path = re.sub(r"<(?:[a-zA-Z0-9_]+:)?([a-zA-Z0-9_]+)>", r"{\1}", raw_path)
56
+ # Remove regex anchor symbols
57
+ clean_path = re.sub(r"[\^\$\?]+", "", clean_path).rstrip("/")
58
+ norm_path = clean_path if clean_path.startswith("/") else f"/{clean_path}"
59
+
60
+ # Infer methods: standard Django views typically handle GET or POST
61
+ methods = ["GET"]
62
+ if any(term in handler_name.lower() for term in ("create", "delete", "update", "post", "mutation", "reset", "patch")):
63
+ methods = ["POST"]
64
+
65
+ auth_required = any(
66
+ term in content.lower()
67
+ for term in ("login_required", "isauthenticated", "permission_classes", "tokenauthentication")
68
+ )
69
+ roles = ["admin"] if "admin" in handler_name.lower() or "admin" in norm_path.lower() else []
70
+
71
+ line_no = content[: match.start()].count("\n") + 1
72
+ src_loc = SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no, column_start=0, column_end=0)
73
+
74
+ for m in methods:
75
+ ep_id = f"ep_django_{parsed_file.file_path}_{handler_name}_{m}".replace("/", "_").replace(".", "_")
76
+ endpoints.append(
77
+ Endpoint(
78
+ id=ep_id,
79
+ method=m,
80
+ path=norm_path,
81
+ handler_name=handler_name,
82
+ auth_required=auth_required,
83
+ roles=roles,
84
+ parameters=[
85
+ EndpointParameter(name=p, location="path", required=True)
86
+ for p in path_params
87
+ ],
88
+ database_access=True, # Django views almost universally access ORM models
89
+ object_identifier=bool(path_params),
90
+ state_changing=m in ("POST", "PUT", "DELETE", "PATCH"),
91
+ external_network=False,
92
+ sensitive_data="admin" in norm_path.lower() or "auth" in norm_path.lower() or "user" in norm_path.lower(),
93
+ source=src_loc,
94
+ )
95
+ )
96
+
97
+ return endpoints
98
+
99
+ def _extract_from_views(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
100
+ endpoints: List[Endpoint] = []
101
+ content_lines = content.splitlines()
102
+
103
+ # 1. DRF @api_view(['GET', 'POST'])
104
+ for fn in parsed_file.functions:
105
+ fn_methods = []
106
+ for dec in fn.decorators:
107
+ dec_text = dec.raw_text.strip()
108
+ api_match = re.search(r"""@?api_view\s*\(\s*\[([^\]]+)\]""", dec_text, re.IGNORECASE)
109
+ if api_match:
110
+ fn_methods = [
111
+ m.strip().strip("'\"").upper()
112
+ for m in api_match.group(1).split(",")
113
+ if m.strip()
114
+ ]
115
+
116
+ if fn_methods:
117
+ fn_body = fn.body_text or ""
118
+ auth_required = any(
119
+ term in fn_body.lower() or term in "".join(d.raw_text.lower() for d in fn.decorators)
120
+ for term in ("isauthenticated", "isadminuser", "login_required", "permission_classes")
121
+ )
122
+ roles = ["admin"] if "admin" in fn.name.lower() else []
123
+ for m in fn_methods:
124
+ ep_id = f"ep_drf_{parsed_file.file_path}_{fn.name}_{m}".replace("/", "_").replace(".", "_")
125
+ endpoints.append(
126
+ Endpoint(
127
+ id=ep_id,
128
+ method=m,
129
+ path=f"/api/{fn.name.replace('_', '-')}",
130
+ handler_name=fn.qualified_name,
131
+ auth_required=auth_required,
132
+ roles=roles,
133
+ parameters=[],
134
+ database_access="objects." in fn_body or "filter(" in fn_body or ".raw(" in fn_body,
135
+ object_identifier=False,
136
+ state_changing=m in ("POST", "PUT", "DELETE", "PATCH"),
137
+ external_network="requests." in fn_body or "httpx." in fn_body or "urllib" in fn_body,
138
+ sensitive_data="admin" in fn.name.lower() or "secret" in fn.name.lower(),
139
+ source=fn.location,
140
+ )
141
+ )
142
+
143
+ # 2. DRF Class-Based Views (APIView, ModelViewSet, GenericAPIView)
144
+ for cls in parsed_file.classes:
145
+ is_view_class = any(
146
+ base in cls.base_classes
147
+ for base in ("APIView", "View", "GenericAPIView", "ModelViewSet", "ViewSet", "ReadOnlyModelViewSet")
148
+ ) or cls.name.endswith("View") or cls.name.endswith("ViewSet")
149
+
150
+ if is_view_class:
151
+ start_l = max(0, cls.location.line_start - 1)
152
+ end_l = min(len(content_lines), cls.location.line_end)
153
+ class_body = "\n".join(content_lines[start_l:end_l])
154
+
155
+ auth_required = any(
156
+ term in class_body.lower()
157
+ for term in ("isauthenticated", "isadminuser", "permission_classes", "isauthenticatedorreadonly")
158
+ )
159
+ roles = ["admin"] if "admin" in cls.name.lower() or "isadminuser" in class_body.lower() else []
160
+
161
+ # Look for handler methods: get, post, put, delete, patch
162
+ for fn in cls.methods:
163
+ method_name = fn.name.lower()
164
+ if method_name in ("get", "post", "put", "delete", "patch"):
165
+ http_method = method_name.upper()
166
+ ep_id = f"ep_cbv_{parsed_file.file_path}_{cls.name}_{http_method}".replace("/", "_").replace(".", "_")
167
+ base_path = f"/api/{cls.name.replace('View', '').replace('ViewSet', '').lower()}"
168
+ fn_body = fn.body_text or ""
169
+
170
+ endpoints.append(
171
+ Endpoint(
172
+ id=ep_id,
173
+ method=http_method,
174
+ path=base_path,
175
+ handler_name=f"{cls.name}.{fn.name}",
176
+ auth_required=auth_required,
177
+ roles=roles,
178
+ parameters=[],
179
+ database_access="objects." in fn_body or "filter(" in fn_body or ".raw(" in fn_body,
180
+ object_identifier=http_method in ("PUT", "DELETE", "PATCH"),
181
+ state_changing=http_method in ("POST", "PUT", "DELETE", "PATCH"),
182
+ external_network="requests." in fn_body or "httpx." in fn_body,
183
+ sensitive_data="admin" in cls.name.lower() or "user" in cls.name.lower(),
184
+ source=fn.location,
185
+ )
186
+ )
187
+
188
+ return endpoints