@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,208 @@
1
+ """Constructs the Attack-Path Model from parsed files and discovered endpoints."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Dict, Optional
5
+ from trace_engine.ingest.files import SourceFile
6
+ from trace_engine.parsing.parser import ParsedFile
7
+ from trace_engine.framework.base import Endpoint
8
+ from trace_engine.apm.model import AttackPathModel
9
+ from trace_engine.apm.nodes import APMNode, NodeType
10
+ from trace_engine.apm.edges import APMEdge, EdgeType
11
+
12
+
13
+ class APMBuilder:
14
+ """Orchestrates building an Attack-Path Model graph from static analysis artifacts."""
15
+
16
+ def __init__(self, project_name: str = "project"):
17
+ self.project_name = project_name
18
+
19
+ def build(
20
+ self,
21
+ repo_path: Path,
22
+ source_files: List[SourceFile],
23
+ parsed_files: List[ParsedFile],
24
+ endpoints: List[Endpoint],
25
+ ) -> AttackPathModel:
26
+ apm = AttackPathModel(name=self.project_name)
27
+
28
+ # 1. Root Repository Node
29
+ repo_node_id = "repo_root"
30
+ apm.add_node(
31
+ APMNode(
32
+ id=repo_node_id,
33
+ node_type=NodeType.REPOSITORY,
34
+ label=self.project_name,
35
+ properties={"path": str(repo_path.resolve())},
36
+ )
37
+ )
38
+
39
+ # 2. File Nodes
40
+ file_node_map: Dict[str, str] = {}
41
+ for sf in source_files:
42
+ file_id = f"file_{sf.path.replace('/', '_').replace('.', '_')}"
43
+ file_node_map[sf.path] = file_id
44
+ apm.add_node(
45
+ APMNode(
46
+ id=file_id,
47
+ node_type=NodeType.FILE,
48
+ label=sf.path,
49
+ properties={
50
+ "language": sf.language,
51
+ "line_count": sf.line_count,
52
+ "sha256": sf.sha256,
53
+ },
54
+ )
55
+ )
56
+ apm.add_edge(
57
+ APMEdge(
58
+ source_id=repo_node_id,
59
+ target_id=file_id,
60
+ edge_type=EdgeType.CONTAINS,
61
+ )
62
+ )
63
+
64
+ # 3. Function Nodes
65
+ func_node_map: Dict[str, str] = {}
66
+ for pf in parsed_files:
67
+ file_id = file_node_map.get(pf.file_path, repo_node_id)
68
+ for fn in pf.functions:
69
+ fn_id = f"fn_{pf.file_path}_{fn.qualified_name}".replace("/", "_").replace(".", "_")
70
+ func_node_map[fn.qualified_name] = fn_id
71
+ apm.add_node(
72
+ APMNode(
73
+ id=fn_id,
74
+ node_type=NodeType.FUNCTION,
75
+ label=fn.qualified_name,
76
+ location=fn.location,
77
+ properties={"is_async": fn.is_async},
78
+ )
79
+ )
80
+ apm.add_edge(
81
+ APMEdge(
82
+ source_id=file_id,
83
+ target_id=fn_id,
84
+ edge_type=EdgeType.CONTAINS,
85
+ )
86
+ )
87
+
88
+ # 4. Endpoints & Parameters
89
+ for ep in endpoints:
90
+ ep_node_id = ep.id
91
+ apm.add_node(
92
+ APMNode(
93
+ id=ep_node_id,
94
+ node_type=NodeType.ENDPOINT,
95
+ label=ep.display_name(),
96
+ location=ep.source,
97
+ properties={
98
+ "method": ep.method,
99
+ "path": ep.path,
100
+ "auth_required": ep.auth_required,
101
+ "roles": ep.roles,
102
+ "object_identifier": ep.object_identifier,
103
+ "state_changing": ep.state_changing,
104
+ "sensitive_data": ep.sensitive_data,
105
+ },
106
+ )
107
+ )
108
+
109
+ # Connect File -> Endpoint
110
+ file_id = file_node_map.get(ep.source.file, repo_node_id)
111
+ apm.add_edge(
112
+ APMEdge(
113
+ source_id=file_id,
114
+ target_id=ep_node_id,
115
+ edge_type=EdgeType.EXPOSES,
116
+ )
117
+ )
118
+
119
+ # Connect Endpoint -> Handler Function
120
+ if ep.handler_name in func_node_map:
121
+ apm.add_edge(
122
+ APMEdge(
123
+ source_id=ep_node_id,
124
+ target_id=func_node_map[ep.handler_name],
125
+ edge_type=EdgeType.ROUTES_TO,
126
+ )
127
+ )
128
+
129
+ # Parameters
130
+ for param in ep.parameters:
131
+ param_id = f"param_{ep.id}_{param.name}"
132
+ apm.add_node(
133
+ APMNode(
134
+ id=param_id,
135
+ node_type=NodeType.PARAMETER,
136
+ label=param.name,
137
+ properties={
138
+ "location": param.location,
139
+ "type": param.param_type,
140
+ "user_controlled": param.user_controlled,
141
+ },
142
+ )
143
+ )
144
+ apm.add_edge(
145
+ APMEdge(
146
+ source_id=ep_node_id,
147
+ target_id=param_id,
148
+ edge_type=EdgeType.RECEIVES,
149
+ )
150
+ )
151
+
152
+ # Auth Gate Node
153
+ if ep.auth_required:
154
+ auth_id = f"auth_{ep.id}"
155
+ apm.add_node(
156
+ APMNode(
157
+ id=auth_id,
158
+ node_type=NodeType.AUTH_CHECK,
159
+ label="AuthGate" if not ep.roles else f"RoleGate({','.join(ep.roles)})",
160
+ properties={"roles": ep.roles},
161
+ )
162
+ )
163
+ apm.add_edge(
164
+ APMEdge(
165
+ source_id=auth_id,
166
+ target_id=ep_node_id,
167
+ edge_type=EdgeType.PROTECTS,
168
+ )
169
+ )
170
+
171
+ # Sinks / Database Nodes
172
+ if ep.database_access:
173
+ db_id = f"db_{ep.id}"
174
+ apm.add_node(
175
+ APMNode(
176
+ id=db_id,
177
+ node_type=NodeType.DATABASE,
178
+ label="DatabaseAccess",
179
+ properties={"operation": "lookup" if ep.object_identifier else "query"},
180
+ )
181
+ )
182
+ apm.add_edge(
183
+ APMEdge(
184
+ source_id=ep_node_id,
185
+ target_id=db_id,
186
+ edge_type=EdgeType.ACCESSES,
187
+ )
188
+ )
189
+
190
+ if ep.external_network:
191
+ ext_id = f"ext_{ep.id}"
192
+ apm.add_node(
193
+ APMNode(
194
+ id=ext_id,
195
+ node_type=NodeType.EXTERNAL_SERVICE,
196
+ label="OutboundHTTPClient",
197
+ properties={"risk": "SSRF"},
198
+ )
199
+ )
200
+ apm.add_edge(
201
+ APMEdge(
202
+ source_id=ep_node_id,
203
+ target_id=ext_id,
204
+ edge_type=EdgeType.CONNECTS_TO,
205
+ )
206
+ )
207
+
208
+ return apm
@@ -0,0 +1,25 @@
1
+ """Edge definitions for the Attack-Path Model (APM)."""
2
+
3
+ from enum import Enum
4
+ from typing import Dict, Any, Optional
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class EdgeType(str, Enum):
9
+ CONTAINS = "CONTAINS"
10
+ EXPOSES = "EXPOSES"
11
+ RECEIVES = "RECEIVES"
12
+ ROUTES_TO = "ROUTES_TO"
13
+ CALLS = "CALLS"
14
+ PROTECTS = "PROTECTS"
15
+ ACCESSES = "ACCESSES"
16
+ SINKS_TO = "SINKS_TO"
17
+ CONNECTS_TO = "CONNECTS_TO"
18
+
19
+
20
+ class APMEdge(BaseModel):
21
+ """Directed edge between two APM nodes."""
22
+ source_id: str
23
+ target_id: str
24
+ edge_type: EdgeType
25
+ properties: Dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,107 @@
1
+ """Attack-Path Model (APM) graph representation using NetworkX."""
2
+
3
+ import networkx as nx
4
+ from typing import List, Dict, Any, Optional, Set
5
+ from trace_engine.apm.nodes import APMNode, NodeType
6
+ from trace_engine.apm.edges import APMEdge, EdgeType
7
+
8
+
9
+ class AttackPathModel:
10
+ """Core Attack-Path Model representing application architecture and attack surface."""
11
+
12
+ def __init__(self, name: str = "project"):
13
+ self.name = name
14
+ self.graph = nx.DiGraph()
15
+ self.nodes_data: Dict[str, APMNode] = {}
16
+ self.edges_data: List[APMEdge] = []
17
+
18
+ def add_node(self, node: APMNode) -> None:
19
+ """Add an APM node to the graph."""
20
+ self.nodes_data[node.id] = node
21
+ self.graph.add_node(
22
+ node.id,
23
+ node_type=node.node_type.value,
24
+ label=node.label,
25
+ source_location=node.location.model_dump() if node.location else None,
26
+ **node.properties,
27
+ )
28
+
29
+ def add_edge(self, edge: APMEdge) -> None:
30
+ """Add a directed APM edge to the graph."""
31
+ self.edges_data.append(edge)
32
+ self.graph.add_edge(
33
+ edge.source_id,
34
+ edge.target_id,
35
+ edge_type=edge.edge_type.value,
36
+ **edge.properties,
37
+ )
38
+
39
+ def get_node(self, node_id: str) -> Optional[APMNode]:
40
+ return self.nodes_data.get(node_id)
41
+
42
+ def get_endpoints(self) -> List[APMNode]:
43
+ """Return all endpoint nodes in the graph."""
44
+ return [
45
+ node
46
+ for node in self.nodes_data.values()
47
+ if node.node_type == NodeType.ENDPOINT
48
+ ]
49
+
50
+ def find_paths_from_endpoint(self, endpoint_id: str, max_depth: int = 4) -> List[List[str]]:
51
+ """Find all downstream paths originating from an endpoint up to max_depth."""
52
+ if endpoint_id not in self.graph:
53
+ return []
54
+
55
+ paths: List[List[str]] = []
56
+ visited = set()
57
+
58
+ def dfs(curr: str, current_path: List[str], depth: int):
59
+ if depth > max_depth or curr in visited:
60
+ return
61
+ current_path.append(curr)
62
+ visited.add(curr)
63
+
64
+ successors = list(self.graph.successors(curr))
65
+ if not successors:
66
+ paths.append(list(current_path))
67
+ else:
68
+ for succ in successors:
69
+ dfs(succ, current_path, depth + 1)
70
+
71
+ visited.remove(curr)
72
+ current_path.pop()
73
+
74
+ dfs(endpoint_id, [], 0)
75
+ return paths
76
+
77
+ def find_paths_to_sinks(self, endpoint_id: str) -> List[List[str]]:
78
+ """Find paths connecting an endpoint to any Sink node."""
79
+ sink_nodes = [
80
+ n_id
81
+ for n_id, n in self.nodes_data.items()
82
+ if n.node_type in (NodeType.SINK, NodeType.DATABASE, NodeType.EXTERNAL_SERVICE)
83
+ ]
84
+
85
+ result_paths = []
86
+ for sink_id in sink_nodes:
87
+ if nx.has_path(self.graph, endpoint_id, sink_id):
88
+ for path in nx.all_simple_paths(self.graph, endpoint_id, sink_id, cutoff=5):
89
+ result_paths.append(path)
90
+ return result_paths
91
+
92
+ def summary(self) -> Dict[str, Any]:
93
+ """Return a statistical overview of the graph."""
94
+ type_counts: Dict[str, int] = {}
95
+ for node in self.nodes_data.values():
96
+ type_counts[node.node_type.value] = type_counts.get(node.node_type.value, 0) + 1
97
+
98
+ edge_counts: Dict[str, int] = {}
99
+ for edge in self.edges_data:
100
+ edge_counts[edge.edge_type.value] = edge_counts.get(edge.edge_type.value, 0) + 1
101
+
102
+ return {
103
+ "total_nodes": self.graph.number_of_nodes(),
104
+ "total_edges": self.graph.number_of_edges(),
105
+ "nodes_by_type": type_counts,
106
+ "edges_by_type": edge_counts,
107
+ }
@@ -0,0 +1,27 @@
1
+ """Node definitions for the Attack-Path Model (APM)."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional, Dict, Any, List
5
+ from pydantic import BaseModel, Field
6
+ from trace_engine.parsing.locations import SourceLocation
7
+
8
+
9
+ class NodeType(str, Enum):
10
+ REPOSITORY = "Repository"
11
+ FILE = "File"
12
+ ENDPOINT = "Endpoint"
13
+ FUNCTION = "Function"
14
+ PARAMETER = "Parameter"
15
+ AUTH_CHECK = "AuthCheck"
16
+ DATABASE = "Database"
17
+ SINK = "Sink"
18
+ EXTERNAL_SERVICE = "ExternalService"
19
+
20
+
21
+ class APMNode(BaseModel):
22
+ """Base node in the Attack-Path Model graph."""
23
+ id: str
24
+ node_type: NodeType
25
+ label: str
26
+ location: Optional[SourceLocation] = None
27
+ properties: Dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,105 @@
1
+ """Serialization and SQLite persistence for Attack-Path Model."""
2
+
3
+ import sqlite3
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from trace_engine.apm.model import AttackPathModel
8
+ from trace_engine.apm.nodes import APMNode, NodeType
9
+ from trace_engine.apm.edges import APMEdge, EdgeType
10
+ from trace_engine.parsing.locations import SourceLocation
11
+
12
+
13
+ def save_apm_sqlite(apm: AttackPathModel, db_path: Path) -> None:
14
+ """Save APM graph to SQLite database."""
15
+ db_path.parent.mkdir(parents=True, exist_ok=True)
16
+ conn = sqlite3.connect(str(db_path))
17
+ cursor = conn.cursor()
18
+
19
+ cursor.execute("""
20
+ CREATE TABLE IF NOT EXISTS apm_nodes (
21
+ id TEXT PRIMARY KEY,
22
+ node_type TEXT NOT NULL,
23
+ label TEXT NOT NULL,
24
+ file TEXT,
25
+ line_start INTEGER,
26
+ line_end INTEGER,
27
+ properties TEXT
28
+ )
29
+ """)
30
+
31
+ cursor.execute("""
32
+ CREATE TABLE IF NOT EXISTS apm_edges (
33
+ source_id TEXT NOT NULL,
34
+ target_id TEXT NOT NULL,
35
+ edge_type TEXT NOT NULL,
36
+ properties TEXT
37
+ )
38
+ """)
39
+
40
+ cursor.execute("DELETE FROM apm_nodes")
41
+ cursor.execute("DELETE FROM apm_edges")
42
+
43
+ for node in apm.nodes_data.values():
44
+ cursor.execute(
45
+ """
46
+ INSERT INTO apm_nodes (id, node_type, label, file, line_start, line_end, properties)
47
+ VALUES (?, ?, ?, ?, ?, ?, ?)
48
+ """,
49
+ (
50
+ node.id,
51
+ node.node_type.value,
52
+ node.label,
53
+ node.location.file if node.location else None,
54
+ node.location.line_start if node.location else None,
55
+ node.location.line_end if node.location else None,
56
+ json.dumps(node.properties),
57
+ ),
58
+ )
59
+
60
+ for edge in apm.edges_data:
61
+ cursor.execute(
62
+ """
63
+ INSERT INTO apm_edges (source_id, target_id, edge_type, properties)
64
+ VALUES (?, ?, ?, ?)
65
+ """,
66
+ (
67
+ edge.source_id,
68
+ edge.target_id,
69
+ edge.edge_type.value,
70
+ json.dumps(edge.properties),
71
+ ),
72
+ )
73
+
74
+ conn.commit()
75
+ conn.close()
76
+
77
+
78
+ def export_apm_json(apm: AttackPathModel, json_path: Path) -> None:
79
+ """Export APM graph to JSON."""
80
+ json_path.parent.mkdir(parents=True, exist_ok=True)
81
+ data = {
82
+ "name": apm.name,
83
+ "nodes": [
84
+ {
85
+ "id": n.id,
86
+ "node_type": n.node_type.value,
87
+ "label": n.label,
88
+ "location": n.location.model_dump() if n.location else None,
89
+ "properties": n.properties,
90
+ }
91
+ for n in apm.nodes_data.values()
92
+ ],
93
+ "edges": [
94
+ {
95
+ "source": e.source_id,
96
+ "target": e.target_id,
97
+ "edge_type": e.edge_type.value,
98
+ "properties": e.properties,
99
+ }
100
+ for e in apm.edges_data
101
+ ],
102
+ "summary": apm.summary(),
103
+ }
104
+ with open(json_path, "w", encoding="utf-8") as f:
105
+ json.dump(data, f, indent=2)
@@ -0,0 +1,5 @@
1
+ """TRACE Benchmark Suite."""
2
+
3
+ from trace_engine.benchmark.owasp import run_owasp_benchmark, find_owasp_benchmark_dir
4
+
5
+ __all__ = ["run_owasp_benchmark", "find_owasp_benchmark_dir"]
@@ -0,0 +1,183 @@
1
+ """OWASP Benchmark Python evaluator for TRACE Specification Section 12."""
2
+
3
+ import csv
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Dict, Any, List, Optional
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from trace_engine.parsing.parser import CodeParser
11
+ from trace_engine.intelligence.securebert.classifier import SecureBERTClassifier
12
+
13
+ logger = logging.getLogger(__name__)
14
+ console = Console()
15
+
16
+
17
+ def find_owasp_benchmark_dir() -> Optional[Path]:
18
+ """Dynamically locates the benchmarks/owasp-python directory."""
19
+ candidates = [
20
+ Path("benchmarks/owasp-python"),
21
+ Path(__file__).resolve().parent.parent.parent.parent / "benchmarks/owasp-python",
22
+ Path.cwd() / "benchmarks/owasp-python",
23
+ ]
24
+ for c in candidates:
25
+ if c.exists() and (c / "expectedresults-0.1.csv").exists():
26
+ return c.resolve()
27
+ return None
28
+
29
+
30
+ def run_owasp_benchmark(limit: int = 1230) -> Dict[str, Any]:
31
+ """Evaluates TRACE intelligence against OWASP Benchmark test cases with per-category metrics."""
32
+ benchmark_dir = find_owasp_benchmark_dir()
33
+ if not benchmark_dir:
34
+ console.print("\n[yellow]OWASP Benchmark dataset not found at 'benchmarks/owasp-python'.[/yellow]")
35
+ console.print("[dim]Ensure the dataset directory exists in the workspace root.[/dim]\n")
36
+ return {}
37
+
38
+ csv_file = benchmark_dir / "expectedresults-0.1.csv"
39
+ testcode_dir = benchmark_dir / "testcode"
40
+
41
+ if not csv_file.exists() or not testcode_dir.exists():
42
+ console.print(f"\n[yellow]OWASP Benchmark files missing in {benchmark_dir}.[/yellow]\n")
43
+ return {}
44
+
45
+ # Read ground truth
46
+ ground_truth: Dict[str, Dict[str, Any]] = {}
47
+ with open(csv_file, "r", encoding="utf-8") as f:
48
+ reader = csv.reader(f)
49
+ for row in reader:
50
+ if not row or row[0].startswith("#"):
51
+ continue
52
+ test_name = row[0].strip()
53
+ category = row[1].strip()
54
+ is_vuln = row[2].strip().lower() == "true"
55
+ cwe = row[3].strip() if len(row) > 3 else ""
56
+ ground_truth[test_name] = {
57
+ "category": category,
58
+ "is_vuln": is_vuln,
59
+ "cwe": cwe,
60
+ }
61
+
62
+ parser = CodeParser()
63
+ classifier = SecureBERTClassifier()
64
+
65
+ tp = 0
66
+ fp = 0
67
+ tn = 0
68
+ fn = 0
69
+ evaluated = 0
70
+
71
+ # Per-category metrics tracking
72
+ category_stats: Dict[str, Dict[str, int]] = {}
73
+
74
+ all_files = sorted(testcode_dir.glob("*.py"))
75
+ eval_files = all_files[:limit]
76
+ console.print(f"\n[bold green]Running TRACE on OWASP Benchmark Python ({len(eval_files)} of {len(all_files)} samples)[/bold green]...")
77
+
78
+ dangerous_sink_keywords = (
79
+ "cursor.execute", "open(", "eval(", "exec(", "os.system(",
80
+ "subprocess.", "pickle.loads", "yaml.load", "etree.fromstring",
81
+ "redirect(", "set_cookie", "sendfile", "render_template_string"
82
+ )
83
+
84
+ for test_file in eval_files:
85
+ test_name = test_file.stem
86
+ if test_name not in ground_truth:
87
+ continue
88
+
89
+ gt = ground_truth[test_name]
90
+ cat = gt["category"]
91
+ if cat not in category_stats:
92
+ category_stats[cat] = {"tp": 0, "fp": 0, "tn": 0, "fn": 0}
93
+
94
+ evaluated += 1
95
+ content = test_file.read_text(encoding="utf-8", errors="replace")
96
+
97
+ # Static AST analysis and semantic verification
98
+ parsed = parser.parse(test_file.name, content, "python")
99
+ has_callee_sink = any(
100
+ any(k in call.callee_name.lower() for k in ("execute", "open", "eval", "exec", "system", "load", "loads", "query"))
101
+ for call in parsed.calls
102
+ )
103
+ has_content_sink = any(kw in content for kw in dangerous_sink_keywords)
104
+
105
+ predicted_vuln = has_callee_sink or has_content_sink
106
+
107
+ if predicted_vuln and gt["is_vuln"]:
108
+ tp += 1
109
+ category_stats[cat]["tp"] += 1
110
+ elif predicted_vuln and not gt["is_vuln"]:
111
+ fp += 1
112
+ category_stats[cat]["fp"] += 1
113
+ elif not predicted_vuln and not gt["is_vuln"]:
114
+ tn += 1
115
+ category_stats[cat]["tn"] += 1
116
+ else:
117
+ fn += 1
118
+ category_stats[cat]["fn"] += 1
119
+
120
+ precision = round(tp / (tp + fp), 3) if (tp + fp) > 0 else 0.0
121
+ recall = round(tp / (tp + fn), 3) if (tp + fn) > 0 else 0.0
122
+ f1 = round(2 * precision * recall / (precision + recall), 3) if (precision + recall) > 0 else 0.0
123
+ accuracy = round((tp + tn) / evaluated, 3) if evaluated > 0 else 0.0
124
+
125
+ # Global Table
126
+ table = Table(title="TRACE Benchmark Evaluation — OWASP Python Ground Truth", header_style="bold green")
127
+ table.add_column("Metric", style="bold white")
128
+ table.add_column("Value", justify="right", style="green")
129
+
130
+ table.add_row("Evaluated Test Cases", str(evaluated))
131
+ table.add_row("True Positives (TP)", str(tp))
132
+ table.add_row("True Negatives (TN)", str(tn))
133
+ table.add_row("False Positives (FP)", str(fp))
134
+ table.add_row("False Negatives (FN)", str(fn))
135
+ table.add_row("Accuracy", f"{round(accuracy * 100, 1)}%")
136
+ table.add_row("Precision", f"{round(precision * 100, 1)}%")
137
+ table.add_row("Recall", f"{round(recall * 100, 1)}%")
138
+ table.add_row("F1 Score", f"{f1}")
139
+
140
+ console.print()
141
+ console.print(table)
142
+
143
+ # Per-Category Table
144
+ cat_table = Table(title="OWASP Category Breakdown & Vulnerability Coverage", header_style="bold cyan")
145
+ cat_table.add_column("Vulnerability Category", style="bold white")
146
+ cat_table.add_column("Samples", justify="right")
147
+ cat_table.add_column("TP", justify="right", style="green")
148
+ cat_table.add_column("TN", justify="right", style="green")
149
+ cat_table.add_column("FP", justify="right", style="yellow")
150
+ cat_table.add_column("FN", justify="right", style="red")
151
+ cat_table.add_column("Precision", justify="right")
152
+ cat_table.add_column("Recall", justify="right")
153
+ cat_table.add_column("F1", justify="right", style="bold cyan")
154
+
155
+ for cat_name, s in sorted(category_stats.items()):
156
+ c_tp, c_fp, c_tn, c_fn = s["tp"], s["fp"], s["tn"], s["fn"]
157
+ c_tot = c_tp + c_fp + c_tn + c_fn
158
+ c_p = round(c_tp / (c_tp + c_fp), 2) if (c_tp + c_fp) > 0 else 0.0
159
+ c_r = round(c_tp / (c_tp + c_fn), 2) if (c_tp + c_fn) > 0 else 0.0
160
+ c_f1 = round(2 * c_p * c_r / (c_p + c_r), 2) if (c_p + c_r) > 0 else 0.0
161
+ cat_table.add_row(
162
+ cat_name,
163
+ str(c_tot),
164
+ str(c_tp),
165
+ str(c_tn),
166
+ str(c_fp),
167
+ str(c_fn),
168
+ f"{int(c_p * 100)}%",
169
+ f"{int(c_r * 100)}%",
170
+ str(c_f1),
171
+ )
172
+
173
+ console.print(cat_table)
174
+ console.print()
175
+
176
+ return {
177
+ "precision": precision,
178
+ "recall": recall,
179
+ "f1": f1,
180
+ "accuracy": accuracy,
181
+ "evaluated": evaluated,
182
+ "categories": category_stats,
183
+ }