torusguard 2.0.0-alpha → 2.1.1

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 (227) hide show
  1. package/.torusguard/.manifest.json +94 -30
  2. package/.torusguard/core/__init__.py +146 -0
  3. package/.torusguard/core/agent_roles.py +104 -0
  4. package/.torusguard/core/ast_walker.py +283 -0
  5. package/.torusguard/core/authorization.py +218 -0
  6. package/.torusguard/core/browser_verifier.py +128 -0
  7. package/.torusguard/core/bundle.py +141 -0
  8. package/.torusguard/core/call_graph.py +184 -0
  9. package/.torusguard/core/clustering.py +275 -0
  10. package/.torusguard/core/confidence.py +120 -0
  11. package/.torusguard/core/cross_file_taint.py +101 -0
  12. package/.torusguard/core/exploit_checker.py +317 -0
  13. package/.torusguard/core/formatter.py +351 -0
  14. package/.torusguard/core/governance.py +210 -0
  15. package/.torusguard/core/identity.py +104 -0
  16. package/.torusguard/core/import_resolver.py +91 -0
  17. package/.torusguard/core/incremental.py +102 -0
  18. package/.torusguard/core/lifecycle.py +137 -0
  19. package/.torusguard/core/models.py +425 -0
  20. package/.torusguard/core/parallel.py +56 -0
  21. package/.torusguard/core/parser.py +202 -0
  22. package/.torusguard/core/rechecker.py +107 -0
  23. package/.torusguard/core/replay_trace.py +178 -0
  24. package/.torusguard/core/rules_registry.py +131 -0
  25. package/.torusguard/core/run_folder.py +60 -0
  26. package/.torusguard/core/run_manager.py +163 -0
  27. package/.torusguard/core/runtime_evidence.py +175 -0
  28. package/.torusguard/core/runtime_validator.py +246 -0
  29. package/.torusguard/core/safety_gate.py +139 -0
  30. package/.torusguard/core/sarif.py +189 -0
  31. package/.torusguard/core/stack_profiler.py +184 -0
  32. package/.torusguard/core/symbol_table.py +91 -0
  33. package/.torusguard/core/taint.py +133 -0
  34. package/.torusguard/core/taint_graph.py +235 -0
  35. package/.torusguard/core/taint_rules.py +268 -0
  36. package/.torusguard/core/v070_reporter.py +102 -0
  37. package/.torusguard/core/v070_workflow.py +339 -0
  38. package/.torusguard/core/v6_reporter.py +180 -0
  39. package/.torusguard/core/v6_workflow.py +221 -0
  40. package/.torusguard/core/watcher.py +58 -0
  41. package/.torusguard/rules/TG-INPUT-007-unvalidated-redirect.md +53 -0
  42. package/.torusguard/rules/TG-INPUT-008-insecure-deserialization.md +52 -0
  43. package/.torusguard/rules/container/TG-CONT-001-root-user-execution.md +50 -0
  44. package/.torusguard/rules/container/TG-CONT-002-docker-socket-mount.md +47 -0
  45. package/.torusguard/rules/container/TG-CONT-003-privileged-container-mode.md +53 -0
  46. package/.torusguard/rules/container/TG-CONT-004-build-arg-secret-exposure.md +43 -0
  47. package/.torusguard/rules/git/TG-GIT-001-historical-secret-in-git-commit.md +44 -0
  48. package/.torusguard/rules/git/TG-GIT-002-plaintext-credentials-in-git-config.md +41 -0
  49. package/.torusguard/rules/git/TG-GIT-003-sensitive-tracked-file-gitignore-breach.md +40 -0
  50. package/.torusguard/rules/rag/TG-RAG-001-untrusted-rag-context-injection.md +72 -0
  51. package/.torusguard/rules/rag/TG-RAG-002-autonomous-llm-tool-unsandboxed-call.md +51 -0
  52. package/.torusguard/rules/rag/TG-RAG-003-unpartitioned-vector-tenant-lookup.md +51 -0
  53. package/.torusguard/rules/redos/TG-REDOS-001-catastrophic-exponential-backtracking.md +46 -0
  54. package/.torusguard/rules/redos/TG-REDOS-002-unbounded-nested-quantifier.md +43 -0
  55. package/.torusguard/rules_catalog.json +96 -0
  56. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  57. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  58. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  59. package/.torusguard/scripts/__pycache__/rules_sync.cpython-314.pyc +0 -0
  60. package/.torusguard/scripts/audit_runner.py +108 -10
  61. package/.torusguard/scripts/finding_scorer.py +43 -13
  62. package/.torusguard/scripts/manifest_builder.py +1 -1
  63. package/.torusguard/scripts/skill_profiler.py +26 -0
  64. package/.torusguard/skills/torusguard/SKILL.md +74 -25
  65. package/.torusguard/skills/torusguard/bootstrap.py +57 -24
  66. package/.torusguard/skills/torusguard-ai-guard/SKILL.md +95 -0
  67. package/.torusguard/skills/torusguard-apply/SKILL.md +60 -34
  68. package/.torusguard/skills/torusguard-audit/SKILL.md +131 -57
  69. package/.torusguard/skills/torusguard-authorize/SKILL.md +48 -6
  70. package/.torusguard/skills/torusguard-container/SKILL.md +94 -0
  71. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +50 -6
  72. package/.torusguard/skills/torusguard-full/SKILL.md +62 -19
  73. package/.torusguard/skills/torusguard-git-mine/SKILL.md +92 -0
  74. package/.torusguard/skills/torusguard-harden/SKILL.md +81 -50
  75. package/.torusguard/skills/torusguard-init/SKILL.md +61 -14
  76. package/.torusguard/skills/torusguard-ocr-scan/SKILL.md +94 -0
  77. package/.torusguard/skills/torusguard-recheck/SKILL.md +71 -18
  78. package/.torusguard/skills/torusguard-redos/SKILL.md +91 -0
  79. package/.torusguard/skills/torusguard-report/SKILL.md +50 -9
  80. package/.torusguard/skills/torusguard-status/SKILL.md +63 -10
  81. package/.torusguard/skills/torusguard-verify/SKILL.md +52 -10
  82. package/.torusguard/skills/torusguard-web-validate/SKILL.md +53 -8
  83. package/.torusguard/workflows/ai-guard.md +31 -0
  84. package/.torusguard/workflows/apply.md +32 -55
  85. package/.torusguard/workflows/audit.md +35 -49
  86. package/.torusguard/workflows/authorize.md +27 -50
  87. package/.torusguard/workflows/container.md +29 -0
  88. package/.torusguard/workflows/exploit-check.md +28 -50
  89. package/.torusguard/workflows/git-mine.md +25 -0
  90. package/.torusguard/workflows/harden.md +29 -48
  91. package/.torusguard/workflows/init.md +27 -50
  92. package/.torusguard/workflows/memory.md +18 -23
  93. package/.torusguard/workflows/ocr-scan.md +25 -0
  94. package/.torusguard/workflows/recheck.md +28 -46
  95. package/.torusguard/workflows/redos.md +27 -0
  96. package/.torusguard/workflows/report.md +33 -52
  97. package/.torusguard/workflows/status.md +31 -52
  98. package/.torusguard/workflows/verify.md +29 -49
  99. package/.torusguard/workflows/web-validate.md +22 -45
  100. package/README.md +96 -60
  101. package/package.json +7 -2
  102. package/skills/torusguard/SKILL.md +75 -24
  103. package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
  104. package/skills/torusguard/bootstrap.py +60 -71
  105. package/skills/torusguard/payload/.manifest.json +94 -31
  106. package/skills/torusguard/payload/core/__init__.py +146 -0
  107. package/skills/torusguard/payload/core/agent_roles.py +104 -0
  108. package/skills/torusguard/payload/core/ast_walker.py +283 -0
  109. package/skills/torusguard/payload/core/authorization.py +218 -0
  110. package/skills/torusguard/payload/core/browser_verifier.py +128 -0
  111. package/skills/torusguard/payload/core/bundle.py +141 -0
  112. package/skills/torusguard/payload/core/call_graph.py +184 -0
  113. package/skills/torusguard/payload/core/clustering.py +275 -0
  114. package/skills/torusguard/payload/core/confidence.py +120 -0
  115. package/skills/torusguard/payload/core/cross_file_taint.py +101 -0
  116. package/skills/torusguard/payload/core/exploit_checker.py +317 -0
  117. package/skills/torusguard/payload/core/formatter.py +351 -0
  118. package/skills/torusguard/payload/core/governance.py +210 -0
  119. package/skills/torusguard/payload/core/identity.py +104 -0
  120. package/skills/torusguard/payload/core/import_resolver.py +91 -0
  121. package/skills/torusguard/payload/core/incremental.py +102 -0
  122. package/skills/torusguard/payload/core/lifecycle.py +137 -0
  123. package/skills/torusguard/payload/core/models.py +425 -0
  124. package/skills/torusguard/payload/core/parallel.py +56 -0
  125. package/skills/torusguard/payload/core/parser.py +202 -0
  126. package/skills/torusguard/payload/core/rechecker.py +107 -0
  127. package/skills/torusguard/payload/core/replay_trace.py +178 -0
  128. package/skills/torusguard/payload/core/rules_registry.py +131 -0
  129. package/skills/torusguard/payload/core/run_folder.py +60 -0
  130. package/skills/torusguard/payload/core/run_manager.py +163 -0
  131. package/skills/torusguard/payload/core/runtime_evidence.py +175 -0
  132. package/skills/torusguard/payload/core/runtime_validator.py +246 -0
  133. package/skills/torusguard/payload/core/safety_gate.py +139 -0
  134. package/skills/torusguard/payload/core/sarif.py +189 -0
  135. package/skills/torusguard/payload/core/stack_profiler.py +184 -0
  136. package/skills/torusguard/payload/core/symbol_table.py +91 -0
  137. package/skills/torusguard/payload/core/taint.py +133 -0
  138. package/skills/torusguard/payload/core/taint_graph.py +235 -0
  139. package/skills/torusguard/payload/core/taint_rules.py +268 -0
  140. package/skills/torusguard/payload/core/v070_reporter.py +102 -0
  141. package/skills/torusguard/payload/core/v070_workflow.py +339 -0
  142. package/skills/torusguard/payload/core/v6_reporter.py +180 -0
  143. package/skills/torusguard/payload/core/v6_workflow.py +221 -0
  144. package/skills/torusguard/payload/core/watcher.py +58 -0
  145. package/skills/torusguard/payload/rules/TG-INPUT-007-unvalidated-redirect.md +53 -0
  146. package/skills/torusguard/payload/rules/TG-INPUT-008-insecure-deserialization.md +52 -0
  147. package/skills/torusguard/payload/rules/container/TG-CONT-001-root-user-execution.md +50 -0
  148. package/skills/torusguard/payload/rules/container/TG-CONT-002-docker-socket-mount.md +47 -0
  149. package/skills/torusguard/payload/rules/container/TG-CONT-003-privileged-container-mode.md +53 -0
  150. package/skills/torusguard/payload/rules/container/TG-CONT-004-build-arg-secret-exposure.md +43 -0
  151. package/skills/torusguard/payload/rules/git/TG-GIT-001-historical-secret-in-git-commit.md +44 -0
  152. package/skills/torusguard/payload/rules/git/TG-GIT-002-plaintext-credentials-in-git-config.md +41 -0
  153. package/skills/torusguard/payload/rules/git/TG-GIT-003-sensitive-tracked-file-gitignore-breach.md +40 -0
  154. package/skills/torusguard/payload/rules/rag/TG-RAG-001-untrusted-rag-context-injection.md +72 -0
  155. package/skills/torusguard/payload/rules/rag/TG-RAG-002-autonomous-llm-tool-unsandboxed-call.md +51 -0
  156. package/skills/torusguard/payload/rules/rag/TG-RAG-003-unpartitioned-vector-tenant-lookup.md +51 -0
  157. package/skills/torusguard/payload/rules/redos/TG-REDOS-001-catastrophic-exponential-backtracking.md +46 -0
  158. package/skills/torusguard/payload/rules/redos/TG-REDOS-002-unbounded-nested-quantifier.md +43 -0
  159. package/skills/torusguard/payload/rules_catalog.json +338 -518
  160. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  161. package/skills/torusguard/payload/scripts/audit_runner.py +108 -10
  162. package/skills/torusguard/payload/scripts/finding_scorer.py +43 -13
  163. package/skills/torusguard/payload/scripts/manifest_builder.py +1 -1
  164. package/skills/torusguard/payload/skills/torusguard/SKILL.md +74 -25
  165. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +57 -24
  166. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -41
  167. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -41
  168. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -40
  169. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -25
  170. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -40
  171. package/skills/torusguard/payload/skills/torusguard-ai-guard/SKILL.md +95 -0
  172. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +60 -34
  173. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +131 -57
  174. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +48 -6
  175. package/skills/torusguard/payload/skills/torusguard-container/SKILL.md +94 -0
  176. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +50 -6
  177. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +62 -19
  178. package/skills/torusguard/payload/skills/torusguard-git-mine/SKILL.md +92 -0
  179. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +81 -50
  180. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +61 -14
  181. package/skills/torusguard/payload/skills/torusguard-ocr-scan/SKILL.md +94 -0
  182. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +71 -18
  183. package/skills/torusguard/payload/skills/torusguard-redos/SKILL.md +91 -0
  184. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +50 -9
  185. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +63 -10
  186. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +52 -10
  187. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +53 -8
  188. package/skills/torusguard/payload/workflows/ai-guard.md +31 -0
  189. package/skills/torusguard/payload/workflows/apply.md +31 -62
  190. package/skills/torusguard/payload/workflows/audit.md +35 -55
  191. package/skills/torusguard/payload/workflows/authorize.md +27 -50
  192. package/skills/torusguard/payload/workflows/container.md +29 -0
  193. package/skills/torusguard/payload/workflows/exploit-check.md +28 -50
  194. package/skills/torusguard/payload/workflows/git-mine.md +25 -0
  195. package/skills/torusguard/payload/workflows/harden.md +28 -52
  196. package/skills/torusguard/payload/workflows/init.md +27 -56
  197. package/skills/torusguard/payload/workflows/memory.md +18 -23
  198. package/skills/torusguard/payload/workflows/ocr-scan.md +25 -0
  199. package/skills/torusguard/payload/workflows/recheck.md +28 -46
  200. package/skills/torusguard/payload/workflows/redos.md +27 -0
  201. package/skills/torusguard/payload/workflows/report.md +39 -62
  202. package/skills/torusguard/payload/workflows/status.md +31 -55
  203. package/skills/torusguard/payload/workflows/torusguard-audit.md +35 -55
  204. package/skills/torusguard/payload/workflows/verify.md +29 -49
  205. package/skills/torusguard/payload/workflows/web-validate.md +22 -45
  206. package/skills/torusguard/references/csharp-security.md +41 -0
  207. package/skills/torusguard/references/go-security.md +41 -0
  208. package/skills/torusguard/references/java-security.md +40 -0
  209. package/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  210. package/skills/torusguard/references/rust-security.md +40 -0
  211. package/skills/torusguard-ai-guard/SKILL.md +95 -0
  212. package/skills/torusguard-apply/SKILL.md +60 -34
  213. package/skills/torusguard-audit/SKILL.md +130 -57
  214. package/skills/torusguard-authorize/SKILL.md +48 -6
  215. package/skills/torusguard-container/SKILL.md +94 -0
  216. package/skills/torusguard-exploit-check/SKILL.md +50 -6
  217. package/skills/torusguard-full/SKILL.md +62 -19
  218. package/skills/torusguard-git-mine/SKILL.md +92 -0
  219. package/skills/torusguard-harden/SKILL.md +81 -50
  220. package/skills/torusguard-init/SKILL.md +61 -14
  221. package/skills/torusguard-ocr-scan/SKILL.md +94 -0
  222. package/skills/torusguard-recheck/SKILL.md +71 -18
  223. package/skills/torusguard-redos/SKILL.md +91 -0
  224. package/skills/torusguard-report/SKILL.md +50 -9
  225. package/skills/torusguard-status/SKILL.md +63 -10
  226. package/skills/torusguard-verify/SKILL.md +52 -10
  227. package/skills/torusguard-web-validate/SKILL.md +53 -8
@@ -0,0 +1,128 @@
1
+ """
2
+ TorusGuard v0.7.0 Browser-Assisted Verification Engine
3
+ Validates client-side route guards, authenticated frontend flows, and DOM/UI evidence
4
+ with capped navigation depth, action logging, and safety controls.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Dict, Any, List, Optional
10
+ from dataclasses import dataclass, asdict
11
+
12
+ from core.authorization import AuthorizationRecord, AuthorizationManager, AuthorizationError
13
+ from core.safety_gate import SafetyGate, SafetyReviewLevel
14
+
15
+
16
+ @dataclass
17
+ class BrowserAction:
18
+ action_id: str
19
+ action_type: str # "navigate", "assert_dom", "check_route_guard", "capture_snapshot"
20
+ target_url: str
21
+ selector: Optional[str] = None
22
+ outcome: str = ""
23
+ dom_snippet: Optional[str] = None
24
+ requires_approval: bool = False
25
+ approved: bool = True
26
+
27
+ def to_dict(self) -> Dict[str, Any]:
28
+ return asdict(self)
29
+
30
+
31
+ class BrowserVerifier:
32
+ """
33
+ Simulates and executes bounded browser-assisted validations against client-side flows.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ auth_record: AuthorizationRecord,
39
+ max_depth: int = 3
40
+ ):
41
+ self.auth = auth_record
42
+ self.max_depth = min(max_depth, auth_record.scope.max_depth)
43
+ self.current_depth = 0
44
+ self.action_history: List[BrowserAction] = []
45
+
46
+ def verify_route_guard(
47
+ self,
48
+ target_url: str,
49
+ authenticated: bool,
50
+ expected_redirect_url: Optional[str] = None,
51
+ protected_dom_marker: Optional[str] = None,
52
+ client_dom_content: Optional[str] = None
53
+ ) -> Dict[str, Any]:
54
+ """
55
+ Validates whether client-side router/guards protect sensitive UI components
56
+ or leak them before authentication.
57
+ """
58
+ AuthorizationManager.check_authorized_or_raise(target_url, self.auth, method="GET")
59
+
60
+ action_id = f"BRW-{len(self.action_history) + 1:03d}"
61
+ decision = SafetyGate.evaluate_action(action_id, "GET", target_url)
62
+
63
+ if not decision.allowed_to_proceed:
64
+ action = BrowserAction(
65
+ action_id=action_id,
66
+ action_type="check_route_guard",
67
+ target_url=target_url,
68
+ outcome="BLOCKED_BY_SAFETY_GATE",
69
+ requires_approval=True,
70
+ approved=False
71
+ )
72
+ self.action_history.append(action)
73
+ return {"status": "Blocked by Safety Gate", "action": action.to_dict()}
74
+
75
+ # Evaluate Client-Side DOM Guard
76
+ dom = client_dom_content or ""
77
+ guard_intact = True
78
+ leak_detected = False
79
+
80
+ if not authenticated:
81
+ # When unauthenticated, sensitive DOM marker should NOT be present
82
+ if protected_dom_marker and protected_dom_marker in dom:
83
+ guard_intact = False
84
+ leak_detected = True
85
+ outcome = f"Client Route Guard Failed: Sensitive marker '{protected_dom_marker}' rendered without authentication."
86
+ else:
87
+ outcome = "Client Route Guard Enforced: Protected view withheld from unauthenticated DOM."
88
+ else:
89
+ outcome = "Authenticated Session Verified: Client loaded permitted views."
90
+
91
+ action = BrowserAction(
92
+ action_id=action_id,
93
+ action_type="check_route_guard",
94
+ target_url=target_url,
95
+ outcome=outcome,
96
+ dom_snippet=dom[:200] if dom else None,
97
+ requires_approval=False,
98
+ approved=True
99
+ )
100
+ self.action_history.append(action)
101
+
102
+ return {
103
+ "guard_intact": guard_intact,
104
+ "leak_detected": leak_detected,
105
+ "outcome": outcome,
106
+ "action": action.to_dict()
107
+ }
108
+
109
+ def write_report(self, run_dir: Path) -> Path:
110
+ run_dir.mkdir(parents=True, exist_ok=True)
111
+ report_file = run_dir / "browser-validation.md"
112
+ lines = [
113
+ "# TorusGuard v0.7.0 Browser-Assisted Validation Report",
114
+ f"\n**Authorization ID:** `{self.auth.authorization_id}`",
115
+ f"**Max Navigation Depth:** `{self.max_depth}` levels",
116
+ f"**Total Browser Actions:** `{len(self.action_history)}`",
117
+ "\n---",
118
+ "\n## 🖥️ UI Action Traces\n",
119
+ "| Action ID | Type | Target URL | Outcome | Approval Status |",
120
+ "|---|---|---|---|:---:|",
121
+ ]
122
+ for a in self.action_history:
123
+ lines.append(f"| `{a.action_id}` | `{a.action_type}` | `{a.target_url}` | {a.outcome} | **{'Approved' if a.approved else 'Blocked'}** |")
124
+
125
+ with open(report_file, "w", encoding="utf-8") as f:
126
+ f.write("\n".join(lines) + "\n")
127
+
128
+ return report_file
@@ -0,0 +1,141 @@
1
+ """
2
+ TorusGuard v6 Remediation Bundle Generator
3
+ Constructs self-contained, structured remediation packages per cluster or finding containing:
4
+ - finding.md
5
+ - remediation.md
6
+ - minimal_patch_plan.md
7
+ - verify-after-change.md
8
+ - metadata.json
9
+ """
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from dataclasses import dataclass, field, asdict
14
+ from typing import Dict, Any, List, Optional
15
+
16
+
17
+ @dataclass
18
+ class RemediationBundle:
19
+ bundle_id: str
20
+ finding_id: str
21
+ rule_id: str
22
+ title: str
23
+ target_files: List[str]
24
+ risk_severity: str
25
+ what_is_wrong: str
26
+ why_it_matters: str
27
+ what_should_change: str
28
+ proposed_diff: str
29
+ verification_steps: str
30
+ cluster_id: Optional[str] = None
31
+ created_at: str = ""
32
+
33
+ def write_to_directory(self, base_dir: Path) -> Path:
34
+ bundle_dir = base_dir / self.bundle_id
35
+ bundle_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ # 1. finding.md
38
+ with open(bundle_dir / "finding.md", "w", encoding="utf-8") as f:
39
+ f.write(f"# Finding: {self.title}\n\n")
40
+ f.write(f"- **Finding ID:** `{self.finding_id}`\n")
41
+ f.write(f"- **Rule ID:** `{self.rule_id}`\n")
42
+ f.write(f"- **Severity:** {self.risk_severity}\n")
43
+ if self.cluster_id:
44
+ f.write(f"- **Root-Cause Cluster:** `{self.cluster_id}`\n")
45
+ f.write(f"- **Target Files:** {', '.join(f'`{tf}`' for tf in self.target_files)}\n\n")
46
+ f.write("## What Is Wrong\n")
47
+ f.write(f"{self.what_is_wrong}\n\n")
48
+ f.write("## Why It Matters\n")
49
+ f.write(f"{self.why_it_matters}\n")
50
+
51
+ # 2. remediation.md
52
+ with open(bundle_dir / "remediation.md", "w", encoding="utf-8") as f:
53
+ f.write(f"# Remediation Guide: {self.title}\n\n")
54
+ f.write("## What Should Change\n")
55
+ f.write(f"{self.what_should_change}\n\n")
56
+ f.write("## Target Files to Modify\n")
57
+ for tf in self.target_files:
58
+ f.write(f"- `{tf}`\n")
59
+
60
+ # 3. minimal_patch_plan.md
61
+ with open(bundle_dir / "minimal_patch_plan.md", "w", encoding="utf-8") as f:
62
+ f.write(f"# Minimal Patch Plan for `{self.finding_id}`\n\n")
63
+ f.write("```diff\n")
64
+ f.write(self.proposed_diff.strip() + "\n")
65
+ f.write("```\n")
66
+
67
+ # 4. verify-after-change.md
68
+ with open(bundle_dir / "verify-after-change.md", "w", encoding="utf-8") as f:
69
+ f.write(f"# Verification Steps for `{self.finding_id}`\n\n")
70
+ f.write(f"{self.verification_steps}\n\n")
71
+ f.write("### Recheck Command\n")
72
+ f.write("```bash\n/torusguard recheck\n```\n")
73
+
74
+ # 5. metadata.json
75
+ meta = {
76
+ "bundle_id": self.bundle_id,
77
+ "finding_id": self.finding_id,
78
+ "rule_id": self.rule_id,
79
+ "title": self.title,
80
+ "cluster_id": self.cluster_id,
81
+ "target_files": self.target_files,
82
+ "risk_severity": self.risk_severity,
83
+ "created_at": self.created_at,
84
+ }
85
+ with open(bundle_dir / "metadata.json", "w", encoding="utf-8") as f:
86
+ json.dump(meta, f, indent=2)
87
+
88
+ return bundle_dir
89
+
90
+
91
+ class BundleManager:
92
+ """
93
+ Factory for generating remediation bundles from findings and clusters.
94
+ """
95
+
96
+ @staticmethod
97
+ def create_bundle(finding: Dict[str, Any], cluster_id: Optional[str] = None) -> RemediationBundle:
98
+ f_id = finding.get("finding_id", "fnd-01")
99
+ rule_id = finding.get("rule_id", "TG-GENERIC")
100
+ title = finding.get("title", "Security Finding")
101
+ target = finding.get("target", {})
102
+ file_path = target.get("file_path", "app.py")
103
+ severity = finding.get("severity", "High")
104
+
105
+ bundle_id = f"bundle-{f_id}"
106
+
107
+ what_is_wrong = finding.get(
108
+ "what_is_wrong",
109
+ f"The component in `{file_path}` violates security rule `{rule_id}`."
110
+ )
111
+ why_it_matters = finding.get(
112
+ "why_it_matters",
113
+ "This vulnerability could allow attackers to bypass authorization, access unauthorized tenant data, or inject malicious payloads."
114
+ )
115
+ what_should_change = finding.get(
116
+ "what_should_change",
117
+ f"Apply least-invasive framework-idiomatic hardening to `{file_path}`."
118
+ )
119
+ proposed_diff = finding.get(
120
+ "proposed_diff",
121
+ f"--- a/{file_path}\n+++ b/{file_path}\n@@ -1,3 +1,3 @@\n-# Unsafe pattern\n+# Hardened pattern\n"
122
+ )
123
+ verification_steps = finding.get(
124
+ "verification_steps",
125
+ f"Run `/torusguard recheck` on `{file_path}` to confirm vulnerability resolution."
126
+ )
127
+
128
+ return RemediationBundle(
129
+ bundle_id=bundle_id,
130
+ finding_id=f_id,
131
+ rule_id=rule_id,
132
+ title=title,
133
+ target_files=[file_path],
134
+ risk_severity=severity,
135
+ what_is_wrong=what_is_wrong,
136
+ why_it_matters=why_it_matters,
137
+ what_should_change=what_should_change,
138
+ proposed_diff=proposed_diff,
139
+ verification_steps=verification_steps,
140
+ cluster_id=cluster_id,
141
+ )
@@ -0,0 +1,184 @@
1
+ """
2
+ TorusGuard Project-Wide Call Graph Engine
3
+ Maps interprocedural call sites, function definitions, and caller/callee relationships
4
+ across module boundaries.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from dataclasses import dataclass, field, asdict
9
+ from typing import Dict, List, Optional, Set, Any
10
+ import re
11
+
12
+ from core.parser import PolyglotParser, ParseResult
13
+ from core.import_resolver import ImportResolver
14
+
15
+
16
+ @dataclass
17
+ class CallSite:
18
+ caller_file: str
19
+ caller_func: str
20
+ caller_line: int
21
+ callee_name: str
22
+ callee_file: Optional[str] = None
23
+ callee_line: Optional[int] = None
24
+ arguments: List[str] = field(default_factory=list)
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ return asdict(self)
28
+
29
+
30
+ @dataclass
31
+ class FunctionDef:
32
+ file_path: str
33
+ func_name: str
34
+ start_line: int
35
+ end_line: int = 0
36
+ parameters: List[str] = field(default_factory=list)
37
+ return_expressions: List[str] = field(default_factory=list)
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ return asdict(self)
41
+
42
+
43
+ class CallGraph:
44
+ """Project-wide interprocedural function call graph."""
45
+
46
+ def __init__(self, project_root: Path):
47
+ self.project_root = project_root.resolve()
48
+ self.import_resolver = ImportResolver(self.project_root)
49
+ # Key: (file_path, func_name) -> FunctionDef
50
+ self.functions: Dict[Tuple[str, str], FunctionDef] = {}
51
+ # Key: func_name -> list of FunctionDef (for quick symbol lookup)
52
+ self.functions_by_name: Dict[str, List[FunctionDef]] = {}
53
+ # Key: (file_path, func_name) -> list of CallSite where this func is caller
54
+ self.callees: Dict[Tuple[str, str], List[CallSite]] = {}
55
+ # Key: (file_path, func_name) -> list of CallSite where this func is callee
56
+ self.callers: Dict[Tuple[str, str], List[CallSite]] = {}
57
+
58
+ def register_function(self, fdef: FunctionDef) -> None:
59
+ key = (fdef.file_path, fdef.func_name)
60
+ self.functions[key] = fdef
61
+ if fdef.func_name not in self.functions_by_name:
62
+ self.functions_by_name[fdef.func_name] = []
63
+ self.functions_by_name[fdef.func_name].append(fdef)
64
+
65
+ def add_call(self, call: CallSite) -> None:
66
+ caller_key = (call.caller_file, call.caller_func)
67
+ if caller_key not in self.callees:
68
+ self.callees[caller_key] = []
69
+ self.callees[caller_key].append(call)
70
+
71
+ if call.callee_file:
72
+ callee_key = (call.callee_file, call.callee_name)
73
+ if callee_key not in self.callers:
74
+ self.callers[callee_key] = []
75
+ self.callers[callee_key].append(call)
76
+
77
+ def build_from_files(self, file_paths: List[Path], parser: PolyglotParser) -> None:
78
+ """Parse files and construct functions and call edges."""
79
+ parsed_results: Dict[str, ParseResult] = {}
80
+
81
+ # Pass 1: Extract all function definitions
82
+ for p in file_paths:
83
+ try:
84
+ res = parser.parse_file(p)
85
+ rel_p = str(p.resolve().relative_to(self.project_root)).replace("\\", "/")
86
+ parsed_results[rel_p] = res
87
+ self._extract_function_defs(rel_p, res.raw_content, res.language)
88
+ except Exception:
89
+ continue
90
+
91
+ # Pass 2: Extract call sites and link to callees
92
+ for rel_p, res in parsed_results.items():
93
+ abs_p = self.project_root / rel_p
94
+ # Build import map: alias/name -> resolved Path
95
+ resolved_imports: Dict[str, str] = {}
96
+ for imp in res.imports:
97
+ target_p = self.import_resolver.resolve(abs_p, imp.module)
98
+ if target_p:
99
+ rel_target = str(target_p.resolve().relative_to(self.project_root)).replace("\\", "/")
100
+ for name in imp.imported_names:
101
+ resolved_imports[name] = rel_target
102
+ for alias, orig in imp.alias_map.items():
103
+ resolved_imports[alias] = rel_target
104
+
105
+ # Process function calls
106
+ for fc in res.function_calls:
107
+ callee_name = fc.name
108
+ callee_file = None
109
+ callee_line = None
110
+
111
+ # Method call e.g. mod.func
112
+ if "." in callee_name:
113
+ prefix, actual_name = callee_name.split(".", 1)
114
+ if prefix in resolved_imports:
115
+ callee_file = resolved_imports[prefix]
116
+ callee_name = actual_name
117
+ elif callee_name in resolved_imports:
118
+ callee_file = resolved_imports[callee_name]
119
+ else:
120
+ # Check local in same file
121
+ if (rel_p, callee_name) in self.functions:
122
+ callee_file = rel_p
123
+
124
+ # Find which enclosing function contains this call
125
+ caller_func = self._find_enclosing_function(rel_p, fc.line_number)
126
+
127
+ call_site = CallSite(
128
+ caller_file=rel_p,
129
+ caller_func=caller_func,
130
+ caller_line=fc.line_number,
131
+ callee_name=callee_name,
132
+ callee_file=callee_file,
133
+ arguments=fc.arguments
134
+ )
135
+ self.add_call(call_site)
136
+
137
+ def _extract_function_defs(self, file_path: str, content: str, language: str) -> None:
138
+ lines = content.splitlines()
139
+ for idx, line in enumerate(lines):
140
+ line_num = idx + 1
141
+ stripped = line.strip()
142
+
143
+ # Python: def foo(a, b):
144
+ py_m = re.match(r"^def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*?)\)", stripped)
145
+ if py_m:
146
+ fname = py_m.group(1)
147
+ params = [p.strip() for p in py_m.group(2).split(",") if p.strip()]
148
+ self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
149
+ continue
150
+
151
+ # JS/TS: function foo(a, b) or const foo = (a, b) =>
152
+ js_m = re.match(r"^(?:async\s+)?function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*?)\)", stripped)
153
+ if js_m:
154
+ fname = js_m.group(1)
155
+ params = [p.strip() for p in js_m.group(2).split(",") if p.strip()]
156
+ self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
157
+ continue
158
+
159
+ arrow_m = re.match(r"^(?:const|let|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\((.*?)\)\s*=>", stripped)
160
+ if arrow_m:
161
+ fname = arrow_m.group(1)
162
+ params = [p.strip() for p in arrow_m.group(2).split(",") if p.strip()]
163
+ self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
164
+ continue
165
+
166
+ # Go: func foo(a string)
167
+ go_m = re.match(r"^func\s+(?:\(.*?\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*?)\)", stripped)
168
+ if go_m:
169
+ fname = go_m.group(1)
170
+ params = [p.strip() for p in go_m.group(2).split(",") if p.strip()]
171
+ self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
172
+
173
+ def _find_enclosing_function(self, file_path: str, line_num: int) -> str:
174
+ candidates = [f for (fp, fn), f in self.functions.items() if fp == file_path and f.start_line <= line_num]
175
+ if not candidates:
176
+ return "<global>"
177
+ candidates.sort(key=lambda f: f.start_line, reverse=True)
178
+ return candidates[0].func_name
179
+
180
+ def get_callers(self, file_path: str, func_name: str) -> List[CallSite]:
181
+ return self.callers.get((file_path, func_name), [])
182
+
183
+ def get_callees(self, file_path: str, func_name: str) -> List[CallSite]:
184
+ return self.callees.get((file_path, func_name), [])