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.
- package/.torusguard/.manifest.json +94 -30
- package/.torusguard/core/__init__.py +146 -0
- package/.torusguard/core/agent_roles.py +104 -0
- package/.torusguard/core/ast_walker.py +283 -0
- package/.torusguard/core/authorization.py +218 -0
- package/.torusguard/core/browser_verifier.py +128 -0
- package/.torusguard/core/bundle.py +141 -0
- package/.torusguard/core/call_graph.py +184 -0
- package/.torusguard/core/clustering.py +275 -0
- package/.torusguard/core/confidence.py +120 -0
- package/.torusguard/core/cross_file_taint.py +101 -0
- package/.torusguard/core/exploit_checker.py +317 -0
- package/.torusguard/core/formatter.py +351 -0
- package/.torusguard/core/governance.py +210 -0
- package/.torusguard/core/identity.py +104 -0
- package/.torusguard/core/import_resolver.py +91 -0
- package/.torusguard/core/incremental.py +102 -0
- package/.torusguard/core/lifecycle.py +137 -0
- package/.torusguard/core/models.py +425 -0
- package/.torusguard/core/parallel.py +56 -0
- package/.torusguard/core/parser.py +202 -0
- package/.torusguard/core/rechecker.py +107 -0
- package/.torusguard/core/replay_trace.py +178 -0
- package/.torusguard/core/rules_registry.py +131 -0
- package/.torusguard/core/run_folder.py +60 -0
- package/.torusguard/core/run_manager.py +163 -0
- package/.torusguard/core/runtime_evidence.py +175 -0
- package/.torusguard/core/runtime_validator.py +246 -0
- package/.torusguard/core/safety_gate.py +139 -0
- package/.torusguard/core/sarif.py +189 -0
- package/.torusguard/core/stack_profiler.py +184 -0
- package/.torusguard/core/symbol_table.py +91 -0
- package/.torusguard/core/taint.py +133 -0
- package/.torusguard/core/taint_graph.py +235 -0
- package/.torusguard/core/taint_rules.py +268 -0
- package/.torusguard/core/v070_reporter.py +102 -0
- package/.torusguard/core/v070_workflow.py +339 -0
- package/.torusguard/core/v6_reporter.py +180 -0
- package/.torusguard/core/v6_workflow.py +221 -0
- package/.torusguard/core/watcher.py +58 -0
- package/.torusguard/rules/TG-INPUT-007-unvalidated-redirect.md +53 -0
- package/.torusguard/rules/TG-INPUT-008-insecure-deserialization.md +52 -0
- package/.torusguard/rules/container/TG-CONT-001-root-user-execution.md +50 -0
- package/.torusguard/rules/container/TG-CONT-002-docker-socket-mount.md +47 -0
- package/.torusguard/rules/container/TG-CONT-003-privileged-container-mode.md +53 -0
- package/.torusguard/rules/container/TG-CONT-004-build-arg-secret-exposure.md +43 -0
- package/.torusguard/rules/git/TG-GIT-001-historical-secret-in-git-commit.md +44 -0
- package/.torusguard/rules/git/TG-GIT-002-plaintext-credentials-in-git-config.md +41 -0
- package/.torusguard/rules/git/TG-GIT-003-sensitive-tracked-file-gitignore-breach.md +40 -0
- package/.torusguard/rules/rag/TG-RAG-001-untrusted-rag-context-injection.md +72 -0
- package/.torusguard/rules/rag/TG-RAG-002-autonomous-llm-tool-unsandboxed-call.md +51 -0
- package/.torusguard/rules/rag/TG-RAG-003-unpartitioned-vector-tenant-lookup.md +51 -0
- package/.torusguard/rules/redos/TG-REDOS-001-catastrophic-exponential-backtracking.md +46 -0
- package/.torusguard/rules/redos/TG-REDOS-002-unbounded-nested-quantifier.md +43 -0
- package/.torusguard/rules_catalog.json +96 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/rules_sync.cpython-314.pyc +0 -0
- package/.torusguard/scripts/audit_runner.py +108 -10
- package/.torusguard/scripts/finding_scorer.py +43 -13
- package/.torusguard/scripts/manifest_builder.py +1 -1
- package/.torusguard/scripts/skill_profiler.py +26 -0
- package/.torusguard/skills/torusguard/SKILL.md +74 -25
- package/.torusguard/skills/torusguard/bootstrap.py +57 -24
- package/.torusguard/skills/torusguard-ai-guard/SKILL.md +95 -0
- package/.torusguard/skills/torusguard-apply/SKILL.md +60 -34
- package/.torusguard/skills/torusguard-audit/SKILL.md +131 -57
- package/.torusguard/skills/torusguard-authorize/SKILL.md +48 -6
- package/.torusguard/skills/torusguard-container/SKILL.md +94 -0
- package/.torusguard/skills/torusguard-exploit-check/SKILL.md +50 -6
- package/.torusguard/skills/torusguard-full/SKILL.md +62 -19
- package/.torusguard/skills/torusguard-git-mine/SKILL.md +92 -0
- package/.torusguard/skills/torusguard-harden/SKILL.md +81 -50
- package/.torusguard/skills/torusguard-init/SKILL.md +61 -14
- package/.torusguard/skills/torusguard-ocr-scan/SKILL.md +94 -0
- package/.torusguard/skills/torusguard-recheck/SKILL.md +71 -18
- package/.torusguard/skills/torusguard-redos/SKILL.md +91 -0
- package/.torusguard/skills/torusguard-report/SKILL.md +50 -9
- package/.torusguard/skills/torusguard-status/SKILL.md +63 -10
- package/.torusguard/skills/torusguard-verify/SKILL.md +52 -10
- package/.torusguard/skills/torusguard-web-validate/SKILL.md +53 -8
- package/.torusguard/workflows/ai-guard.md +31 -0
- package/.torusguard/workflows/apply.md +32 -55
- package/.torusguard/workflows/audit.md +35 -49
- package/.torusguard/workflows/authorize.md +27 -50
- package/.torusguard/workflows/container.md +29 -0
- package/.torusguard/workflows/exploit-check.md +28 -50
- package/.torusguard/workflows/git-mine.md +25 -0
- package/.torusguard/workflows/harden.md +29 -48
- package/.torusguard/workflows/init.md +27 -50
- package/.torusguard/workflows/memory.md +18 -23
- package/.torusguard/workflows/ocr-scan.md +25 -0
- package/.torusguard/workflows/recheck.md +28 -46
- package/.torusguard/workflows/redos.md +27 -0
- package/.torusguard/workflows/report.md +33 -52
- package/.torusguard/workflows/status.md +31 -52
- package/.torusguard/workflows/verify.md +29 -49
- package/.torusguard/workflows/web-validate.md +22 -45
- package/README.md +96 -60
- package/package.json +7 -2
- package/skills/torusguard/SKILL.md +75 -24
- package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/skills/torusguard/bootstrap.py +60 -71
- package/skills/torusguard/payload/.manifest.json +94 -31
- package/skills/torusguard/payload/core/__init__.py +146 -0
- package/skills/torusguard/payload/core/agent_roles.py +104 -0
- package/skills/torusguard/payload/core/ast_walker.py +283 -0
- package/skills/torusguard/payload/core/authorization.py +218 -0
- package/skills/torusguard/payload/core/browser_verifier.py +128 -0
- package/skills/torusguard/payload/core/bundle.py +141 -0
- package/skills/torusguard/payload/core/call_graph.py +184 -0
- package/skills/torusguard/payload/core/clustering.py +275 -0
- package/skills/torusguard/payload/core/confidence.py +120 -0
- package/skills/torusguard/payload/core/cross_file_taint.py +101 -0
- package/skills/torusguard/payload/core/exploit_checker.py +317 -0
- package/skills/torusguard/payload/core/formatter.py +351 -0
- package/skills/torusguard/payload/core/governance.py +210 -0
- package/skills/torusguard/payload/core/identity.py +104 -0
- package/skills/torusguard/payload/core/import_resolver.py +91 -0
- package/skills/torusguard/payload/core/incremental.py +102 -0
- package/skills/torusguard/payload/core/lifecycle.py +137 -0
- package/skills/torusguard/payload/core/models.py +425 -0
- package/skills/torusguard/payload/core/parallel.py +56 -0
- package/skills/torusguard/payload/core/parser.py +202 -0
- package/skills/torusguard/payload/core/rechecker.py +107 -0
- package/skills/torusguard/payload/core/replay_trace.py +178 -0
- package/skills/torusguard/payload/core/rules_registry.py +131 -0
- package/skills/torusguard/payload/core/run_folder.py +60 -0
- package/skills/torusguard/payload/core/run_manager.py +163 -0
- package/skills/torusguard/payload/core/runtime_evidence.py +175 -0
- package/skills/torusguard/payload/core/runtime_validator.py +246 -0
- package/skills/torusguard/payload/core/safety_gate.py +139 -0
- package/skills/torusguard/payload/core/sarif.py +189 -0
- package/skills/torusguard/payload/core/stack_profiler.py +184 -0
- package/skills/torusguard/payload/core/symbol_table.py +91 -0
- package/skills/torusguard/payload/core/taint.py +133 -0
- package/skills/torusguard/payload/core/taint_graph.py +235 -0
- package/skills/torusguard/payload/core/taint_rules.py +268 -0
- package/skills/torusguard/payload/core/v070_reporter.py +102 -0
- package/skills/torusguard/payload/core/v070_workflow.py +339 -0
- package/skills/torusguard/payload/core/v6_reporter.py +180 -0
- package/skills/torusguard/payload/core/v6_workflow.py +221 -0
- package/skills/torusguard/payload/core/watcher.py +58 -0
- package/skills/torusguard/payload/rules/TG-INPUT-007-unvalidated-redirect.md +53 -0
- package/skills/torusguard/payload/rules/TG-INPUT-008-insecure-deserialization.md +52 -0
- package/skills/torusguard/payload/rules/container/TG-CONT-001-root-user-execution.md +50 -0
- package/skills/torusguard/payload/rules/container/TG-CONT-002-docker-socket-mount.md +47 -0
- package/skills/torusguard/payload/rules/container/TG-CONT-003-privileged-container-mode.md +53 -0
- package/skills/torusguard/payload/rules/container/TG-CONT-004-build-arg-secret-exposure.md +43 -0
- package/skills/torusguard/payload/rules/git/TG-GIT-001-historical-secret-in-git-commit.md +44 -0
- package/skills/torusguard/payload/rules/git/TG-GIT-002-plaintext-credentials-in-git-config.md +41 -0
- package/skills/torusguard/payload/rules/git/TG-GIT-003-sensitive-tracked-file-gitignore-breach.md +40 -0
- package/skills/torusguard/payload/rules/rag/TG-RAG-001-untrusted-rag-context-injection.md +72 -0
- package/skills/torusguard/payload/rules/rag/TG-RAG-002-autonomous-llm-tool-unsandboxed-call.md +51 -0
- package/skills/torusguard/payload/rules/rag/TG-RAG-003-unpartitioned-vector-tenant-lookup.md +51 -0
- package/skills/torusguard/payload/rules/redos/TG-REDOS-001-catastrophic-exponential-backtracking.md +46 -0
- package/skills/torusguard/payload/rules/redos/TG-REDOS-002-unbounded-nested-quantifier.md +43 -0
- package/skills/torusguard/payload/rules_catalog.json +338 -518
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/audit_runner.py +108 -10
- package/skills/torusguard/payload/scripts/finding_scorer.py +43 -13
- package/skills/torusguard/payload/scripts/manifest_builder.py +1 -1
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +74 -25
- package/skills/torusguard/payload/skills/torusguard/bootstrap.py +57 -24
- package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -41
- package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -41
- package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -40
- package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -25
- package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -40
- package/skills/torusguard/payload/skills/torusguard-ai-guard/SKILL.md +95 -0
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +60 -34
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +131 -57
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +48 -6
- package/skills/torusguard/payload/skills/torusguard-container/SKILL.md +94 -0
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +50 -6
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +62 -19
- package/skills/torusguard/payload/skills/torusguard-git-mine/SKILL.md +92 -0
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +81 -50
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +61 -14
- package/skills/torusguard/payload/skills/torusguard-ocr-scan/SKILL.md +94 -0
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +71 -18
- package/skills/torusguard/payload/skills/torusguard-redos/SKILL.md +91 -0
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +50 -9
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +63 -10
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +52 -10
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +53 -8
- package/skills/torusguard/payload/workflows/ai-guard.md +31 -0
- package/skills/torusguard/payload/workflows/apply.md +31 -62
- package/skills/torusguard/payload/workflows/audit.md +35 -55
- package/skills/torusguard/payload/workflows/authorize.md +27 -50
- package/skills/torusguard/payload/workflows/container.md +29 -0
- package/skills/torusguard/payload/workflows/exploit-check.md +28 -50
- package/skills/torusguard/payload/workflows/git-mine.md +25 -0
- package/skills/torusguard/payload/workflows/harden.md +28 -52
- package/skills/torusguard/payload/workflows/init.md +27 -56
- package/skills/torusguard/payload/workflows/memory.md +18 -23
- package/skills/torusguard/payload/workflows/ocr-scan.md +25 -0
- package/skills/torusguard/payload/workflows/recheck.md +28 -46
- package/skills/torusguard/payload/workflows/redos.md +27 -0
- package/skills/torusguard/payload/workflows/report.md +39 -62
- package/skills/torusguard/payload/workflows/status.md +31 -55
- package/skills/torusguard/payload/workflows/torusguard-audit.md +35 -55
- package/skills/torusguard/payload/workflows/verify.md +29 -49
- package/skills/torusguard/payload/workflows/web-validate.md +22 -45
- package/skills/torusguard/references/csharp-security.md +41 -0
- package/skills/torusguard/references/go-security.md +41 -0
- package/skills/torusguard/references/java-security.md +40 -0
- package/skills/torusguard/references/polyglot-security-matrix.md +25 -0
- package/skills/torusguard/references/rust-security.md +40 -0
- package/skills/torusguard-ai-guard/SKILL.md +95 -0
- package/skills/torusguard-apply/SKILL.md +60 -34
- package/skills/torusguard-audit/SKILL.md +130 -57
- package/skills/torusguard-authorize/SKILL.md +48 -6
- package/skills/torusguard-container/SKILL.md +94 -0
- package/skills/torusguard-exploit-check/SKILL.md +50 -6
- package/skills/torusguard-full/SKILL.md +62 -19
- package/skills/torusguard-git-mine/SKILL.md +92 -0
- package/skills/torusguard-harden/SKILL.md +81 -50
- package/skills/torusguard-init/SKILL.md +61 -14
- package/skills/torusguard-ocr-scan/SKILL.md +94 -0
- package/skills/torusguard-recheck/SKILL.md +71 -18
- package/skills/torusguard-redos/SKILL.md +91 -0
- package/skills/torusguard-report/SKILL.md +50 -9
- package/skills/torusguard-status/SKILL.md +63 -10
- package/skills/torusguard-verify/SKILL.md +52 -10
- package/skills/torusguard-web-validate/SKILL.md +53 -8
|
@@ -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
|