torusguard 2.1.0 → 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 (133) hide show
  1. package/.torusguard/.manifest.json +47 -5
  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/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  44. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  45. package/.torusguard/scripts/__pycache__/rules_sync.cpython-314.pyc +0 -0
  46. package/.torusguard/scripts/audit_runner.py +108 -10
  47. package/.torusguard/scripts/finding_scorer.py +43 -13
  48. package/.torusguard/scripts/skill_profiler.py +26 -0
  49. package/.torusguard/skills/torusguard/SKILL.md +6 -2
  50. package/.torusguard/skills/torusguard-audit/SKILL.md +109 -84
  51. package/.torusguard/workflows/audit.md +21 -17
  52. package/README.md +19 -11
  53. package/package.json +7 -2
  54. package/skills/torusguard/SKILL.md +6 -2
  55. package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
  56. package/skills/torusguard/bootstrap.py +3 -3
  57. package/skills/torusguard/payload/.manifest.json +48 -7
  58. package/skills/torusguard/payload/core/__init__.py +146 -0
  59. package/skills/torusguard/payload/core/agent_roles.py +104 -0
  60. package/skills/torusguard/payload/core/ast_walker.py +283 -0
  61. package/skills/torusguard/payload/core/authorization.py +218 -0
  62. package/skills/torusguard/payload/core/browser_verifier.py +128 -0
  63. package/skills/torusguard/payload/core/bundle.py +141 -0
  64. package/skills/torusguard/payload/core/call_graph.py +184 -0
  65. package/skills/torusguard/payload/core/clustering.py +275 -0
  66. package/skills/torusguard/payload/core/confidence.py +120 -0
  67. package/skills/torusguard/payload/core/cross_file_taint.py +101 -0
  68. package/skills/torusguard/payload/core/exploit_checker.py +317 -0
  69. package/skills/torusguard/payload/core/formatter.py +351 -0
  70. package/skills/torusguard/payload/core/governance.py +210 -0
  71. package/skills/torusguard/payload/core/identity.py +104 -0
  72. package/skills/torusguard/payload/core/import_resolver.py +91 -0
  73. package/skills/torusguard/payload/core/incremental.py +102 -0
  74. package/skills/torusguard/payload/core/lifecycle.py +137 -0
  75. package/skills/torusguard/payload/core/models.py +425 -0
  76. package/skills/torusguard/payload/core/parallel.py +56 -0
  77. package/skills/torusguard/payload/core/parser.py +202 -0
  78. package/skills/torusguard/payload/core/rechecker.py +107 -0
  79. package/skills/torusguard/payload/core/replay_trace.py +178 -0
  80. package/skills/torusguard/payload/core/rules_registry.py +131 -0
  81. package/skills/torusguard/payload/core/run_folder.py +60 -0
  82. package/skills/torusguard/payload/core/run_manager.py +163 -0
  83. package/skills/torusguard/payload/core/runtime_evidence.py +175 -0
  84. package/skills/torusguard/payload/core/runtime_validator.py +246 -0
  85. package/skills/torusguard/payload/core/safety_gate.py +139 -0
  86. package/skills/torusguard/payload/core/sarif.py +189 -0
  87. package/skills/torusguard/payload/core/stack_profiler.py +184 -0
  88. package/skills/torusguard/payload/core/symbol_table.py +91 -0
  89. package/skills/torusguard/payload/core/taint.py +133 -0
  90. package/skills/torusguard/payload/core/taint_graph.py +235 -0
  91. package/skills/torusguard/payload/core/taint_rules.py +268 -0
  92. package/skills/torusguard/payload/core/v070_reporter.py +102 -0
  93. package/skills/torusguard/payload/core/v070_workflow.py +339 -0
  94. package/skills/torusguard/payload/core/v6_reporter.py +180 -0
  95. package/skills/torusguard/payload/core/v6_workflow.py +221 -0
  96. package/skills/torusguard/payload/core/watcher.py +58 -0
  97. package/skills/torusguard/payload/rules/TG-INPUT-007-unvalidated-redirect.md +53 -0
  98. package/skills/torusguard/payload/rules/TG-INPUT-008-insecure-deserialization.md +52 -0
  99. package/skills/torusguard/payload/rules/container/TG-CONT-001-root-user-execution.md +50 -50
  100. package/skills/torusguard/payload/rules/container/TG-CONT-002-docker-socket-mount.md +47 -47
  101. package/skills/torusguard/payload/rules/container/TG-CONT-003-privileged-container-mode.md +53 -53
  102. package/skills/torusguard/payload/rules/container/TG-CONT-004-build-arg-secret-exposure.md +43 -43
  103. package/skills/torusguard/payload/rules/git/TG-GIT-001-historical-secret-in-git-commit.md +44 -44
  104. package/skills/torusguard/payload/rules/git/TG-GIT-002-plaintext-credentials-in-git-config.md +41 -41
  105. package/skills/torusguard/payload/rules/git/TG-GIT-003-sensitive-tracked-file-gitignore-breach.md +40 -40
  106. package/skills/torusguard/payload/rules/rag/TG-RAG-001-untrusted-rag-context-injection.md +72 -72
  107. package/skills/torusguard/payload/rules/rag/TG-RAG-002-autonomous-llm-tool-unsandboxed-call.md +51 -51
  108. package/skills/torusguard/payload/rules/rag/TG-RAG-003-unpartitioned-vector-tenant-lookup.md +51 -51
  109. package/skills/torusguard/payload/rules/redos/TG-REDOS-001-catastrophic-exponential-backtracking.md +46 -46
  110. package/skills/torusguard/payload/rules/redos/TG-REDOS-002-unbounded-nested-quantifier.md +43 -43
  111. package/skills/torusguard/payload/scripts/audit_runner.py +108 -10
  112. package/skills/torusguard/payload/scripts/finding_scorer.py +43 -13
  113. package/skills/torusguard/payload/skills/torusguard/SKILL.md +6 -2
  114. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +3 -3
  115. package/skills/torusguard/payload/skills/torusguard-ai-guard/SKILL.md +95 -95
  116. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +109 -84
  117. package/skills/torusguard/payload/skills/torusguard-container/SKILL.md +94 -94
  118. package/skills/torusguard/payload/skills/torusguard-git-mine/SKILL.md +92 -92
  119. package/skills/torusguard/payload/skills/torusguard-ocr-scan/SKILL.md +94 -94
  120. package/skills/torusguard/payload/skills/torusguard-redos/SKILL.md +91 -91
  121. package/skills/torusguard/payload/workflows/ai-guard.md +31 -31
  122. package/skills/torusguard/payload/workflows/audit.md +21 -17
  123. package/skills/torusguard/payload/workflows/container.md +29 -29
  124. package/skills/torusguard/payload/workflows/git-mine.md +25 -25
  125. package/skills/torusguard/payload/workflows/ocr-scan.md +25 -25
  126. package/skills/torusguard/payload/workflows/redos.md +27 -27
  127. package/skills/torusguard/payload/workflows/torusguard-audit.md +35 -55
  128. package/skills/torusguard/references/csharp-security.md +41 -41
  129. package/skills/torusguard/references/go-security.md +41 -41
  130. package/skills/torusguard/references/java-security.md +40 -40
  131. package/skills/torusguard/references/polyglot-security-matrix.md +25 -25
  132. package/skills/torusguard/references/rust-security.md +40 -40
  133. package/skills/torusguard-audit/SKILL.md +107 -83
@@ -0,0 +1,175 @@
1
+ """
2
+ TorusGuard v0.7.0 Runtime Evidence Model & Secret Redaction Engine
3
+ Captures reproducible HTTP & browser validation evidence while automatically
4
+ redacting credentials, JWTs, API keys, and sensitive payload parameters.
5
+ """
6
+
7
+ import re
8
+ import json
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Dict, Any, List, Optional
12
+ from dataclasses import dataclass, asdict
13
+
14
+
15
+ class RedactionEngine:
16
+ """
17
+ Sanitizes authorization headers, cookies, tokens, and secret parameters.
18
+ """
19
+
20
+ PATTERNS = [
21
+ (re.compile(r"Bearer\s+([A-Za-z0-9_\-\.]+)", re.IGNORECASE), "Bearer [REDACTED_TOKEN]"),
22
+ (re.compile(r"Basic\s+([A-Za-z0-9+/=]+)", re.IGNORECASE), "Basic [REDACTED_CREDS]"),
23
+ (re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+"), "[REDACTED_JWT]"),
24
+ (re.compile(r"sk_live_[0-9a-zA-Z]{24,}"), "sk_live_[REDACTED_KEY]"),
25
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "AKIA[REDACTED_AWS_KEY]"),
26
+ (re.compile(r"([\"']?password[\"']?\s*[:=]\s*[\"'])([^\"']+)([\"'])", re.IGNORECASE), r"\1[REDACTED_PASSWORD]\3"),
27
+ (re.compile(r"([\"']?secret[\"']?\s*[:=]\s*[\"'])([^\"']+)([\"'])", re.IGNORECASE), r"\1[REDACTED_SECRET]\3"),
28
+ ]
29
+
30
+ @classmethod
31
+ def redact_text(cls, text: Optional[str]) -> str:
32
+ if not text:
33
+ return ""
34
+ redacted = text
35
+ for pattern, repl in cls.PATTERNS:
36
+ redacted = pattern.sub(repl, redacted)
37
+ return redacted
38
+
39
+ @classmethod
40
+ def redact_headers(cls, headers: Optional[Dict[str, str]]) -> Dict[str, str]:
41
+ if not headers:
42
+ return {}
43
+ sanitized = {}
44
+ sensitive_headers = {"authorization", "cookie", "set-cookie", "x-api-key", "proxy-authorization"}
45
+ for k, v in headers.items():
46
+ if k.lower() in sensitive_headers:
47
+ sanitized[k] = cls.redact_text(str(v))
48
+ else:
49
+ sanitized[k] = str(v)
50
+ return sanitized
51
+
52
+
53
+ @dataclass
54
+ class RuntimeEvidenceItem:
55
+ evidence_id: str
56
+ finding_id: str
57
+ cluster_id: str
58
+ timestamp: str
59
+ method: str
60
+ url: str
61
+ path: str
62
+ status_code: int
63
+ request_headers: Dict[str, str]
64
+ response_headers: Dict[str, str]
65
+ request_body_redacted: Optional[str]
66
+ response_body_snippet: str
67
+ exploitability_status: str
68
+ reproducible: bool = True
69
+ route_context: Optional[str] = None
70
+ reviewer_notes: Optional[str] = None
71
+ screenshot_path: Optional[str] = None
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ return asdict(self)
75
+
76
+
77
+ class EvidenceCollector:
78
+ """
79
+ Collects runtime HTTP request/response evidence and writes sanitized artifacts.
80
+ """
81
+
82
+ def __init__(self):
83
+ self.evidence_items: List[RuntimeEvidenceItem] = []
84
+ self.requests_log: List[Dict[str, Any]] = []
85
+ self.responses_log: List[Dict[str, Any]] = []
86
+
87
+ def record_interaction(
88
+ self,
89
+ finding_id: str,
90
+ cluster_id: str,
91
+ method: str,
92
+ url: str,
93
+ path: str,
94
+ status_code: int,
95
+ req_headers: Dict[str, str],
96
+ resp_headers: Dict[str, str],
97
+ req_body: Optional[str],
98
+ resp_body: Optional[str],
99
+ exploitability_status: str,
100
+ route_context: Optional[str] = None,
101
+ reviewer_notes: Optional[str] = None,
102
+ screenshot_path: Optional[str] = None,
103
+ ) -> RuntimeEvidenceItem:
104
+ now = datetime.now(timezone.utc).isoformat()
105
+ ev_id = f"EV-RT-{len(self.evidence_items) + 1:04d}"
106
+
107
+ sanitized_req_headers = RedactionEngine.redact_headers(req_headers)
108
+ sanitized_resp_headers = RedactionEngine.redact_headers(resp_headers)
109
+ sanitized_req_body = RedactionEngine.redact_text(req_body) if req_body else None
110
+ snippet = RedactionEngine.redact_text((resp_body or "")[:500])
111
+
112
+ item = RuntimeEvidenceItem(
113
+ evidence_id=ev_id,
114
+ finding_id=finding_id,
115
+ cluster_id=cluster_id,
116
+ timestamp=now,
117
+ method=method.upper(),
118
+ url=url,
119
+ path=path,
120
+ status_code=status_code,
121
+ request_headers=sanitized_req_headers,
122
+ response_headers=sanitized_resp_headers,
123
+ request_body_redacted=sanitized_req_body,
124
+ response_body_snippet=snippet,
125
+ exploitability_status=exploitability_status,
126
+ reproducible=True,
127
+ route_context=route_context,
128
+ reviewer_notes=reviewer_notes,
129
+ screenshot_path=screenshot_path,
130
+ )
131
+
132
+ self.evidence_items.append(item)
133
+
134
+ # Append to logs
135
+ self.requests_log.append({
136
+ "evidence_id": ev_id,
137
+ "timestamp": now,
138
+ "method": method.upper(),
139
+ "url": url,
140
+ "headers": sanitized_req_headers,
141
+ "body": sanitized_req_body,
142
+ })
143
+ self.responses_log.append({
144
+ "evidence_id": ev_id,
145
+ "timestamp": now,
146
+ "status_code": status_code,
147
+ "headers": sanitized_resp_headers,
148
+ "body_snippet": snippet,
149
+ })
150
+
151
+ return item
152
+
153
+ def write_artifacts(self, run_dir: Path) -> Dict[str, Path]:
154
+ """
155
+ Emits requests.json, responses.json, and runtime-evidence.json.
156
+ """
157
+ run_dir.mkdir(parents=True, exist_ok=True)
158
+ req_file = run_dir / "requests.json"
159
+ resp_file = run_dir / "responses.json"
160
+ ev_file = run_dir / "runtime-evidence.json"
161
+
162
+ with open(req_file, "w", encoding="utf-8") as f:
163
+ json.dump(self.requests_log, f, indent=2)
164
+
165
+ with open(resp_file, "w", encoding="utf-8") as f:
166
+ json.dump(self.responses_log, f, indent=2)
167
+
168
+ with open(ev_file, "w", encoding="utf-8") as f:
169
+ json.dump([item.to_dict() for item in self.evidence_items], f, indent=2)
170
+
171
+ return {
172
+ "requests": req_file,
173
+ "responses": resp_file,
174
+ "evidence": ev_file,
175
+ }
@@ -0,0 +1,246 @@
1
+ """
2
+ TorusGuard v0.7.0 Web Validation Engine
3
+ Executes authorized HTTP probes against running applications, captures requests/responses,
4
+ maintains session state across endpoints, and strictly abides by scope & safety gates.
5
+ """
6
+
7
+ import json
8
+ import urllib.request
9
+ import urllib.error
10
+ from urllib.parse import urljoin, urlparse
11
+ from pathlib import Path
12
+ from typing import Dict, Any, List, Optional, Tuple
13
+ from dataclasses import dataclass, field
14
+
15
+ from core.authorization import AuthorizationRecord, AuthorizationManager, AuthorizationError
16
+ from core.safety_gate import SafetyGate, SafetyDecision, SafetyReviewLevel
17
+ from core.runtime_evidence import EvidenceCollector, RuntimeEvidenceItem
18
+
19
+
20
+ @dataclass
21
+ class SessionState:
22
+ session_id: str
23
+ cookies: Dict[str, str] = field(default_factory=dict)
24
+ custom_headers: Dict[str, str] = field(default_factory=dict)
25
+ notes: List[str] = field(default_factory=list)
26
+
27
+ def add_cookie(self, name: str, value: str):
28
+ self.cookies[name] = value
29
+
30
+ def add_header(self, name: str, value: str):
31
+ self.custom_headers[name] = value
32
+
33
+ def log_note(self, note: str):
34
+ self.notes.append(note)
35
+
36
+ def get_cookie_header(self) -> Optional[str]:
37
+ if not self.cookies:
38
+ return None
39
+ return "; ".join(f"{k}={v}" for k, v in self.cookies.items())
40
+
41
+
42
+ class WebValidator:
43
+ """
44
+ Executes scoped and authorized HTTP interactions against target applications.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ auth_record: AuthorizationRecord,
50
+ evidence_collector: Optional[EvidenceCollector] = None,
51
+ timeout_seconds: int = 5,
52
+ max_requests: int = 100
53
+ ):
54
+ self.auth = auth_record
55
+ self.evidence_collector = evidence_collector or EvidenceCollector()
56
+ self.timeout = timeout_seconds
57
+ self.max_requests = max_requests
58
+ self.request_count = 0
59
+ self.session = SessionState(session_id="session-default")
60
+ self.validation_logs: List[Dict[str, Any]] = []
61
+
62
+ def execute_probe(
63
+ self,
64
+ finding_id: str,
65
+ cluster_id: str,
66
+ method: str,
67
+ target_url: str,
68
+ headers: Optional[Dict[str, str]] = None,
69
+ body: Optional[str] = None,
70
+ expected_status: Optional[int] = None,
71
+ pre_approved: bool = False,
72
+ route_context: Optional[str] = None,
73
+ ) -> Tuple[int, Dict[str, str], str, SafetyDecision]:
74
+ """
75
+ Validates scope and safety before dispatching an HTTP probe.
76
+ """
77
+ # 1. Authorization check
78
+ AuthorizationManager.check_authorized_or_raise(target_url, self.auth, method=method)
79
+
80
+ # 2. Budget check
81
+ if self.request_count >= self.max_requests:
82
+ raise AuthorizationError(f"Request budget exceeded (max {self.max_requests} requests reached).")
83
+ self.request_count += 1
84
+
85
+ # 3. Safety Gate Evaluation
86
+ parsed = urlparse(target_url)
87
+ path = parsed.path or "/"
88
+ action_id = f"ACT-{self.request_count:03d}"
89
+ decision = SafetyGate.evaluate_action(action_id, method, path, context=route_context, pre_approved=pre_approved)
90
+
91
+ if not decision.allowed_to_proceed:
92
+ # Action blocked by safety gate
93
+ self.session.log_note(f"Probe to {method} {target_url} blocked by Safety Gate ({decision.review_level}).")
94
+ self.validation_logs.append({
95
+ "action_id": action_id,
96
+ "status": "BLOCKED",
97
+ "review_level": decision.review_level,
98
+ "rationale": decision.rationale,
99
+ "url": target_url,
100
+ })
101
+ return 0, {}, "BLOCKED_BY_SAFETY_GATE", decision
102
+
103
+ # 4. Prepare Headers (inject session cookies and custom headers)
104
+ merged_headers = self._prepare_request_headers(headers)
105
+
106
+ # 5. Dispatch HTTP Request
107
+ status_code, resp_headers, resp_body = self._dispatch_http(
108
+ method=method,
109
+ target_url=target_url,
110
+ headers=merged_headers,
111
+ body=body
112
+ )
113
+
114
+ # 6. Record interaction in Evidence Collector
115
+ exploit_status = "Runtime Confirmed" if (expected_status and status_code == expected_status) else "Runtime Likely"
116
+ self.evidence_collector.record_interaction(
117
+ finding_id=finding_id,
118
+ cluster_id=cluster_id,
119
+ method=method,
120
+ url=target_url,
121
+ path=path,
122
+ status_code=status_code,
123
+ req_headers=merged_headers,
124
+ resp_headers=resp_headers,
125
+ req_body=body,
126
+ resp_body=resp_body,
127
+ exploitability_status=exploit_status,
128
+ route_context=route_context,
129
+ reviewer_notes=f"HTTP {status_code} received from {path}"
130
+ )
131
+
132
+ self.validation_logs.append({
133
+ "action_id": action_id,
134
+ "status": "COMPLETED",
135
+ "method": method.upper(),
136
+ "url": target_url,
137
+ "status_code": status_code,
138
+ "review_level": decision.review_level
139
+ })
140
+
141
+ return status_code, resp_headers, resp_body, decision
142
+
143
+ def _prepare_request_headers(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
144
+ """Merges session headers, cookies, and transparent TorusGuard audit headers."""
145
+ merged_headers = dict(self.session.custom_headers)
146
+ if headers:
147
+ merged_headers.update(headers)
148
+
149
+ cookie_str = self.session.get_cookie_header()
150
+ if cookie_str and "Cookie" not in merged_headers:
151
+ merged_headers["Cookie"] = cookie_str
152
+
153
+ merged_headers["User-Agent"] = "TorusGuard-RuntimeValidator/0.7.0 (Authorized Audit)"
154
+ merged_headers["X-TorusGuard-AuthID"] = self.auth.authorization_id
155
+ return merged_headers
156
+
157
+ def _dispatch_http(
158
+ self, method: str, target_url: str, headers: Dict[str, str], body: Optional[str] = None
159
+ ) -> Tuple[int, Dict[str, str], str]:
160
+ """Executes low-level HTTP interaction and parses response cookies/errors."""
161
+ status_code = 0
162
+ resp_headers: Dict[str, str] = {}
163
+ resp_body = ""
164
+
165
+ req_data = body.encode("utf-8") if body else None
166
+ req = urllib.request.Request(target_url, data=req_data, headers=headers, method=method.upper())
167
+
168
+ try:
169
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
170
+ status_code = resp.getcode()
171
+ resp_headers = dict(resp.headers)
172
+ resp_body = resp.read().decode("utf-8", errors="replace")
173
+
174
+ if "Set-Cookie" in resp_headers:
175
+ self._parse_and_store_cookies(resp_headers["Set-Cookie"])
176
+
177
+ except urllib.error.HTTPError as e:
178
+ status_code = e.code
179
+ resp_headers = dict(e.headers)
180
+ resp_body = e.read().decode("utf-8", errors="replace")
181
+ except urllib.error.URLError as e:
182
+ status_code = 599
183
+ resp_body = f"Network connection error: {e.reason}"
184
+ except Exception as e:
185
+ status_code = 599
186
+ resp_body = f"Unexpected runtime error: {str(e)}"
187
+
188
+ return status_code, resp_headers, resp_body
189
+
190
+ def _parse_and_store_cookies(self, cookie_header: str):
191
+ for part in cookie_header.split(","):
192
+ cookie_pair = part.split(";")[0].strip()
193
+ if "=" in cookie_pair:
194
+ k, v = cookie_pair.split("=", 1)
195
+ self.session.add_cookie(k.strip(), v.strip())
196
+
197
+ def write_report_artifacts(self, run_dir: Path) -> Dict[str, Path]:
198
+ """
199
+ Emits web-validation.md, session-notes.md, and evidence logs.
200
+ """
201
+ run_dir.mkdir(parents=True, exist_ok=True)
202
+ ev_artifacts = self.evidence_collector.write_artifacts(run_dir)
203
+
204
+ # 1. web-validation.md
205
+ wv_file = run_dir / "web-validation.md"
206
+ lines = [
207
+ "# TorusGuard v0.7.0 Web Validation Summary",
208
+ f"\n**Authorization ID:** `{self.auth.authorization_id}`",
209
+ f"**Target:** `{self.auth.target_name}`",
210
+ f"**Total Requests Dispatched:** `{self.request_count}` / `{self.max_requests}`",
211
+ "\n---",
212
+ "\n## 🌐 Interaction Audit Log\n",
213
+ "| Action ID | Method | Target URL | HTTP Status | Safety Review Level | Execution Status |",
214
+ "|---|---|---|:---:|:---:|:---:|",
215
+ ]
216
+ for log in self.validation_logs:
217
+ lines.append(
218
+ f"| `{log.get('action_id')}` | `{log.get('method', 'N/A')}` | `{log.get('url')}` | `{log.get('status_code', '-')}` | `{log.get('review_level')}` | **{log.get('status')}** |"
219
+ )
220
+
221
+ with open(wv_file, "w", encoding="utf-8") as f:
222
+ f.write("\n".join(lines) + "\n")
223
+
224
+ # 2. session-notes.md
225
+ session_file = run_dir / "session-notes.md"
226
+ s_lines = [
227
+ "# TorusGuard v0.7.0 Runtime Session Notes",
228
+ f"\n**Session Identifier:** `{self.session.session_id}`",
229
+ f"**Captured Cookies:** `{len(self.session.cookies)}` active cookies",
230
+ f"**Custom Headers Set:** `{list(self.session.custom_headers.keys())}`",
231
+ "\n### Session Event Timeline\n",
232
+ ]
233
+ if self.session.notes:
234
+ for note in self.session.notes:
235
+ s_lines.append(f"- {note}")
236
+ else:
237
+ s_lines.append("- No manual session interventions recorded.")
238
+
239
+ with open(session_file, "w", encoding="utf-8") as f:
240
+ f.write("\n".join(s_lines) + "\n")
241
+
242
+ return {
243
+ "web_validation": wv_file,
244
+ "session_notes": session_file,
245
+ **ev_artifacts
246
+ }
@@ -0,0 +1,139 @@
1
+ """
2
+ TorusGuard v0.7.0 Safety & Approval Gates
3
+ Enforces explicit governance gates for sensitive endpoints, privileged actions,
4
+ and state-changing runtime operations before execution.
5
+ """
6
+
7
+ import json
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Dict, Any, List, Optional
11
+ from dataclasses import dataclass, asdict
12
+
13
+
14
+ class SafetyReviewLevel(str, Enum):
15
+ AUTO_ALLOWED = "Auto-Allowed"
16
+ APPROVAL_REQUIRED = "Approval Required"
17
+ MANUAL_ONLY = "Manual Only"
18
+
19
+
20
+ @dataclass
21
+ class SafetyDecision:
22
+ action_id: str
23
+ method: str
24
+ target_path: str
25
+ review_level: str
26
+ allowed_to_proceed: bool
27
+ rationale: str
28
+ requires_human_approval: bool
29
+ escalation_reasons: List[str]
30
+
31
+ def to_dict(self) -> Dict[str, Any]:
32
+ return asdict(self)
33
+
34
+
35
+ class SafetyGate:
36
+ """
37
+ Evaluates runtime actions and determines whether they are auto-allowed,
38
+ require explicit human/agent approval, or are strictly manual-only.
39
+ """
40
+
41
+ CRITICAL_MANUAL_PATHS = [
42
+ "/admin/delete",
43
+ "/admin/users/delete",
44
+ "/system/shutdown",
45
+ "/system/reset",
46
+ "/api/v1/database/drop",
47
+ "/billing/charge",
48
+ "/auth/password-reset-confirm"
49
+ ]
50
+
51
+ SENSITIVE_APPROVAL_KEYWORDS = [
52
+ "auth",
53
+ "login",
54
+ "session",
55
+ "token",
56
+ "tenant",
57
+ "upload",
58
+ "transfer",
59
+ "credential",
60
+ "secret",
61
+ "role"
62
+ ]
63
+
64
+ @classmethod
65
+ def evaluate_action(
66
+ cls,
67
+ action_id: str,
68
+ method: str,
69
+ path: str,
70
+ context: Optional[str] = None,
71
+ pre_approved: bool = False
72
+ ) -> SafetyDecision:
73
+ """
74
+ Evaluates safety risk and returns a formal SafetyDecision.
75
+ """
76
+ method_upper = method.upper()
77
+ path_lower = path.lower()
78
+ context_lower = (context or "").lower()
79
+ escalations = []
80
+
81
+ # 1. Check for Critical / Destructive paths -> Manual Only
82
+ for crit in cls.CRITICAL_MANUAL_PATHS:
83
+ if crit in path_lower or crit in context_lower:
84
+ escalations.append(f"Matches critical path/action pattern '{crit}'")
85
+ return SafetyDecision(
86
+ action_id=action_id,
87
+ method=method_upper,
88
+ target_path=path,
89
+ review_level=SafetyReviewLevel.MANUAL_ONLY.value,
90
+ allowed_to_proceed=False,
91
+ rationale="High-risk destructive/privileged path; automated execution blocked.",
92
+ requires_human_approval=True,
93
+ escalation_reasons=escalations
94
+ )
95
+
96
+ # 2. Check for State-Changing methods or Sensitive keywords -> Approval Required
97
+ is_state_changing = method_upper in ["POST", "PUT", "DELETE", "PATCH"]
98
+ if is_state_changing:
99
+ escalations.append(f"HTTP method '{method_upper}' can alter server state")
100
+
101
+ for kw in cls.SENSITIVE_APPROVAL_KEYWORDS:
102
+ if kw in path_lower or kw in context_lower:
103
+ escalations.append(f"Touches sensitive security context keyword '{kw}'")
104
+
105
+ if escalations:
106
+ # Requires approval
107
+ allowed = pre_approved
108
+ rationale = "Pre-approved by security engineer" if pre_approved else "Requires explicit approval before execution"
109
+ return SafetyDecision(
110
+ action_id=action_id,
111
+ method=method_upper,
112
+ target_path=path,
113
+ review_level=SafetyReviewLevel.APPROVAL_REQUIRED.value,
114
+ allowed_to_proceed=allowed,
115
+ rationale=rationale,
116
+ requires_human_approval=not pre_approved,
117
+ escalation_reasons=escalations
118
+ )
119
+
120
+ # 3. Read-only standard path -> Auto-Allowed
121
+ return SafetyDecision(
122
+ action_id=action_id,
123
+ method=method_upper,
124
+ target_path=path,
125
+ review_level=SafetyReviewLevel.AUTO_ALLOWED.value,
126
+ allowed_to_proceed=True,
127
+ rationale="Read-only query to non-sensitive in-scope endpoint.",
128
+ requires_human_approval=False,
129
+ escalation_reasons=[]
130
+ )
131
+
132
+ @staticmethod
133
+ def record_decisions(run_dir: Path, decisions: List[SafetyDecision]) -> Path:
134
+ run_dir.mkdir(parents=True, exist_ok=True)
135
+ decision_file = run_dir / "safety-decisions.json"
136
+ data = [d.to_dict() for d in decisions]
137
+ with open(decision_file, "w", encoding="utf-8") as f:
138
+ json.dump(data, f, indent=2)
139
+ return decision_file