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,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v0.7.0 Browser-Assisted Verification Engine
|
|
3
|
+
Validates client-side route guards, authenticated frontend flows, and DOM/UI evidence
|
|
4
|
+
with capped navigation depth, action logging, and safety controls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
from dataclasses import dataclass, asdict
|
|
11
|
+
|
|
12
|
+
from core.authorization import AuthorizationRecord, AuthorizationManager, AuthorizationError
|
|
13
|
+
from core.safety_gate import SafetyGate, SafetyReviewLevel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class BrowserAction:
|
|
18
|
+
action_id: str
|
|
19
|
+
action_type: str # "navigate", "assert_dom", "check_route_guard", "capture_snapshot"
|
|
20
|
+
target_url: str
|
|
21
|
+
selector: Optional[str] = None
|
|
22
|
+
outcome: str = ""
|
|
23
|
+
dom_snippet: Optional[str] = None
|
|
24
|
+
requires_approval: bool = False
|
|
25
|
+
approved: bool = True
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
28
|
+
return asdict(self)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BrowserVerifier:
|
|
32
|
+
"""
|
|
33
|
+
Simulates and executes bounded browser-assisted validations against client-side flows.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
auth_record: AuthorizationRecord,
|
|
39
|
+
max_depth: int = 3
|
|
40
|
+
):
|
|
41
|
+
self.auth = auth_record
|
|
42
|
+
self.max_depth = min(max_depth, auth_record.scope.max_depth)
|
|
43
|
+
self.current_depth = 0
|
|
44
|
+
self.action_history: List[BrowserAction] = []
|
|
45
|
+
|
|
46
|
+
def verify_route_guard(
|
|
47
|
+
self,
|
|
48
|
+
target_url: str,
|
|
49
|
+
authenticated: bool,
|
|
50
|
+
expected_redirect_url: Optional[str] = None,
|
|
51
|
+
protected_dom_marker: Optional[str] = None,
|
|
52
|
+
client_dom_content: Optional[str] = None
|
|
53
|
+
) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Validates whether client-side router/guards protect sensitive UI components
|
|
56
|
+
or leak them before authentication.
|
|
57
|
+
"""
|
|
58
|
+
AuthorizationManager.check_authorized_or_raise(target_url, self.auth, method="GET")
|
|
59
|
+
|
|
60
|
+
action_id = f"BRW-{len(self.action_history) + 1:03d}"
|
|
61
|
+
decision = SafetyGate.evaluate_action(action_id, "GET", target_url)
|
|
62
|
+
|
|
63
|
+
if not decision.allowed_to_proceed:
|
|
64
|
+
action = BrowserAction(
|
|
65
|
+
action_id=action_id,
|
|
66
|
+
action_type="check_route_guard",
|
|
67
|
+
target_url=target_url,
|
|
68
|
+
outcome="BLOCKED_BY_SAFETY_GATE",
|
|
69
|
+
requires_approval=True,
|
|
70
|
+
approved=False
|
|
71
|
+
)
|
|
72
|
+
self.action_history.append(action)
|
|
73
|
+
return {"status": "Blocked by Safety Gate", "action": action.to_dict()}
|
|
74
|
+
|
|
75
|
+
# Evaluate Client-Side DOM Guard
|
|
76
|
+
dom = client_dom_content or ""
|
|
77
|
+
guard_intact = True
|
|
78
|
+
leak_detected = False
|
|
79
|
+
|
|
80
|
+
if not authenticated:
|
|
81
|
+
# When unauthenticated, sensitive DOM marker should NOT be present
|
|
82
|
+
if protected_dom_marker and protected_dom_marker in dom:
|
|
83
|
+
guard_intact = False
|
|
84
|
+
leak_detected = True
|
|
85
|
+
outcome = f"Client Route Guard Failed: Sensitive marker '{protected_dom_marker}' rendered without authentication."
|
|
86
|
+
else:
|
|
87
|
+
outcome = "Client Route Guard Enforced: Protected view withheld from unauthenticated DOM."
|
|
88
|
+
else:
|
|
89
|
+
outcome = "Authenticated Session Verified: Client loaded permitted views."
|
|
90
|
+
|
|
91
|
+
action = BrowserAction(
|
|
92
|
+
action_id=action_id,
|
|
93
|
+
action_type="check_route_guard",
|
|
94
|
+
target_url=target_url,
|
|
95
|
+
outcome=outcome,
|
|
96
|
+
dom_snippet=dom[:200] if dom else None,
|
|
97
|
+
requires_approval=False,
|
|
98
|
+
approved=True
|
|
99
|
+
)
|
|
100
|
+
self.action_history.append(action)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"guard_intact": guard_intact,
|
|
104
|
+
"leak_detected": leak_detected,
|
|
105
|
+
"outcome": outcome,
|
|
106
|
+
"action": action.to_dict()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def write_report(self, run_dir: Path) -> Path:
|
|
110
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
report_file = run_dir / "browser-validation.md"
|
|
112
|
+
lines = [
|
|
113
|
+
"# TorusGuard v0.7.0 Browser-Assisted Validation Report",
|
|
114
|
+
f"\n**Authorization ID:** `{self.auth.authorization_id}`",
|
|
115
|
+
f"**Max Navigation Depth:** `{self.max_depth}` levels",
|
|
116
|
+
f"**Total Browser Actions:** `{len(self.action_history)}`",
|
|
117
|
+
"\n---",
|
|
118
|
+
"\n## 🖥️ UI Action Traces\n",
|
|
119
|
+
"| Action ID | Type | Target URL | Outcome | Approval Status |",
|
|
120
|
+
"|---|---|---|---|:---:|",
|
|
121
|
+
]
|
|
122
|
+
for a in self.action_history:
|
|
123
|
+
lines.append(f"| `{a.action_id}` | `{a.action_type}` | `{a.target_url}` | {a.outcome} | **{'Approved' if a.approved else 'Blocked'}** |")
|
|
124
|
+
|
|
125
|
+
with open(report_file, "w", encoding="utf-8") as f:
|
|
126
|
+
f.write("\n".join(lines) + "\n")
|
|
127
|
+
|
|
128
|
+
return report_file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Remediation Bundle Generator
|
|
3
|
+
Constructs self-contained, structured remediation packages per cluster or finding containing:
|
|
4
|
+
- finding.md
|
|
5
|
+
- remediation.md
|
|
6
|
+
- minimal_patch_plan.md
|
|
7
|
+
- verify-after-change.md
|
|
8
|
+
- metadata.json
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from dataclasses import dataclass, field, asdict
|
|
14
|
+
from typing import Dict, Any, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RemediationBundle:
|
|
19
|
+
bundle_id: str
|
|
20
|
+
finding_id: str
|
|
21
|
+
rule_id: str
|
|
22
|
+
title: str
|
|
23
|
+
target_files: List[str]
|
|
24
|
+
risk_severity: str
|
|
25
|
+
what_is_wrong: str
|
|
26
|
+
why_it_matters: str
|
|
27
|
+
what_should_change: str
|
|
28
|
+
proposed_diff: str
|
|
29
|
+
verification_steps: str
|
|
30
|
+
cluster_id: Optional[str] = None
|
|
31
|
+
created_at: str = ""
|
|
32
|
+
|
|
33
|
+
def write_to_directory(self, base_dir: Path) -> Path:
|
|
34
|
+
bundle_dir = base_dir / self.bundle_id
|
|
35
|
+
bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
# 1. finding.md
|
|
38
|
+
with open(bundle_dir / "finding.md", "w", encoding="utf-8") as f:
|
|
39
|
+
f.write(f"# Finding: {self.title}\n\n")
|
|
40
|
+
f.write(f"- **Finding ID:** `{self.finding_id}`\n")
|
|
41
|
+
f.write(f"- **Rule ID:** `{self.rule_id}`\n")
|
|
42
|
+
f.write(f"- **Severity:** {self.risk_severity}\n")
|
|
43
|
+
if self.cluster_id:
|
|
44
|
+
f.write(f"- **Root-Cause Cluster:** `{self.cluster_id}`\n")
|
|
45
|
+
f.write(f"- **Target Files:** {', '.join(f'`{tf}`' for tf in self.target_files)}\n\n")
|
|
46
|
+
f.write("## What Is Wrong\n")
|
|
47
|
+
f.write(f"{self.what_is_wrong}\n\n")
|
|
48
|
+
f.write("## Why It Matters\n")
|
|
49
|
+
f.write(f"{self.why_it_matters}\n")
|
|
50
|
+
|
|
51
|
+
# 2. remediation.md
|
|
52
|
+
with open(bundle_dir / "remediation.md", "w", encoding="utf-8") as f:
|
|
53
|
+
f.write(f"# Remediation Guide: {self.title}\n\n")
|
|
54
|
+
f.write("## What Should Change\n")
|
|
55
|
+
f.write(f"{self.what_should_change}\n\n")
|
|
56
|
+
f.write("## Target Files to Modify\n")
|
|
57
|
+
for tf in self.target_files:
|
|
58
|
+
f.write(f"- `{tf}`\n")
|
|
59
|
+
|
|
60
|
+
# 3. minimal_patch_plan.md
|
|
61
|
+
with open(bundle_dir / "minimal_patch_plan.md", "w", encoding="utf-8") as f:
|
|
62
|
+
f.write(f"# Minimal Patch Plan for `{self.finding_id}`\n\n")
|
|
63
|
+
f.write("```diff\n")
|
|
64
|
+
f.write(self.proposed_diff.strip() + "\n")
|
|
65
|
+
f.write("```\n")
|
|
66
|
+
|
|
67
|
+
# 4. verify-after-change.md
|
|
68
|
+
with open(bundle_dir / "verify-after-change.md", "w", encoding="utf-8") as f:
|
|
69
|
+
f.write(f"# Verification Steps for `{self.finding_id}`\n\n")
|
|
70
|
+
f.write(f"{self.verification_steps}\n\n")
|
|
71
|
+
f.write("### Recheck Command\n")
|
|
72
|
+
f.write("```bash\n/torusguard recheck\n```\n")
|
|
73
|
+
|
|
74
|
+
# 5. metadata.json
|
|
75
|
+
meta = {
|
|
76
|
+
"bundle_id": self.bundle_id,
|
|
77
|
+
"finding_id": self.finding_id,
|
|
78
|
+
"rule_id": self.rule_id,
|
|
79
|
+
"title": self.title,
|
|
80
|
+
"cluster_id": self.cluster_id,
|
|
81
|
+
"target_files": self.target_files,
|
|
82
|
+
"risk_severity": self.risk_severity,
|
|
83
|
+
"created_at": self.created_at,
|
|
84
|
+
}
|
|
85
|
+
with open(bundle_dir / "metadata.json", "w", encoding="utf-8") as f:
|
|
86
|
+
json.dump(meta, f, indent=2)
|
|
87
|
+
|
|
88
|
+
return bundle_dir
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BundleManager:
|
|
92
|
+
"""
|
|
93
|
+
Factory for generating remediation bundles from findings and clusters.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def create_bundle(finding: Dict[str, Any], cluster_id: Optional[str] = None) -> RemediationBundle:
|
|
98
|
+
f_id = finding.get("finding_id", "fnd-01")
|
|
99
|
+
rule_id = finding.get("rule_id", "TG-GENERIC")
|
|
100
|
+
title = finding.get("title", "Security Finding")
|
|
101
|
+
target = finding.get("target", {})
|
|
102
|
+
file_path = target.get("file_path", "app.py")
|
|
103
|
+
severity = finding.get("severity", "High")
|
|
104
|
+
|
|
105
|
+
bundle_id = f"bundle-{f_id}"
|
|
106
|
+
|
|
107
|
+
what_is_wrong = finding.get(
|
|
108
|
+
"what_is_wrong",
|
|
109
|
+
f"The component in `{file_path}` violates security rule `{rule_id}`."
|
|
110
|
+
)
|
|
111
|
+
why_it_matters = finding.get(
|
|
112
|
+
"why_it_matters",
|
|
113
|
+
"This vulnerability could allow attackers to bypass authorization, access unauthorized tenant data, or inject malicious payloads."
|
|
114
|
+
)
|
|
115
|
+
what_should_change = finding.get(
|
|
116
|
+
"what_should_change",
|
|
117
|
+
f"Apply least-invasive framework-idiomatic hardening to `{file_path}`."
|
|
118
|
+
)
|
|
119
|
+
proposed_diff = finding.get(
|
|
120
|
+
"proposed_diff",
|
|
121
|
+
f"--- a/{file_path}\n+++ b/{file_path}\n@@ -1,3 +1,3 @@\n-# Unsafe pattern\n+# Hardened pattern\n"
|
|
122
|
+
)
|
|
123
|
+
verification_steps = finding.get(
|
|
124
|
+
"verification_steps",
|
|
125
|
+
f"Run `/torusguard recheck` on `{file_path}` to confirm vulnerability resolution."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return RemediationBundle(
|
|
129
|
+
bundle_id=bundle_id,
|
|
130
|
+
finding_id=f_id,
|
|
131
|
+
rule_id=rule_id,
|
|
132
|
+
title=title,
|
|
133
|
+
target_files=[file_path],
|
|
134
|
+
risk_severity=severity,
|
|
135
|
+
what_is_wrong=what_is_wrong,
|
|
136
|
+
why_it_matters=why_it_matters,
|
|
137
|
+
what_should_change=what_should_change,
|
|
138
|
+
proposed_diff=proposed_diff,
|
|
139
|
+
verification_steps=verification_steps,
|
|
140
|
+
cluster_id=cluster_id,
|
|
141
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard Project-Wide Call Graph Engine
|
|
3
|
+
Maps interprocedural call sites, function definitions, and caller/callee relationships
|
|
4
|
+
across module boundaries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
from typing import Dict, List, Optional, Set, Any
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from core.parser import PolyglotParser, ParseResult
|
|
13
|
+
from core.import_resolver import ImportResolver
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CallSite:
|
|
18
|
+
caller_file: str
|
|
19
|
+
caller_func: str
|
|
20
|
+
caller_line: int
|
|
21
|
+
callee_name: str
|
|
22
|
+
callee_file: Optional[str] = None
|
|
23
|
+
callee_line: Optional[int] = None
|
|
24
|
+
arguments: List[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
return asdict(self)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FunctionDef:
|
|
32
|
+
file_path: str
|
|
33
|
+
func_name: str
|
|
34
|
+
start_line: int
|
|
35
|
+
end_line: int = 0
|
|
36
|
+
parameters: List[str] = field(default_factory=list)
|
|
37
|
+
return_expressions: List[str] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
40
|
+
return asdict(self)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CallGraph:
|
|
44
|
+
"""Project-wide interprocedural function call graph."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, project_root: Path):
|
|
47
|
+
self.project_root = project_root.resolve()
|
|
48
|
+
self.import_resolver = ImportResolver(self.project_root)
|
|
49
|
+
# Key: (file_path, func_name) -> FunctionDef
|
|
50
|
+
self.functions: Dict[Tuple[str, str], FunctionDef] = {}
|
|
51
|
+
# Key: func_name -> list of FunctionDef (for quick symbol lookup)
|
|
52
|
+
self.functions_by_name: Dict[str, List[FunctionDef]] = {}
|
|
53
|
+
# Key: (file_path, func_name) -> list of CallSite where this func is caller
|
|
54
|
+
self.callees: Dict[Tuple[str, str], List[CallSite]] = {}
|
|
55
|
+
# Key: (file_path, func_name) -> list of CallSite where this func is callee
|
|
56
|
+
self.callers: Dict[Tuple[str, str], List[CallSite]] = {}
|
|
57
|
+
|
|
58
|
+
def register_function(self, fdef: FunctionDef) -> None:
|
|
59
|
+
key = (fdef.file_path, fdef.func_name)
|
|
60
|
+
self.functions[key] = fdef
|
|
61
|
+
if fdef.func_name not in self.functions_by_name:
|
|
62
|
+
self.functions_by_name[fdef.func_name] = []
|
|
63
|
+
self.functions_by_name[fdef.func_name].append(fdef)
|
|
64
|
+
|
|
65
|
+
def add_call(self, call: CallSite) -> None:
|
|
66
|
+
caller_key = (call.caller_file, call.caller_func)
|
|
67
|
+
if caller_key not in self.callees:
|
|
68
|
+
self.callees[caller_key] = []
|
|
69
|
+
self.callees[caller_key].append(call)
|
|
70
|
+
|
|
71
|
+
if call.callee_file:
|
|
72
|
+
callee_key = (call.callee_file, call.callee_name)
|
|
73
|
+
if callee_key not in self.callers:
|
|
74
|
+
self.callers[callee_key] = []
|
|
75
|
+
self.callers[callee_key].append(call)
|
|
76
|
+
|
|
77
|
+
def build_from_files(self, file_paths: List[Path], parser: PolyglotParser) -> None:
|
|
78
|
+
"""Parse files and construct functions and call edges."""
|
|
79
|
+
parsed_results: Dict[str, ParseResult] = {}
|
|
80
|
+
|
|
81
|
+
# Pass 1: Extract all function definitions
|
|
82
|
+
for p in file_paths:
|
|
83
|
+
try:
|
|
84
|
+
res = parser.parse_file(p)
|
|
85
|
+
rel_p = str(p.resolve().relative_to(self.project_root)).replace("\\", "/")
|
|
86
|
+
parsed_results[rel_p] = res
|
|
87
|
+
self._extract_function_defs(rel_p, res.raw_content, res.language)
|
|
88
|
+
except Exception:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# Pass 2: Extract call sites and link to callees
|
|
92
|
+
for rel_p, res in parsed_results.items():
|
|
93
|
+
abs_p = self.project_root / rel_p
|
|
94
|
+
# Build import map: alias/name -> resolved Path
|
|
95
|
+
resolved_imports: Dict[str, str] = {}
|
|
96
|
+
for imp in res.imports:
|
|
97
|
+
target_p = self.import_resolver.resolve(abs_p, imp.module)
|
|
98
|
+
if target_p:
|
|
99
|
+
rel_target = str(target_p.resolve().relative_to(self.project_root)).replace("\\", "/")
|
|
100
|
+
for name in imp.imported_names:
|
|
101
|
+
resolved_imports[name] = rel_target
|
|
102
|
+
for alias, orig in imp.alias_map.items():
|
|
103
|
+
resolved_imports[alias] = rel_target
|
|
104
|
+
|
|
105
|
+
# Process function calls
|
|
106
|
+
for fc in res.function_calls:
|
|
107
|
+
callee_name = fc.name
|
|
108
|
+
callee_file = None
|
|
109
|
+
callee_line = None
|
|
110
|
+
|
|
111
|
+
# Method call e.g. mod.func
|
|
112
|
+
if "." in callee_name:
|
|
113
|
+
prefix, actual_name = callee_name.split(".", 1)
|
|
114
|
+
if prefix in resolved_imports:
|
|
115
|
+
callee_file = resolved_imports[prefix]
|
|
116
|
+
callee_name = actual_name
|
|
117
|
+
elif callee_name in resolved_imports:
|
|
118
|
+
callee_file = resolved_imports[callee_name]
|
|
119
|
+
else:
|
|
120
|
+
# Check local in same file
|
|
121
|
+
if (rel_p, callee_name) in self.functions:
|
|
122
|
+
callee_file = rel_p
|
|
123
|
+
|
|
124
|
+
# Find which enclosing function contains this call
|
|
125
|
+
caller_func = self._find_enclosing_function(rel_p, fc.line_number)
|
|
126
|
+
|
|
127
|
+
call_site = CallSite(
|
|
128
|
+
caller_file=rel_p,
|
|
129
|
+
caller_func=caller_func,
|
|
130
|
+
caller_line=fc.line_number,
|
|
131
|
+
callee_name=callee_name,
|
|
132
|
+
callee_file=callee_file,
|
|
133
|
+
arguments=fc.arguments
|
|
134
|
+
)
|
|
135
|
+
self.add_call(call_site)
|
|
136
|
+
|
|
137
|
+
def _extract_function_defs(self, file_path: str, content: str, language: str) -> None:
|
|
138
|
+
lines = content.splitlines()
|
|
139
|
+
for idx, line in enumerate(lines):
|
|
140
|
+
line_num = idx + 1
|
|
141
|
+
stripped = line.strip()
|
|
142
|
+
|
|
143
|
+
# Python: def foo(a, b):
|
|
144
|
+
py_m = re.match(r"^def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*?)\)", stripped)
|
|
145
|
+
if py_m:
|
|
146
|
+
fname = py_m.group(1)
|
|
147
|
+
params = [p.strip() for p in py_m.group(2).split(",") if p.strip()]
|
|
148
|
+
self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# JS/TS: function foo(a, b) or const foo = (a, b) =>
|
|
152
|
+
js_m = re.match(r"^(?:async\s+)?function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*?)\)", stripped)
|
|
153
|
+
if js_m:
|
|
154
|
+
fname = js_m.group(1)
|
|
155
|
+
params = [p.strip() for p in js_m.group(2).split(",") if p.strip()]
|
|
156
|
+
self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
arrow_m = re.match(r"^(?:const|let|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\((.*?)\)\s*=>", stripped)
|
|
160
|
+
if arrow_m:
|
|
161
|
+
fname = arrow_m.group(1)
|
|
162
|
+
params = [p.strip() for p in arrow_m.group(2).split(",") if p.strip()]
|
|
163
|
+
self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Go: func foo(a string)
|
|
167
|
+
go_m = re.match(r"^func\s+(?:\(.*?\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*?)\)", stripped)
|
|
168
|
+
if go_m:
|
|
169
|
+
fname = go_m.group(1)
|
|
170
|
+
params = [p.strip() for p in go_m.group(2).split(",") if p.strip()]
|
|
171
|
+
self.register_function(FunctionDef(file_path=file_path, func_name=fname, start_line=line_num, parameters=params))
|
|
172
|
+
|
|
173
|
+
def _find_enclosing_function(self, file_path: str, line_num: int) -> str:
|
|
174
|
+
candidates = [f for (fp, fn), f in self.functions.items() if fp == file_path and f.start_line <= line_num]
|
|
175
|
+
if not candidates:
|
|
176
|
+
return "<global>"
|
|
177
|
+
candidates.sort(key=lambda f: f.start_line, reverse=True)
|
|
178
|
+
return candidates[0].func_name
|
|
179
|
+
|
|
180
|
+
def get_callers(self, file_path: str, func_name: str) -> List[CallSite]:
|
|
181
|
+
return self.callers.get((file_path, func_name), [])
|
|
182
|
+
|
|
183
|
+
def get_callees(self, file_path: str, func_name: str) -> List[CallSite]:
|
|
184
|
+
return self.callees.get((file_path, func_name), [])
|