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,283 @@
1
+ """
2
+ TorusGuard Polyglot AST Walker
3
+ Extracts structured primitives (FunctionCall, Assignment, ImportStatement, StringLiteral)
4
+ from Tree-sitter AST nodes and fallback parsers across polyglot languages.
5
+ """
6
+
7
+ from dataclasses import dataclass, field, asdict
8
+ from typing import List, Dict, Any, Optional
9
+
10
+
11
+ @dataclass
12
+ class FunctionCall:
13
+ name: str
14
+ full_call: str
15
+ arguments: List[str]
16
+ line_number: int
17
+ column: int
18
+ receiver: Optional[str] = None
19
+
20
+ def to_dict(self) -> Dict[str, Any]:
21
+ return asdict(self)
22
+
23
+
24
+ @dataclass
25
+ class Assignment:
26
+ target: str
27
+ value_expression: str
28
+ line_number: int
29
+ column: int
30
+
31
+ def to_dict(self) -> Dict[str, Any]:
32
+ return asdict(self)
33
+
34
+
35
+ @dataclass
36
+ class ImportStatement:
37
+ module: str
38
+ imported_names: List[str]
39
+ alias_map: Dict[str, str] = field(default_factory=dict)
40
+ line_number: int = 0
41
+ is_relative: bool = False
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ return asdict(self)
45
+
46
+
47
+ @dataclass
48
+ class StringLiteral:
49
+ value: str
50
+ line_number: int
51
+ column: int
52
+
53
+ def to_dict(self) -> Dict[str, Any]:
54
+ return asdict(self)
55
+
56
+
57
+ class TreeSitterWalker:
58
+ """
59
+ Traverses Tree-sitter parse trees and extracts normalized language constructs.
60
+ """
61
+
62
+ @staticmethod
63
+ def extract_function_calls(root_node, source_bytes: bytes) -> List[FunctionCall]:
64
+ calls: List[FunctionCall] = []
65
+
66
+ def visit(node):
67
+ # Python: call -> function, arguments
68
+ # JS/TS: call_expression -> function, arguments
69
+ # Go: call_expression -> function, argument_list
70
+ if node.type in ("call", "call_expression"):
71
+ func_child = node.child_by_field_name("function")
72
+ args_child = node.child_by_field_name("arguments") or node.child_by_field_name("argument_list")
73
+
74
+ func_name = ""
75
+ receiver = None
76
+ if func_child:
77
+ func_text = source_bytes[func_child.start_byte:func_child.end_byte].decode("utf-8", errors="replace")
78
+ func_name = func_text
79
+ if "." in func_text:
80
+ parts = func_text.split(".")
81
+ receiver = ".".join(parts[:-1])
82
+
83
+ args_list: List[str] = []
84
+ if args_child:
85
+ for ch in args_child.children:
86
+ if ch.type not in (",", "(", ")"):
87
+ arg_text = source_bytes[ch.start_byte:ch.end_byte].decode("utf-8", errors="replace").strip()
88
+ if arg_text:
89
+ args_list.append(arg_text)
90
+
91
+ line_num = node.start_point[0] + 1
92
+ col = node.start_point[1]
93
+ call_text = source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
94
+
95
+ calls.append(FunctionCall(
96
+ name=func_name,
97
+ full_call=call_text,
98
+ arguments=args_list,
99
+ line_number=line_num,
100
+ column=col,
101
+ receiver=receiver
102
+ ))
103
+
104
+ for ch in node.children:
105
+ visit(ch)
106
+
107
+ visit(root_node)
108
+ return calls
109
+
110
+ @staticmethod
111
+ def extract_assignments(root_node, source_bytes: bytes) -> List[Assignment]:
112
+ assignments: List[Assignment] = []
113
+
114
+ def visit(node):
115
+ # Python: assignment -> left, right
116
+ # JS/TS: variable_declarator (name, value), assignment_expression (left, right)
117
+ # Go: short_var_declaration (left, right), assignment_statement (left, right)
118
+ target = ""
119
+ val_expr = ""
120
+ line_num = node.start_point[0] + 1
121
+ col = node.start_point[1]
122
+
123
+ if node.type == "assignment": # Python
124
+ left = node.child_by_field_name("left")
125
+ right = node.child_by_field_name("right")
126
+ if left and right:
127
+ target = source_bytes[left.start_byte:left.end_byte].decode("utf-8", errors="replace").strip()
128
+ val_expr = source_bytes[right.start_byte:right.end_byte].decode("utf-8", errors="replace").strip()
129
+ assignments.append(Assignment(target=target, value_expression=val_expr, line_number=line_num, column=col))
130
+
131
+ elif node.type == "variable_declarator": # JS/TS
132
+ name = node.child_by_field_name("name")
133
+ val = node.child_by_field_name("value")
134
+ if name:
135
+ target = source_bytes[name.start_byte:name.end_byte].decode("utf-8", errors="replace").strip()
136
+ if val:
137
+ val_expr = source_bytes[val.start_byte:val.end_byte].decode("utf-8", errors="replace").strip()
138
+ assignments.append(Assignment(target=target, value_expression=val_expr, line_number=line_num, column=col))
139
+
140
+ elif node.type == "assignment_expression": # JS/TS
141
+ left = node.child_by_field_name("left")
142
+ right = node.child_by_field_name("right")
143
+ if left and right:
144
+ target = source_bytes[left.start_byte:left.end_byte].decode("utf-8", errors="replace").strip()
145
+ val_expr = source_bytes[right.start_byte:right.end_byte].decode("utf-8", errors="replace").strip()
146
+ assignments.append(Assignment(target=target, value_expression=val_expr, line_number=line_num, column=col))
147
+
148
+ elif node.type in ("short_var_declaration", "assignment_statement"): # Go
149
+ left = node.child_by_field_name("left")
150
+ right = node.child_by_field_name("right")
151
+ if left and right:
152
+ target = source_bytes[left.start_byte:left.end_byte].decode("utf-8", errors="replace").strip()
153
+ val_expr = source_bytes[right.start_byte:right.end_byte].decode("utf-8", errors="replace").strip()
154
+ assignments.append(Assignment(target=target, value_expression=val_expr, line_number=line_num, column=col))
155
+
156
+ for ch in node.children:
157
+ visit(ch)
158
+
159
+ visit(root_node)
160
+ return assignments
161
+
162
+ @staticmethod
163
+ def extract_imports(root_node, source_bytes: bytes, language: str) -> List[ImportStatement]:
164
+ imports: List[ImportStatement] = []
165
+
166
+ def visit(node):
167
+ line_num = node.start_point[0] + 1
168
+
169
+ # Python imports
170
+ if node.type == "import_statement":
171
+ raw = source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace").strip()
172
+ # e.g. import os, sys as s
173
+ names = []
174
+ alias_map = {}
175
+ for ch in node.children:
176
+ if ch.type == "dotted_name":
177
+ mod = source_bytes[ch.start_byte:ch.end_byte].decode("utf-8", errors="replace").strip()
178
+ names.append(mod)
179
+ elif ch.type == "aliased_import":
180
+ n = ch.child_by_field_name("name")
181
+ a = ch.child_by_field_name("alias")
182
+ if n and a:
183
+ name_str = source_bytes[n.start_byte:n.end_byte].decode("utf-8", errors="replace").strip()
184
+ alias_str = source_bytes[a.start_byte:a.end_byte].decode("utf-8", errors="replace").strip()
185
+ names.append(name_str)
186
+ alias_map[alias_str] = name_str
187
+ for n in names:
188
+ imports.append(ImportStatement(module=n, imported_names=[n], alias_map=alias_map, line_number=line_num))
189
+
190
+ elif node.type == "import_from_statement":
191
+ # from module import a, b as c
192
+ module_name = ""
193
+ mod_child = node.child_by_field_name("module_name")
194
+ if mod_child:
195
+ module_name = source_bytes[mod_child.start_byte:mod_child.end_byte].decode("utf-8", errors="replace").strip()
196
+ names = []
197
+ alias_map = {}
198
+ for ch in node.children:
199
+ if ch.type == "dotted_name" and ch != mod_child:
200
+ names.append(source_bytes[ch.start_byte:ch.end_byte].decode("utf-8", errors="replace").strip())
201
+ elif ch.type == "aliased_import":
202
+ n = ch.child_by_field_name("name")
203
+ a = ch.child_by_field_name("alias")
204
+ if n and a:
205
+ n_str = source_bytes[n.start_byte:n.end_byte].decode("utf-8", errors="replace").strip()
206
+ a_str = source_bytes[a.start_byte:a.end_byte].decode("utf-8", errors="replace").strip()
207
+ names.append(n_str)
208
+ alias_map[a_str] = n_str
209
+ is_rel = module_name.startswith(".")
210
+ imports.append(ImportStatement(
211
+ module=module_name,
212
+ imported_names=names,
213
+ alias_map=alias_map,
214
+ line_number=line_num,
215
+ is_relative=is_rel
216
+ ))
217
+
218
+ # JS/TS imports: import_statement
219
+ elif node.type == "import_statement":
220
+ raw = source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace").strip()
221
+ source_child = node.child_by_field_name("source")
222
+ source_mod = ""
223
+ if source_child:
224
+ source_mod = source_bytes[source_child.start_byte:source_child.end_byte].decode("utf-8", errors="replace").strip().strip("'\"")
225
+ names = []
226
+ alias_map = {}
227
+ for ch in node.children:
228
+ if ch.type == "import_clause":
229
+ clause_str = source_bytes[ch.start_byte:ch.end_byte].decode("utf-8", errors="replace").strip()
230
+ names.append(clause_str)
231
+ is_rel = source_mod.startswith(".")
232
+ imports.append(ImportStatement(
233
+ module=source_mod,
234
+ imported_names=names,
235
+ alias_map=alias_map,
236
+ line_number=line_num,
237
+ is_relative=is_rel
238
+ ))
239
+
240
+ # Go imports: import_declaration
241
+ elif node.type == "import_declaration":
242
+ for ch in node.children:
243
+ if ch.type in ("import_spec", "import_spec_list"):
244
+ for spec in (ch.children if ch.type == "import_spec_list" else [ch]):
245
+ if spec.type == "import_spec":
246
+ path_child = spec.child_by_field_name("path")
247
+ if path_child:
248
+ path_str = source_bytes[path_child.start_byte:path_child.end_byte].decode("utf-8", errors="replace").strip().strip('"')
249
+ name_child = spec.child_by_field_name("name")
250
+ alias = ""
251
+ if name_child:
252
+ alias = source_bytes[name_child.start_byte:name_child.end_byte].decode("utf-8", errors="replace").strip()
253
+ alias_map = {alias: path_str} if alias else {}
254
+ imports.append(ImportStatement(
255
+ module=path_str,
256
+ imported_names=[path_str],
257
+ alias_map=alias_map,
258
+ line_number=spec.start_point[0] + 1
259
+ ))
260
+
261
+ for ch in node.children:
262
+ visit(ch)
263
+
264
+ visit(root_node)
265
+ return imports
266
+
267
+ @staticmethod
268
+ def extract_string_literals(root_node, source_bytes: bytes) -> List[StringLiteral]:
269
+ literals: List[StringLiteral] = []
270
+
271
+ def visit(node):
272
+ if node.type in ("string", "string_literal", "raw_string_literal", "template_string"):
273
+ text = source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
274
+ literals.append(StringLiteral(
275
+ value=text.strip("\"'`"),
276
+ line_number=node.start_point[0] + 1,
277
+ column=node.start_point[1]
278
+ ))
279
+ for ch in node.children:
280
+ visit(ch)
281
+
282
+ visit(root_node)
283
+ return literals
@@ -0,0 +1,218 @@
1
+ """
2
+ TorusGuard v0.7.0 Authorization & Scope Enforcement Engine
3
+ Enforces explicit target authorization and scope boundaries before any runtime validation.
4
+ No runtime HTTP or browser interaction is permitted without valid, non-expired authorization.
5
+ """
6
+
7
+ import json
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import List, Optional, Dict, Any, Tuple
11
+ from dataclasses import dataclass, asdict
12
+ from urllib.parse import urlparse
13
+
14
+
15
+ class AuthorizationError(PermissionError):
16
+ """Raised when runtime validation is attempted without valid authorization or outside approved scope."""
17
+ pass
18
+
19
+
20
+ @dataclass
21
+ class TargetScope:
22
+ target_hosts: List[str]
23
+ allowed_path_prefixes: List[str]
24
+ forbidden_paths: List[str]
25
+ valid_from: str
26
+ valid_until: str
27
+ max_depth: int = 3
28
+ max_requests: int = 100
29
+ allow_state_changing_methods: bool = False
30
+ allowed_issue_classes: Optional[List[str]] = None
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ return asdict(self)
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: Dict[str, Any]) -> "TargetScope":
37
+ return cls(
38
+ target_hosts=data.get("target_hosts", []),
39
+ allowed_path_prefixes=data.get("allowed_path_prefixes", ["/"]),
40
+ forbidden_paths=data.get("forbidden_paths", ["/admin/delete", "/system/shutdown"]),
41
+ valid_from=data.get("valid_from", ""),
42
+ valid_until=data.get("valid_until", ""),
43
+ max_depth=data.get("max_depth", 3),
44
+ max_requests=data.get("max_requests", 100),
45
+ allow_state_changing_methods=data.get("allow_state_changing_methods", False),
46
+ allowed_issue_classes=data.get("allowed_issue_classes", [
47
+ "auth_bypass",
48
+ "tenant_isolation",
49
+ "header_trust",
50
+ "path_traversal",
51
+ "debug_exposure"
52
+ ])
53
+ )
54
+
55
+
56
+ @dataclass
57
+ class AuthorizationRecord:
58
+ authorization_id: str
59
+ target_name: str
60
+ authorized_by: str
61
+ authorization_type: str # "target_owner", "written_authorization", "ci_sandboxed_test"
62
+ scope: TargetScope
63
+ created_at: str = ""
64
+
65
+ def __post_init__(self):
66
+ if not self.created_at:
67
+ self.created_at = datetime.now(timezone.utc).isoformat()
68
+
69
+ def to_dict(self) -> Dict[str, Any]:
70
+ d = asdict(self)
71
+ d["scope"] = self.scope.to_dict()
72
+ return d
73
+
74
+
75
+ class AuthorizationManager:
76
+ """
77
+ Validates, manages, and stores target authorizations in isolated run folders.
78
+ """
79
+
80
+ @staticmethod
81
+ def is_scope_active(scope: TargetScope) -> Tuple[bool, str]:
82
+ now = datetime.now(timezone.utc)
83
+ if scope.valid_from:
84
+ try:
85
+ v_from = datetime.fromisoformat(scope.valid_from.replace("Z", "+00:00"))
86
+ if now < v_from:
87
+ return False, f"Authorization not yet active (starts {scope.valid_from})"
88
+ except Exception:
89
+ pass
90
+ if scope.valid_until:
91
+ try:
92
+ v_until = datetime.fromisoformat(scope.valid_until.replace("Z", "+00:00"))
93
+ if now > v_until:
94
+ return False, f"Authorization expired at {scope.valid_until}"
95
+ except Exception:
96
+ pass
97
+ return True, "Authorization active"
98
+
99
+ @classmethod
100
+ def _match_host(cls, host: str, target_hosts: List[str]) -> bool:
101
+ """Checks if host matches approved host or host:port patterns."""
102
+ for approved in target_hosts:
103
+ approved_lower = approved.lower()
104
+ if host == approved_lower or host.startswith(approved_lower + ":") or approved_lower.startswith(host + ":"):
105
+ return True
106
+ return False
107
+
108
+ @classmethod
109
+ def _match_forbidden_path(cls, path: str, forbidden_paths: List[str]) -> Optional[str]:
110
+ """Returns matching forbidden path prefix if any, else None."""
111
+ for forbidden in forbidden_paths:
112
+ if path.startswith(forbidden):
113
+ return forbidden
114
+ return None
115
+
116
+ @classmethod
117
+ def _match_allowed_prefix(cls, path: str, allowed_prefixes: List[str]) -> bool:
118
+ """Checks if path matches at least one allowed prefix."""
119
+ for prefix in allowed_prefixes:
120
+ if path.startswith(prefix):
121
+ return True
122
+ return False
123
+
124
+ @classmethod
125
+ def validate_url(cls, url: str, scope: TargetScope) -> Tuple[bool, str]:
126
+ """
127
+ Ensures target URL is strictly inside approved hosts, allowed path prefixes,
128
+ and not in forbidden paths.
129
+ """
130
+ parsed = urlparse(url)
131
+ host = parsed.netloc.lower()
132
+ if not host:
133
+ return False, f"Invalid URL (missing host): {url}"
134
+
135
+ # 1. Match approved host
136
+ if not cls._match_host(host, scope.target_hosts):
137
+ return False, f"Host '{host}' is NOT in approved target_hosts: {scope.target_hosts}"
138
+
139
+ # 2. Check forbidden paths
140
+ path = parsed.path or "/"
141
+ matched_forbidden = cls._match_forbidden_path(path, scope.forbidden_paths)
142
+ if matched_forbidden:
143
+ return False, f"Path '{path}' matches forbidden path '{matched_forbidden}'"
144
+
145
+ # 3. Check allowed path prefixes
146
+ if not cls._match_allowed_prefix(path, scope.allowed_path_prefixes):
147
+ return False, f"Path '{path}' does NOT match any allowed_path_prefixes: {scope.allowed_path_prefixes}"
148
+
149
+ # 4. Check TTL
150
+ active, reason = cls.is_scope_active(scope)
151
+ if not active:
152
+ return False, reason
153
+
154
+ return True, "URL within authorized scope"
155
+
156
+ @classmethod
157
+ def check_authorized_or_raise(cls, url: str, auth: Optional[AuthorizationRecord], method: str = "GET") -> None:
158
+ """
159
+ Hard security gate: raises AuthorizationError if auth is missing, expired,
160
+ or URL violates scope boundaries.
161
+ """
162
+ if not auth:
163
+ raise AuthorizationError("Runtime validation blocked: No authorization record provided.")
164
+
165
+ # Method check
166
+ if method.upper() in ["POST", "PUT", "DELETE", "PATCH"] and not auth.scope.allow_state_changing_methods:
167
+ raise AuthorizationError(f"State-changing method '{method}' is forbidden under current scope.")
168
+
169
+ valid, reason = cls.validate_url(url, auth.scope)
170
+ if not valid:
171
+ raise AuthorizationError(f"Runtime validation blocked: {reason}")
172
+
173
+ @classmethod
174
+ def write_artifacts(cls, run_dir: Path, auth: AuthorizationRecord) -> Tuple[Path, Path]:
175
+ """
176
+ Emits standard scope.json and authorization.md into the run folder.
177
+ """
178
+ run_dir.mkdir(parents=True, exist_ok=True)
179
+ scope_file = run_dir / "scope.json"
180
+ auth_file = run_dir / "authorization.md"
181
+
182
+ # 1. Write scope.json
183
+ with open(scope_file, "w", encoding="utf-8") as f:
184
+ json.dump(auth.to_dict(), f, indent=2)
185
+
186
+ # 2. Write authorization.md
187
+ md_content = f"""# TorusGuard v0.7.0 Runtime Authorization Document
188
+
189
+ **Authorization ID:** `{auth.authorization_id}`
190
+ **Target Application:** `{auth.target_name}`
191
+ **Authorized By:** `{auth.authorized_by}`
192
+ **Authorization Type:** `{auth.authorization_type}`
193
+ **Created At:** `{auth.created_at}`
194
+
195
+ ---
196
+
197
+ ## 🔒 Scope Boundaries & Permitted Constraints
198
+
199
+ | Parameter | Allowed Value |
200
+ |---|---|
201
+ | **Target Hosts** | `{", ".join(auth.scope.target_hosts)}` |
202
+ | **Allowed Path Prefixes** | `{", ".join(auth.scope.allowed_path_prefixes)}` |
203
+ | **Forbidden Paths** | `{", ".join(auth.scope.forbidden_paths)}` |
204
+ | **Active Window** | `{auth.scope.valid_from}` ➔ `{auth.scope.valid_until}` |
205
+ | **Max Navigation Depth** | `{auth.scope.max_depth}` levels |
206
+ | **Max Request Budget** | `{auth.scope.max_requests}` requests |
207
+ | **State-Changing Methods** | `{"Allowed" if auth.scope.allow_state_changing_methods else "Forbidden (Read-only GET/HEAD only)"}` |
208
+ | **Approved Issue Classes** | `{", ".join(auth.scope.allowed_issue_classes or [])}` |
209
+
210
+ ---
211
+
212
+ ## ⚖️ Legal & Governance Statement
213
+ This authorization represents verifiable consent by `{auth.authorized_by}` to perform non-destructive, bounded runtime validation against the specified target hosts. Destructive testing, denial of service, memory corruption, and automated credential stuffing are strictly prohibited.
214
+ """
215
+ with open(auth_file, "w", encoding="utf-8") as f:
216
+ f.write(md_content)
217
+
218
+ return scope_file, auth_file
@@ -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