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