@vk.amogh/trace 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 +208 -0
  2. package/bin/trace.js +112 -0
  3. package/package.json +46 -0
  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,98 @@
1
+ """PHP and Laravel framework adapter for endpoint and route discovery."""
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 PHPFrameworkAdapter(FrameworkAdapter):
11
+ """Detects and extracts PHP endpoints from Laravel routes and native PHP API scripts."""
12
+
13
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
14
+ if parsed_file.language != "php":
15
+ norm = parsed_file.file_path.replace("\\", "/").lower()
16
+ return norm.endswith(".php")
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
+ # Database and system sinks
25
+ has_db_sink = any(
26
+ term in content_lower
27
+ for term in (
28
+ "mysqli_query", "pdo::query", "db::raw", "db::select", "db::table",
29
+ "eloquent", "->get()", "->find(", "->first()"
30
+ )
31
+ )
32
+ has_ext_sink = any(
33
+ term in content_lower
34
+ for term in ("curl_exec", "file_get_contents", "http::get", "guzzle")
35
+ )
36
+
37
+ # 1. Laravel Route definitions: Route::get('/users/{id}', [UserController::class, 'show'])
38
+ laravel_pattern = re.compile(
39
+ r"""Route::(get|post|put|delete|patch|options)\s*\(\s*['"]([^'"]+)['"]\s*,\s*([^)]+)\)""",
40
+ re.IGNORECASE,
41
+ )
42
+ for match in laravel_pattern.finditer(content):
43
+ method = match.group(1).upper()
44
+ raw_path = match.group(2).strip()
45
+ handler = match.group(3).strip()
46
+ line_no = content[:match.start()].count("\n") + 1
47
+
48
+ norm_path = raw_path if raw_path.startswith("/") else f"/{raw_path}"
49
+ path_params = re.findall(r"\{([a-zA-Z0-9_]+)\}", norm_path)
50
+
51
+ ep_id = f"ep_php_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
52
+ endpoints.append(
53
+ Endpoint(
54
+ id=ep_id,
55
+ method=method,
56
+ path=norm_path,
57
+ handler_name=handler,
58
+ auth_required="auth" in content_lower or "middleware('auth" in content_lower,
59
+ roles=["admin"] if "admin" in norm_path.lower() or "admin" in content_lower else [],
60
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
61
+ database_access=has_db_sink,
62
+ object_identifier=bool(path_params),
63
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
64
+ external_network=has_ext_sink,
65
+ sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
66
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
67
+ )
68
+ )
69
+
70
+ # 2. Native PHP file scripts: e.g. api/users.php or get_order.php
71
+ if not endpoints and any(seg in norm_file.lower() for seg in ("/api/", "endpoints/", "public/")):
72
+ script_path = "/" + norm_file.lstrip("/").replace(".php", "")
73
+ params = []
74
+ for get_match in re.findall(r"""\$_GET\s*\[\s*['"]([a-zA-Z0-9_]+)['"]\s*\]""", content):
75
+ params.append(EndpointParameter(name=get_match, location="query", required=False))
76
+ for post_match in re.findall(r"""\$_POST\s*\[\s*['"]([a-zA-Z0-9_]+)['"]\s*\]""", content):
77
+ params.append(EndpointParameter(name=post_match, location="body", required=False))
78
+
79
+ method = "POST" if "$_POST" in content else "GET"
80
+ endpoints.append(
81
+ Endpoint(
82
+ id=f"ep_php_script_{norm_file}".replace("/", "_").replace(".", "_"),
83
+ method=method,
84
+ path=script_path,
85
+ handler_name=parsed_file.file_path,
86
+ auth_required="session_start" in content_lower or "auth" in content_lower,
87
+ roles=["admin"] if "admin" in script_path.lower() else [],
88
+ parameters=params,
89
+ database_access=has_db_sink,
90
+ object_identifier=any(p.name in ("id", "user_id", "order_id") for p in params),
91
+ state_changing=method == "POST",
92
+ external_network=has_ext_sink,
93
+ sensitive_data="admin" in script_path.lower() or "user" in script_path.lower(),
94
+ source=SourceLocation(file=parsed_file.file_path, line_start=1, line_end=max(1, content.count("\n"))),
95
+ )
96
+ )
97
+
98
+ return endpoints
@@ -0,0 +1,331 @@
1
+ """React Router v7 and Remix framework adapter for endpoint discovery."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import List, Dict, Optional, Tuple
6
+ from trace_engine.parsing.parser import ParsedFile
7
+ from trace_engine.framework.base import FrameworkAdapter, Endpoint, EndpointParameter
8
+ from trace_engine.parsing.locations import SourceLocation
9
+
10
+
11
+ class ReactRouterFrameworkAdapter(FrameworkAdapter):
12
+ """Detects and extracts React Router v7 and Remix endpoints, loaders, and actions."""
13
+
14
+ def __init__(self):
15
+ # Maps (normalized_file_key) -> url_path
16
+ self._route_cache: Dict[str, str] = {}
17
+ self._parsed_route_files: set = set()
18
+
19
+ def _normalize_key(self, path_str: str) -> str:
20
+ clean = path_str.replace("\\", "/").strip("/")
21
+ if clean.startswith("app/"):
22
+ clean = clean[4:]
23
+ return clean
24
+
25
+ def _discover_and_parse_routes_file(self, parsed_file: ParsedFile) -> None:
26
+ """Looks up routes.ts / routes.js up the directory tree and caches mappings."""
27
+ file_p = Path(parsed_file.absolute_path) if parsed_file.absolute_path else Path(parsed_file.file_path)
28
+ candidates = []
29
+
30
+ if file_p.is_absolute():
31
+ search_dirs = [file_p.parent] + list(file_p.parents)
32
+ else:
33
+ search_dirs = [Path(".").resolve()]
34
+
35
+ for d in search_dirs:
36
+ for cand_name in ("app/routes.ts", "routes.ts", "app/routes.js", "routes.js", "app/routes.tsx", "routes.tsx"):
37
+ cand = d / cand_name
38
+ if cand.exists() and cand.is_file():
39
+ candidates.append(cand)
40
+ break
41
+ if candidates:
42
+ break
43
+
44
+ for cand in candidates:
45
+ abs_cand_str = str(cand.resolve())
46
+ if abs_cand_str in self._parsed_route_files:
47
+ continue
48
+ self._parsed_route_files.add(abs_cand_str)
49
+ try:
50
+ content = cand.read_text(encoding="utf-8", errors="replace")
51
+ self._parse_routes_config(content)
52
+ except Exception:
53
+ pass
54
+
55
+ def _parse_routes_config(self, content: str) -> None:
56
+ """Parses route definitions from React Router v7 routes.ts."""
57
+ # 1. index("routes/home.tsx")
58
+ for m in re.finditer(r'''index\s*\(\s*["']([^"']+)["']''', content):
59
+ file_target = self._normalize_key(m.group(1))
60
+ self._route_cache[file_target] = "/"
61
+
62
+ # 2. route("path", "file")
63
+ for m in re.finditer(r'''route\s*\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']''', content):
64
+ path_part = m.group(1)
65
+ file_target = self._normalize_key(m.group(2))
66
+ norm_url = "/" + path_part.lstrip("/")
67
+ self._route_cache[file_target] = norm_url
68
+
69
+ # 3. layout("layout_file", [ ... ])
70
+ # Also map layout file if needed
71
+ for m in re.finditer(r'''layout\s*\(\s*["']([^"']+)["']''', content):
72
+ file_target = self._normalize_key(m.group(1))
73
+ if file_target not in self._route_cache:
74
+ self._route_cache[file_target] = "/admin" if "admin" in file_target else "/"
75
+
76
+ def _infer_path_from_filename(self, file_path: str) -> str:
77
+ """Derives route path from Remix / React Router convention-based file names."""
78
+ clean = self._normalize_key(file_path)
79
+ # Strip routes/ prefix
80
+ if clean.startswith("routes/"):
81
+ clean = clean[7:]
82
+
83
+ # Strip extension
84
+ clean = re.sub(r'\.(tsx|ts|jsx|js)$', '', clean)
85
+
86
+ # Handle escaped characters like sitemap[.xml] -> sitemap.xml
87
+ clean = clean.replace("[.", ".").replace(".]", ".")
88
+ clean = clean.replace("[", "").replace("]", "")
89
+
90
+ # Dots in flat routes represent slashes: api.check-username -> api/check-username
91
+ clean = clean.replace(".", "/")
92
+
93
+ # Remix param conventions: $id -> {id}, :id -> {id}
94
+ clean = re.sub(r'\$([a-zA-Z0-9_]+)', r'{\1}', clean)
95
+ clean = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', clean)
96
+
97
+ # Index routes
98
+ clean = re.sub(r'(?:^|/)_?index$', '', clean)
99
+
100
+ # Remove pathless layout segments (e.g. _auth, _public)
101
+ parts = [p for p in clean.split("/") if not (p.startswith("_") and not p == "")]
102
+ clean = "/".join(parts)
103
+
104
+ norm_path = "/" + clean.lstrip("/")
105
+ return norm_path or "/"
106
+
107
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
108
+ if parsed_file.language not in ("javascript", "typescript"):
109
+ return False
110
+
111
+ norm_path = parsed_file.file_path.replace("\\", "/")
112
+ basename = norm_path.split("/")[-1]
113
+
114
+ # Ignore configuration files and hidden/dot files
115
+ if "config" in basename.lower() or basename.startswith("."):
116
+ return False
117
+
118
+ # If it's the routes configuration file itself
119
+ if re.search(r'(?:^|/)(?:app/)?routes\.(ts|js|tsx|jsx)$', norm_path):
120
+ return True
121
+
122
+ # Lazy check and load routes.ts if present
123
+ self._discover_and_parse_routes_file(parsed_file)
124
+
125
+ # Check if file is in cached route table
126
+ key = self._normalize_key(norm_path)
127
+ if key in self._route_cache:
128
+ return True
129
+
130
+ # Check if file is within a routes directory (Remix / React Router convention)
131
+ if "/routes/" in norm_path or norm_path.startswith("routes/"):
132
+ # Exclude non-route subdirectories like styles, assets, or types
133
+ if any(f"/{d}/" in norm_path for d in ("styles", "assets", "types", "+types")):
134
+ return False
135
+ return True
136
+
137
+ return False
138
+
139
+ def extract_endpoints(self, parsed_file: ParsedFile, content: str) -> List[Endpoint]:
140
+ endpoints: List[Endpoint] = []
141
+ norm_file_path = parsed_file.file_path.replace("\\", "/")
142
+
143
+ # Ensure routes.ts is parsed
144
+ self._discover_and_parse_routes_file(parsed_file)
145
+
146
+ # If this file is routes.ts itself, the routes are mapped to individual handler files
147
+ if re.search(r'(?:^|/)(?:app/)?routes\.(ts|js|tsx|jsx)$', norm_file_path):
148
+ # Parse route config to make sure route cache is up to date
149
+ self._parse_routes_config(content)
150
+ return []
151
+
152
+ # Determine route URL path
153
+ norm_key = self._normalize_key(norm_file_path)
154
+ raw_path = self._route_cache.get(norm_key)
155
+ if not raw_path:
156
+ # Try alternate key without leading dir
157
+ basename = norm_file_path.split("/")[-1]
158
+ raw_path = self._route_cache.get(basename)
159
+
160
+ if not raw_path:
161
+ raw_path = self._infer_path_from_filename(norm_file_path)
162
+
163
+ # Normalize dynamic path segments :id -> {id}
164
+ url_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
165
+ url_path = re.sub(r'\$([a-zA-Z0-9_]+)', r'{\1}', url_path)
166
+
167
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', url_path)
168
+
169
+ # Analyze handler bodies and symbols
170
+ content_lower = content.lower()
171
+
172
+ # Database sinks
173
+ has_db_sink = any(
174
+ term in content_lower
175
+ for term in (
176
+ "supabase", "prisma", "sequelize", "mongoose", "knex",
177
+ "pool.query", "client.query", "db.query", "db.select",
178
+ ".from(", ".rpc(", ".server"
179
+ )
180
+ )
181
+
182
+ # External network sinks
183
+ has_ext_sink = any(
184
+ term in content_lower
185
+ for term in (
186
+ "axios", "fetch(", "nodemailer", "got(", "needle",
187
+ "http.request", "https.request", "transporter.sendmail",
188
+ "octokit", "stripe"
189
+ )
190
+ )
191
+
192
+ # Authentication & Role detection
193
+ auth_signals = (
194
+ "requireauth", "requireadmin", "requireadmingate", "getsessionuser",
195
+ "requiregithub", "supabase.auth", "verifytoken", "jwt.verify",
196
+ "isauthenticated", "authsession", "getserversession", "getauth"
197
+ )
198
+ auth_required = any(sig in content_lower for sig in auth_signals) or "admin" in url_path.lower()
199
+ roles = ["admin"] if "admin" in content_lower or "admin" in url_path.lower() else []
200
+
201
+ sensitive_data = any(
202
+ term in url_path.lower() or term in content_lower
203
+ for term in (
204
+ "admin", "user", "auth", "login", "register", "profile",
205
+ "password", "token", "secret", "webhook", "session", "payment"
206
+ )
207
+ )
208
+
209
+ # Parameters
210
+ parameters: List[EndpointParameter] = [
211
+ EndpointParameter(name=p, location="path", required=True, user_controlled=True)
212
+ for p in path_params
213
+ ]
214
+
215
+ # Extract Query Parameters: searchParams.get("...")
216
+ for m in re.finditer(r'''(?:searchParams|nextUrl\.searchParams)\.get\s*\(\s*["']([^"']+)["']''', content):
217
+ p_name = m.group(1)
218
+ if not any(p.name == p_name for p in parameters):
219
+ parameters.append(
220
+ EndpointParameter(name=p_name, location="query", required=False, user_controlled=True)
221
+ )
222
+
223
+ # Extract Body Parameters: formData.get("...")
224
+ for m in re.finditer(r'''formData\.get\s*\(\s*["']([^"']+)["']''', content):
225
+ p_name = m.group(1)
226
+ if not any(p.name == p_name for p in parameters):
227
+ parameters.append(
228
+ EndpointParameter(name=p_name, location="body", required=True, user_controlled=True)
229
+ )
230
+
231
+ # Check for loader (GET)
232
+ loader_match = re.search(
233
+ r'''(?:export\s+)?(?:async\s+)?function\s+loader\b|export\s+(?:const|let|var)\s+loader\b''',
234
+ content
235
+ )
236
+ if loader_match:
237
+ line_no = content[:loader_match.start()].count("\n") + 1
238
+ ep_id = f"ep_rr_get_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
239
+ endpoints.append(
240
+ Endpoint(
241
+ id=ep_id,
242
+ method="GET",
243
+ path=url_path,
244
+ handler_name="loader",
245
+ auth_required=auth_required,
246
+ roles=roles,
247
+ parameters=parameters,
248
+ database_access=has_db_sink,
249
+ object_identifier=bool(path_params),
250
+ state_changing=False,
251
+ external_network=has_ext_sink,
252
+ sensitive_data=sensitive_data,
253
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
254
+ )
255
+ )
256
+
257
+ # Check for action (POST / mutation)
258
+ action_match = re.search(
259
+ r'''(?:export\s+)?(?:async\s+)?function\s+action\b|export\s+(?:const|let|var)\s+action\b''',
260
+ content
261
+ )
262
+ if action_match:
263
+ line_no = content[:action_match.start()].count("\n") + 1
264
+ ep_id = f"ep_rr_post_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
265
+ endpoints.append(
266
+ Endpoint(
267
+ id=ep_id,
268
+ method="POST",
269
+ path=url_path,
270
+ handler_name="action",
271
+ auth_required=auth_required,
272
+ roles=roles,
273
+ parameters=parameters,
274
+ database_access=has_db_sink,
275
+ object_identifier=bool(path_params),
276
+ state_changing=True,
277
+ external_network=has_ext_sink,
278
+ sensitive_data=sensitive_data,
279
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
280
+ )
281
+ )
282
+
283
+ # Check for explicit HTTP verbs: GET, POST, PUT, DELETE, PATCH
284
+ for v_match in re.finditer(r'''export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b''', content):
285
+ verb = v_match.group(1).upper()
286
+ line_no = content[:v_match.start()].count("\n") + 1
287
+ ep_id = f"ep_rr_{verb.lower()}_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
288
+ endpoints.append(
289
+ Endpoint(
290
+ id=ep_id,
291
+ method=verb,
292
+ path=url_path,
293
+ handler_name=verb,
294
+ auth_required=auth_required,
295
+ roles=roles,
296
+ parameters=parameters,
297
+ database_access=has_db_sink,
298
+ object_identifier=bool(path_params),
299
+ state_changing=verb in ("POST", "PUT", "DELETE", "PATCH"),
300
+ external_network=has_ext_sink,
301
+ sensitive_data=sensitive_data,
302
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
303
+ )
304
+ )
305
+
306
+ # If file is a route file (e.g. in routes/ or mapped) but defines neither loader nor action nor verbs,
307
+ # it is a page component serving GET
308
+ if not endpoints:
309
+ default_match = re.search(r'''export\s+default\s+(?:function|const|class)?\s*([a-zA-Z0-9_$]+)?''', content)
310
+ handler_name = default_match.group(1) if (default_match and default_match.group(1)) else "default"
311
+ line_no = content[:default_match.start()].count("\n") + 1 if default_match else 1
312
+ ep_id = f"ep_rr_page_{norm_file_path}_{line_no}".replace("/", "_").replace(".", "_")
313
+ endpoints.append(
314
+ Endpoint(
315
+ id=ep_id,
316
+ method="GET",
317
+ path=url_path,
318
+ handler_name=handler_name,
319
+ auth_required=auth_required,
320
+ roles=roles,
321
+ parameters=parameters,
322
+ database_access=has_db_sink,
323
+ object_identifier=bool(path_params),
324
+ state_changing=False,
325
+ external_network=has_ext_sink,
326
+ sensitive_data=sensitive_data,
327
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
328
+ )
329
+ )
330
+
331
+ return endpoints
@@ -0,0 +1,69 @@
1
+ """Ruby on Rails framework adapter for endpoint and route discovery."""
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 RubyFrameworkAdapter(FrameworkAdapter):
11
+ """Detects and extracts Ruby on Rails routes, controllers, and sinks."""
12
+
13
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
14
+ if parsed_file.language != "ruby":
15
+ norm = parsed_file.file_path.replace("\\", "/").lower()
16
+ return norm.endswith(".rb")
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 ("active_record", "connection.execute", "find_by_sql", "where(", "find(")
27
+ )
28
+ has_ext_sink = any(
29
+ term in content_lower
30
+ for term in ("net::http", "httparty", "faraday", "restclient")
31
+ )
32
+
33
+ # Rails routes: get '/users/:id', to: 'users#show'
34
+ # post '/orders', to: 'orders#create'
35
+ route_pattern = re.compile(
36
+ r"""(?:get|post|put|delete|patch)\s+['"]([^'"]+)['"](?:\s*,\s*to:\s*['"]([^'"]+)['"])?""",
37
+ re.IGNORECASE,
38
+ )
39
+
40
+ for match in route_pattern.finditer(content):
41
+ raw_path = match.group(1).strip()
42
+ handler = match.group(2) or "rails_controller_action"
43
+ line_no = content[:match.start()].count("\n") + 1
44
+
45
+ method = match.group(0).split()[0].upper()
46
+ norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
47
+ norm_path = "/" + norm_path.lstrip("/")
48
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
49
+
50
+ ep_id = f"ep_ruby_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
51
+ endpoints.append(
52
+ Endpoint(
53
+ id=ep_id,
54
+ method=method,
55
+ path=norm_path,
56
+ handler_name=handler,
57
+ auth_required="authenticate" in content_lower or "before_action :authenticate" in content_lower,
58
+ roles=["admin"] if "admin" in norm_path.lower() else [],
59
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
60
+ database_access=has_db_sink,
61
+ object_identifier=bool(path_params),
62
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
63
+ external_network=has_ext_sink,
64
+ sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
65
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
66
+ )
67
+ )
68
+
69
+ return endpoints
@@ -0,0 +1,101 @@
1
+ """Rust framework adapter for Actix-web, Axum, and Rocket 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 RustFrameworkAdapter(FrameworkAdapter):
11
+ """Detects and extracts Rust routes from Actix-web, Axum, and Rocket."""
12
+
13
+ def can_handle(self, parsed_file: ParsedFile) -> bool:
14
+ if parsed_file.language != "rust":
15
+ norm = parsed_file.file_path.replace("\\", "/").lower()
16
+ return norm.endswith(".rs")
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 ("sqlx::", "diesel::", "sea_orm", "query_as!", "query!", "execute(")
27
+ )
28
+ has_ext_sink = any(
29
+ term in content_lower
30
+ for term in ("reqwest::", "hyper::", "surf::")
31
+ )
32
+
33
+ # 1. Actix-web / Rocket macros: #[get("/path/{id}")] or #[post("/path")]
34
+ macro_pattern = re.compile(
35
+ r"""#\[(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']\s*\)\]\s*(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)""",
36
+ re.IGNORECASE,
37
+ )
38
+ for match in macro_pattern.finditer(content):
39
+ method = match.group(1).upper()
40
+ raw_path = match.group(2).strip()
41
+ handler = match.group(3).strip()
42
+ line_no = content[:match.start()].count("\n") + 1
43
+
44
+ norm_path = raw_path if raw_path.startswith("/") else f"/{raw_path}"
45
+ norm_path = re.sub(r'<([a-zA-Z0-9_]+)>', r'{\1}', norm_path)
46
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
47
+
48
+ ep_id = f"ep_rust_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
49
+ endpoints.append(
50
+ Endpoint(
51
+ id=ep_id,
52
+ method=method,
53
+ path=norm_path,
54
+ handler_name=handler,
55
+ auth_required="auth" in content_lower or "claims" in content_lower,
56
+ roles=["admin"] if "admin" in norm_path.lower() else [],
57
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
58
+ database_access=has_db_sink,
59
+ object_identifier=bool(path_params),
60
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
61
+ external_network=has_ext_sink,
62
+ sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
63
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
64
+ )
65
+ )
66
+
67
+ # 2. Axum routes: .route("/path", get(handler)) or post(handler)
68
+ axum_pattern = re.compile(
69
+ r"""\.route\s*\(\s*["']([^"']+)["']\s*,\s*(get|post|put|delete|patch)\s*\(\s*([a-zA-Z0-9_:]+)\s*\)\s*\)""",
70
+ re.IGNORECASE,
71
+ )
72
+ for match in axum_pattern.finditer(content):
73
+ raw_path = match.group(1).strip()
74
+ method = match.group(2).upper()
75
+ handler = match.group(3).strip()
76
+ line_no = content[:match.start()].count("\n") + 1
77
+
78
+ norm_path = re.sub(r':([a-zA-Z0-9_]+)', r'{\1}', raw_path)
79
+ norm_path = "/" + norm_path.lstrip("/")
80
+ path_params = re.findall(r'\{([a-zA-Z0-9_]+)\}', norm_path)
81
+
82
+ ep_id = f"ep_rust_axum_{norm_file}_{line_no}_{method}".replace("/", "_").replace(".", "_")
83
+ endpoints.append(
84
+ Endpoint(
85
+ id=ep_id,
86
+ method=method,
87
+ path=norm_path,
88
+ handler_name=handler,
89
+ auth_required="auth" in content_lower or "claims" in content_lower,
90
+ roles=["admin"] if "admin" in norm_path.lower() else [],
91
+ parameters=[EndpointParameter(name=p, location="path", required=True) for p in path_params],
92
+ database_access=has_db_sink,
93
+ object_identifier=bool(path_params),
94
+ state_changing=method in ("POST", "PUT", "DELETE", "PATCH"),
95
+ external_network=has_ext_sink,
96
+ sensitive_data="admin" in norm_path.lower() or "user" in norm_path.lower(),
97
+ source=SourceLocation(file=parsed_file.file_path, line_start=line_no, line_end=line_no),
98
+ )
99
+ )
100
+
101
+ return endpoints