torusguard 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.torusguard/.manifest.json +47 -5
- 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/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.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/skill_profiler.py +26 -0
- package/.torusguard/skills/torusguard/SKILL.md +6 -2
- package/.torusguard/skills/torusguard-audit/SKILL.md +109 -84
- package/.torusguard/workflows/audit.md +21 -17
- package/README.md +19 -11
- package/package.json +7 -2
- package/skills/torusguard/SKILL.md +6 -2
- package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/skills/torusguard/bootstrap.py +3 -3
- package/skills/torusguard/payload/.manifest.json +48 -7
- 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 -50
- package/skills/torusguard/payload/rules/container/TG-CONT-002-docker-socket-mount.md +47 -47
- package/skills/torusguard/payload/rules/container/TG-CONT-003-privileged-container-mode.md +53 -53
- package/skills/torusguard/payload/rules/container/TG-CONT-004-build-arg-secret-exposure.md +43 -43
- package/skills/torusguard/payload/rules/git/TG-GIT-001-historical-secret-in-git-commit.md +44 -44
- package/skills/torusguard/payload/rules/git/TG-GIT-002-plaintext-credentials-in-git-config.md +41 -41
- package/skills/torusguard/payload/rules/git/TG-GIT-003-sensitive-tracked-file-gitignore-breach.md +40 -40
- package/skills/torusguard/payload/rules/rag/TG-RAG-001-untrusted-rag-context-injection.md +72 -72
- package/skills/torusguard/payload/rules/rag/TG-RAG-002-autonomous-llm-tool-unsandboxed-call.md +51 -51
- package/skills/torusguard/payload/rules/rag/TG-RAG-003-unpartitioned-vector-tenant-lookup.md +51 -51
- package/skills/torusguard/payload/rules/redos/TG-REDOS-001-catastrophic-exponential-backtracking.md +46 -46
- package/skills/torusguard/payload/rules/redos/TG-REDOS-002-unbounded-nested-quantifier.md +43 -43
- package/skills/torusguard/payload/scripts/audit_runner.py +108 -10
- package/skills/torusguard/payload/scripts/finding_scorer.py +43 -13
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +6 -2
- package/skills/torusguard/payload/skills/torusguard/bootstrap.py +3 -3
- package/skills/torusguard/payload/skills/torusguard-ai-guard/SKILL.md +95 -95
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +109 -84
- package/skills/torusguard/payload/skills/torusguard-container/SKILL.md +94 -94
- package/skills/torusguard/payload/skills/torusguard-git-mine/SKILL.md +92 -92
- package/skills/torusguard/payload/skills/torusguard-ocr-scan/SKILL.md +94 -94
- package/skills/torusguard/payload/skills/torusguard-redos/SKILL.md +91 -91
- package/skills/torusguard/payload/workflows/ai-guard.md +31 -31
- package/skills/torusguard/payload/workflows/audit.md +21 -17
- package/skills/torusguard/payload/workflows/container.md +29 -29
- package/skills/torusguard/payload/workflows/git-mine.md +25 -25
- package/skills/torusguard/payload/workflows/ocr-scan.md +25 -25
- package/skills/torusguard/payload/workflows/redos.md +27 -27
- package/skills/torusguard/payload/workflows/torusguard-audit.md +35 -55
- package/skills/torusguard/references/csharp-security.md +41 -41
- package/skills/torusguard/references/go-security.md +41 -41
- package/skills/torusguard/references/java-security.md +40 -40
- package/skills/torusguard/references/polyglot-security-matrix.md +25 -25
- package/skills/torusguard/references/rust-security.md +40 -40
- package/skills/torusguard-audit/SKILL.md +107 -83
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Targeted Recheck Engine
|
|
3
|
+
Executes scoped differential re-audits verifying only impacted files and adjacent trust boundaries.
|
|
4
|
+
Evaluates formal status transitions:
|
|
5
|
+
- Confirmed Fixed
|
|
6
|
+
- Partially Fixed
|
|
7
|
+
- Needs Manual Review
|
|
8
|
+
- Regressed
|
|
9
|
+
- Not Reproducible / Limited Confidence
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field, asdict
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Any, List, Optional
|
|
16
|
+
import hashlib
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RecheckOutcome(str, Enum):
|
|
21
|
+
CONFIRMED_FIXED = "Confirmed Fixed"
|
|
22
|
+
PARTIALLY_FIXED = "Partially Fixed"
|
|
23
|
+
NEEDS_MANUAL_REVIEW = "Needs Manual Review"
|
|
24
|
+
REGRESSED = "Regressed"
|
|
25
|
+
NOT_REPRODUCIBLE = "Not Reproducible / Limited Confidence"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TargetedRecheckResult:
|
|
30
|
+
finding_id: str
|
|
31
|
+
rule_id: str
|
|
32
|
+
target_file: str
|
|
33
|
+
outcome: RecheckOutcome
|
|
34
|
+
explanation: str
|
|
35
|
+
original_evidence_hash: str
|
|
36
|
+
post_fix_evidence_hash: str
|
|
37
|
+
adjacent_boundaries_checked: List[str] = field(default_factory=list)
|
|
38
|
+
regressions_detected: List[str] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
41
|
+
d = asdict(self)
|
|
42
|
+
d["outcome"] = self.outcome.value
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TargetedRechecker:
|
|
47
|
+
"""
|
|
48
|
+
Evaluates modified files and adjacent boundaries to determine recheck outcome.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def verify_finding(
|
|
53
|
+
finding_id: str,
|
|
54
|
+
rule_id: str,
|
|
55
|
+
target_file: str,
|
|
56
|
+
original_code_snippet: str,
|
|
57
|
+
post_fix_code_snippet: str,
|
|
58
|
+
is_safe_pattern_present: bool = True,
|
|
59
|
+
is_unsafe_pattern_present: bool = False,
|
|
60
|
+
introduced_new_flaws: Optional[List[str]] = None,
|
|
61
|
+
requires_manual_context: bool = False,
|
|
62
|
+
) -> TargetedRecheckResult:
|
|
63
|
+
"""
|
|
64
|
+
Computes recheck status based on AST evaluation and evidence hashes.
|
|
65
|
+
"""
|
|
66
|
+
orig_hash = hashlib.sha256(original_code_snippet.encode("utf-8")).hexdigest()
|
|
67
|
+
post_hash = hashlib.sha256(post_fix_code_snippet.encode("utf-8")).hexdigest()
|
|
68
|
+
|
|
69
|
+
# Identify adjacent trust boundaries (e.g. imports, route decorators, parent caller)
|
|
70
|
+
adjacent_boundaries = [
|
|
71
|
+
f"{target_file}:imports",
|
|
72
|
+
f"{target_file}:route_guards",
|
|
73
|
+
f"{target_file}:database_session",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
regressions = introduced_new_flaws or []
|
|
77
|
+
|
|
78
|
+
if regressions:
|
|
79
|
+
outcome = RecheckOutcome.REGRESSED
|
|
80
|
+
explanation = f"Patch introduced secondary regression: {', '.join(regressions)}."
|
|
81
|
+
elif requires_manual_context:
|
|
82
|
+
outcome = RecheckOutcome.NEEDS_MANUAL_REVIEW
|
|
83
|
+
explanation = "Patch applied, but verification requires out-of-band infrastructure or runtime authentication checks."
|
|
84
|
+
elif is_safe_pattern_present and not is_unsafe_pattern_present:
|
|
85
|
+
outcome = RecheckOutcome.CONFIRMED_FIXED
|
|
86
|
+
explanation = f"Vulnerability pattern `{rule_id}` resolved. Hardened pattern validated with 0 detected regressions."
|
|
87
|
+
elif is_safe_pattern_present and is_unsafe_pattern_present:
|
|
88
|
+
outcome = RecheckOutcome.PARTIALLY_FIXED
|
|
89
|
+
explanation = "Hardened pattern partially added, but unsafe execution pathway remains accessible."
|
|
90
|
+
elif orig_hash == post_hash:
|
|
91
|
+
outcome = RecheckOutcome.NOT_REPRODUCIBLE
|
|
92
|
+
explanation = "Target code snippet unchanged; fix not detected in active buffer."
|
|
93
|
+
else:
|
|
94
|
+
outcome = RecheckOutcome.PARTIALLY_FIXED
|
|
95
|
+
explanation = "Code was modified, but rule violation remains detectable."
|
|
96
|
+
|
|
97
|
+
return TargetedRecheckResult(
|
|
98
|
+
finding_id=finding_id,
|
|
99
|
+
rule_id=rule_id,
|
|
100
|
+
target_file=target_file,
|
|
101
|
+
outcome=outcome,
|
|
102
|
+
explanation=explanation,
|
|
103
|
+
original_evidence_hash=orig_hash,
|
|
104
|
+
post_fix_evidence_hash=post_hash,
|
|
105
|
+
adjacent_boundaries_checked=adjacent_boundaries,
|
|
106
|
+
regressions_detected=regressions,
|
|
107
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v0.7.0 Replayable Validation Traces Engine
|
|
3
|
+
Records and deterministically replays runtime verification sequences to prove
|
|
4
|
+
that vulnerabilities or remediations are reliably reproducible.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
11
|
+
from dataclasses import dataclass, asdict
|
|
12
|
+
|
|
13
|
+
from core.runtime_evidence import RedactionEngine
|
|
14
|
+
from core.runtime_validator import WebValidator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ReplayStep:
|
|
19
|
+
step_number: int
|
|
20
|
+
action_type: str # "http_request", "status_assert", "header_assert"
|
|
21
|
+
target: str
|
|
22
|
+
method: str = "GET"
|
|
23
|
+
headers: Optional[Dict[str, str]] = None
|
|
24
|
+
expected_status: Optional[int] = None
|
|
25
|
+
expected_pattern: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
28
|
+
return asdict(self)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ReplayTrace:
|
|
33
|
+
trace_id: str
|
|
34
|
+
finding_id: str
|
|
35
|
+
created_at: str
|
|
36
|
+
target_base_url: str
|
|
37
|
+
description: str
|
|
38
|
+
steps: List[ReplayStep]
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
41
|
+
d = asdict(self)
|
|
42
|
+
d["steps"] = [s.to_dict() for s in self.steps]
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ReplayManager:
|
|
47
|
+
"""
|
|
48
|
+
Manages recording and re-executing deterministic validation sequences.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, finding_id: str, target_base_url: str, description: str = ""):
|
|
52
|
+
self.trace_id = f"TRC-{finding_id}"
|
|
53
|
+
self.finding_id = finding_id
|
|
54
|
+
self.target_base_url = target_base_url
|
|
55
|
+
self.description = description
|
|
56
|
+
self.steps: List[ReplayStep] = []
|
|
57
|
+
|
|
58
|
+
def add_step(
|
|
59
|
+
self,
|
|
60
|
+
action_type: str,
|
|
61
|
+
target: str,
|
|
62
|
+
method: str = "GET",
|
|
63
|
+
headers: Optional[Dict[str, str]] = None,
|
|
64
|
+
expected_status: Optional[int] = None,
|
|
65
|
+
expected_pattern: Optional[str] = None
|
|
66
|
+
):
|
|
67
|
+
sanitized_headers = RedactionEngine.redact_headers(headers)
|
|
68
|
+
step = ReplayStep(
|
|
69
|
+
step_number=len(self.steps) + 1,
|
|
70
|
+
action_type=action_type,
|
|
71
|
+
target=target,
|
|
72
|
+
method=method.upper(),
|
|
73
|
+
headers=sanitized_headers,
|
|
74
|
+
expected_status=expected_status,
|
|
75
|
+
expected_pattern=expected_pattern
|
|
76
|
+
)
|
|
77
|
+
self.steps.append(step)
|
|
78
|
+
|
|
79
|
+
def to_trace(self) -> ReplayTrace:
|
|
80
|
+
return ReplayTrace(
|
|
81
|
+
trace_id=self.trace_id,
|
|
82
|
+
finding_id=self.finding_id,
|
|
83
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
84
|
+
target_base_url=self.target_base_url,
|
|
85
|
+
description=self.description,
|
|
86
|
+
steps=self.steps
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def write_artifacts(self, run_dir: Path) -> Tuple[Path, Path]:
|
|
90
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
json_file = run_dir / "replay.json"
|
|
92
|
+
md_file = run_dir / "replay.md"
|
|
93
|
+
|
|
94
|
+
trace = self.to_trace()
|
|
95
|
+
|
|
96
|
+
# 1. Write replay.json
|
|
97
|
+
with open(json_file, "w", encoding="utf-8") as f:
|
|
98
|
+
json.dump(trace.to_dict(), f, indent=2)
|
|
99
|
+
|
|
100
|
+
# 2. Write replay.md
|
|
101
|
+
lines = [
|
|
102
|
+
f"# TorusGuard v0.7.0 Replayable Verification Trace: `{self.finding_id}`",
|
|
103
|
+
f"\n**Trace ID:** `{self.trace_id}`",
|
|
104
|
+
f"**Target Base URL:** `{self.target_base_url}`",
|
|
105
|
+
f"**Total Steps:** `{len(self.steps)}`",
|
|
106
|
+
f"**Description:** {self.description or 'Deterministic runtime verification trace.'}",
|
|
107
|
+
"\n---",
|
|
108
|
+
"\n## 🔁 Execution Steps\n",
|
|
109
|
+
"| Step | Action | Method | Target Path / URI | Expected Status | Assertion Pattern |",
|
|
110
|
+
"|:---:|---|:---:|---|:---:|---|",
|
|
111
|
+
]
|
|
112
|
+
for s in self.steps:
|
|
113
|
+
lines.append(
|
|
114
|
+
f"| {s.step_number} | `{s.action_type}` | `{s.method}` | `{s.target}` | `{s.expected_status or '-'}` | `{s.expected_pattern or '-'}` |"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
lines.extend([
|
|
118
|
+
"\n---",
|
|
119
|
+
"\n## 🚀 Replay Instructions",
|
|
120
|
+
"To execute this deterministic verification trace via the TorusGuard CLI:",
|
|
121
|
+
"```bash",
|
|
122
|
+
f"python -m core.replay_runner --trace runs/<run-id>/replay.json",
|
|
123
|
+
"```"
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
with open(md_file, "w", encoding="utf-8") as f:
|
|
127
|
+
f.write("\n".join(lines) + "\n")
|
|
128
|
+
|
|
129
|
+
return json_file, md_file
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def execute_replay(cls, trace: ReplayTrace, validator: WebValidator) -> Dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Re-executes the trace steps against the target application to verify reproducibility.
|
|
135
|
+
"""
|
|
136
|
+
results = []
|
|
137
|
+
all_passed = True
|
|
138
|
+
|
|
139
|
+
for step in trace.steps:
|
|
140
|
+
target_url = step.target
|
|
141
|
+
if not target_url.startswith("http"):
|
|
142
|
+
target_url = f"{trace.target_base_url.rstrip('/')}/{step.target.lstrip('/')}"
|
|
143
|
+
|
|
144
|
+
status, headers, body, decision = validator.execute_probe(
|
|
145
|
+
finding_id=trace.finding_id,
|
|
146
|
+
cluster_id="cluster-replay",
|
|
147
|
+
method=step.method,
|
|
148
|
+
target_url=target_url,
|
|
149
|
+
headers=step.headers,
|
|
150
|
+
expected_status=step.expected_status
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
step_passed = True
|
|
154
|
+
reason = "Success"
|
|
155
|
+
|
|
156
|
+
if step.expected_status and status != step.expected_status:
|
|
157
|
+
step_passed = False
|
|
158
|
+
reason = f"Status mismatch: expected {step.expected_status}, got {status}"
|
|
159
|
+
all_passed = False
|
|
160
|
+
|
|
161
|
+
if step.expected_pattern and step.expected_pattern not in body:
|
|
162
|
+
step_passed = False
|
|
163
|
+
reason = f"Pattern mismatch: '{step.expected_pattern}' not found in response"
|
|
164
|
+
all_passed = False
|
|
165
|
+
|
|
166
|
+
results.append({
|
|
167
|
+
"step": step.step_number,
|
|
168
|
+
"passed": step_passed,
|
|
169
|
+
"status_code": status,
|
|
170
|
+
"reason": reason
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"trace_id": trace.trace_id,
|
|
175
|
+
"reproducible": all_passed,
|
|
176
|
+
"total_steps": len(trace.steps),
|
|
177
|
+
"step_results": results
|
|
178
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard Rules Registry
|
|
3
|
+
Centralized repository loading and validating canonical security rules
|
|
4
|
+
across all 22 architectural rule families.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CANONICAL_FAMILIES = [
|
|
14
|
+
"TG-SEC", "TG-AUTH", "TG-DB", "TG-INPUT", "TG-RATE", "TG-AGENT",
|
|
15
|
+
"TG-SSRF", "TG-WEBHOOK", "TG-WS", "TG-CSRF", "TG-GQL", "TG-SUPPLY",
|
|
16
|
+
"TG-BIZ", "TG-CACHE", "TG-CLIENT", "TG-PLATFORM", "TG-DIFF",
|
|
17
|
+
"TG-EDGE", "TG-CONT", "TG-GIT", "TG-REDOS", "TG-RAG"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CanonicalRule:
|
|
23
|
+
rule_id: str
|
|
24
|
+
family: str
|
|
25
|
+
title: str
|
|
26
|
+
severity: str
|
|
27
|
+
category: str
|
|
28
|
+
description: str
|
|
29
|
+
remediation_guidance: str = ""
|
|
30
|
+
file_path: str = ""
|
|
31
|
+
detection_patterns: List[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
34
|
+
return asdict(self)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RulesRegistry:
|
|
38
|
+
"""Centralized loader and validator for TorusGuard rules."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, rules_root: Optional[Path] = None):
|
|
41
|
+
self.rules_root = rules_root or self._locate_rules_dir()
|
|
42
|
+
self.rules: Dict[str, CanonicalRule] = {}
|
|
43
|
+
self.load()
|
|
44
|
+
|
|
45
|
+
def _locate_rules_dir(self) -> Path:
|
|
46
|
+
base = Path(__file__).resolve().parent.parent / ".torusguard" / "rules"
|
|
47
|
+
if base.exists():
|
|
48
|
+
return base
|
|
49
|
+
return Path.cwd() / ".torusguard" / "rules"
|
|
50
|
+
|
|
51
|
+
def load(self) -> None:
|
|
52
|
+
if not self.rules_root.exists():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
for p in self.rules_root.glob("**/*.md"):
|
|
56
|
+
if p.name.startswith("README") or p.name.startswith("TORUSGUARD"):
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
60
|
+
rule = self._parse_rule_markdown(p, content)
|
|
61
|
+
if rule:
|
|
62
|
+
self.rules[rule.rule_id] = rule
|
|
63
|
+
|
|
64
|
+
def _parse_rule_markdown(self, path: Path, content: str) -> Optional[CanonicalRule]:
|
|
65
|
+
filename = path.stem
|
|
66
|
+
# Extract rule_id from filename or first heading
|
|
67
|
+
rule_id_match = re.search(r"(TG-[A-Z]+-\d{3})", filename)
|
|
68
|
+
if not rule_id_match:
|
|
69
|
+
rule_id_match = re.search(r"#\s*(TG-[A-Z]+-\d{3})", content)
|
|
70
|
+
if not rule_id_match:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
rule_id = rule_id_match.group(1)
|
|
74
|
+
family = rule_id.rsplit("-", 1)[0]
|
|
75
|
+
|
|
76
|
+
# Extract title
|
|
77
|
+
title = filename.replace(rule_id, "").strip(" -_").replace("-", " ").title()
|
|
78
|
+
title_match = re.search(r"#\s*(?:TG-[A-Z]+-\d{3}[\s:—-]+)?(.+)", content)
|
|
79
|
+
if title_match:
|
|
80
|
+
cand_title = title_match.group(1).strip()
|
|
81
|
+
if cand_title and not cand_title.startswith("TG-"):
|
|
82
|
+
title = cand_title
|
|
83
|
+
|
|
84
|
+
# Severity
|
|
85
|
+
severity = "High"
|
|
86
|
+
sev_match = re.search(r"(?i)severity[:\s*|`]+(Critical|High|Medium|Low)", content)
|
|
87
|
+
if sev_match:
|
|
88
|
+
severity = sev_match.group(1).capitalize()
|
|
89
|
+
|
|
90
|
+
# Description
|
|
91
|
+
desc = ""
|
|
92
|
+
desc_match = re.search(r"(?i)(?:###?\s*Description|##\s*Overview|Summary)[:\s]*\n([^\n#]+)", content)
|
|
93
|
+
if desc_match:
|
|
94
|
+
desc = desc_match.group(1).strip()
|
|
95
|
+
else:
|
|
96
|
+
desc = title
|
|
97
|
+
|
|
98
|
+
# Remediation
|
|
99
|
+
remed = ""
|
|
100
|
+
rem_match = re.search(r"(?i)(?:###?\s*Remediation|###?\s*Mitigation|Fix)[:\s]*\n([^\n#]+)", content)
|
|
101
|
+
if rem_match:
|
|
102
|
+
remed = rem_match.group(1).strip()
|
|
103
|
+
|
|
104
|
+
return CanonicalRule(
|
|
105
|
+
rule_id=rule_id,
|
|
106
|
+
family=family,
|
|
107
|
+
title=title,
|
|
108
|
+
severity=severity,
|
|
109
|
+
category=family.replace("TG-", "").lower(),
|
|
110
|
+
description=desc,
|
|
111
|
+
remediation_guidance=remed,
|
|
112
|
+
file_path=str(path)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def get_rule(self, rule_id: str) -> Optional[CanonicalRule]:
|
|
116
|
+
return self.rules.get(rule_id)
|
|
117
|
+
|
|
118
|
+
def get_rules_by_family(self, family: str) -> List[CanonicalRule]:
|
|
119
|
+
fam_norm = family.upper()
|
|
120
|
+
if not fam_norm.startswith("TG-"):
|
|
121
|
+
fam_norm = f"TG-{fam_norm}"
|
|
122
|
+
return [r for r in self.rules.values() if r.family == fam_norm]
|
|
123
|
+
|
|
124
|
+
def get_rules_by_severity(self, severity: str) -> List[CanonicalRule]:
|
|
125
|
+
return [r for r in self.rules.values() if r.severity.lower() == severity.lower()]
|
|
126
|
+
|
|
127
|
+
def all_rules(self) -> List[CanonicalRule]:
|
|
128
|
+
return sorted(self.rules.values(), key=lambda r: r.rule_id)
|
|
129
|
+
|
|
130
|
+
def count(self) -> int:
|
|
131
|
+
return len(self.rules)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
class RunFolder:
|
|
7
|
+
"""
|
|
8
|
+
Manages the per-run folder state and artifacts for a TorusGuard execution.
|
|
9
|
+
Creates a unique directory (e.g., .torusguard/runs/run-YYYYMMDD-HHMMSS) containing
|
|
10
|
+
findings.md, remediation.md, recheck.md, metadata.json, patches/, and logs/.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, output_root: str = ".torusguard/runs", run_name: Optional[str] = None):
|
|
13
|
+
self.output_root = Path(output_root).resolve()
|
|
14
|
+
|
|
15
|
+
if run_name:
|
|
16
|
+
self.run_id = run_name
|
|
17
|
+
else:
|
|
18
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
19
|
+
self.run_id = f"run-{timestamp}"
|
|
20
|
+
|
|
21
|
+
self.run_path = self.output_root / self.run_id
|
|
22
|
+
|
|
23
|
+
# Subdirectories
|
|
24
|
+
self.patches_dir = self.run_path / "patches"
|
|
25
|
+
self.logs_dir = self.run_path / "logs"
|
|
26
|
+
|
|
27
|
+
# Files
|
|
28
|
+
self.findings_file = self.run_path / "findings.md"
|
|
29
|
+
self.remediation_file = self.run_path / "remediation.md"
|
|
30
|
+
self.recheck_file = self.run_path / "recheck.md"
|
|
31
|
+
self.metadata_file = self.run_path / "metadata.json"
|
|
32
|
+
|
|
33
|
+
self._initialize_folders()
|
|
34
|
+
self._write_metadata()
|
|
35
|
+
|
|
36
|
+
def _initialize_folders(self):
|
|
37
|
+
self.run_path.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
self.patches_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
def _write_metadata(self):
|
|
42
|
+
metadata = {
|
|
43
|
+
"run_id": self.run_id,
|
|
44
|
+
"created_at": datetime.now().isoformat(),
|
|
45
|
+
"status": "initialized"
|
|
46
|
+
}
|
|
47
|
+
with open(self.metadata_file, "w", encoding="utf-8") as f:
|
|
48
|
+
json.dump(metadata, f, indent=2)
|
|
49
|
+
|
|
50
|
+
def get_path(self, item: str) -> Path:
|
|
51
|
+
"""Helper to get a path within the run structure."""
|
|
52
|
+
paths = {
|
|
53
|
+
"findings.md": self.findings_file,
|
|
54
|
+
"remediation.md": self.remediation_file,
|
|
55
|
+
"recheck.md": self.recheck_file,
|
|
56
|
+
"metadata.json": self.metadata_file,
|
|
57
|
+
"patches": self.patches_dir,
|
|
58
|
+
"logs": self.logs_dir
|
|
59
|
+
}
|
|
60
|
+
return paths.get(item, self.run_path / item)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Run Folder & Artifact Registry Manager
|
|
3
|
+
Creates and manages isolated run directories under runs/<run-id>/ containing:
|
|
4
|
+
- manifest.json
|
|
5
|
+
- summary.md
|
|
6
|
+
- findings.md
|
|
7
|
+
- remediation.md
|
|
8
|
+
- apply-plan.md
|
|
9
|
+
- recheck.md
|
|
10
|
+
- evidence.json
|
|
11
|
+
- diff-summary.md
|
|
12
|
+
- changed-files.txt
|
|
13
|
+
- sarif.json (optional)
|
|
14
|
+
- logs/
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional, Dict, Any, List
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RunManager:
|
|
25
|
+
"""
|
|
26
|
+
Manages the lifecycle of an isolated TorusGuard run directory.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
base_dir: Optional[Path] = None,
|
|
32
|
+
target_name: str = "workspace",
|
|
33
|
+
command: str = "audit",
|
|
34
|
+
run_id: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
self.base_dir = base_dir or Path(".torusguard/runs")
|
|
37
|
+
self.target_name = target_name
|
|
38
|
+
self.command = command
|
|
39
|
+
|
|
40
|
+
timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
|
|
41
|
+
self.run_id = run_id or f"run-{timestamp}-{target_name}"
|
|
42
|
+
self.run_path = self.base_dir / self.run_id
|
|
43
|
+
|
|
44
|
+
# Subdirectories
|
|
45
|
+
self.logs_dir = self.run_path / "logs"
|
|
46
|
+
self.patches_dir = self.run_path / "patches"
|
|
47
|
+
self.bundles_dir = self.run_path / "bundles"
|
|
48
|
+
|
|
49
|
+
# Standard Artifact Paths
|
|
50
|
+
self.manifest_file = self.run_path / "manifest.json"
|
|
51
|
+
self.summary_file = self.run_path / "summary.md"
|
|
52
|
+
self.findings_file = self.run_path / "findings.md"
|
|
53
|
+
self.remediation_file = self.run_path / "remediation.md"
|
|
54
|
+
self.apply_plan_file = self.run_path / "apply-plan.md"
|
|
55
|
+
self.recheck_file = self.run_path / "recheck.md"
|
|
56
|
+
self.evidence_file = self.run_path / "evidence.json"
|
|
57
|
+
self.diff_summary_file = self.run_path / "diff-summary.md"
|
|
58
|
+
self.changed_files_file = self.run_path / "changed-files.txt"
|
|
59
|
+
self.sarif_file = self.run_path / "sarif.json"
|
|
60
|
+
|
|
61
|
+
self._init_directories()
|
|
62
|
+
|
|
63
|
+
def _init_directories(self):
|
|
64
|
+
self.run_path.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
self.patches_dir.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
self.bundles_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
def get_git_commit(self) -> Optional[str]:
|
|
70
|
+
try:
|
|
71
|
+
res = subprocess.run(
|
|
72
|
+
["git", "rev-parse", "HEAD"],
|
|
73
|
+
capture_output=True,
|
|
74
|
+
text=True,
|
|
75
|
+
timeout=5,
|
|
76
|
+
)
|
|
77
|
+
if res.returncode == 0:
|
|
78
|
+
return res.stdout.strip()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def write_manifest(
|
|
84
|
+
self,
|
|
85
|
+
status_counts: Optional[Dict[str, int]] = None,
|
|
86
|
+
extra_meta: Optional[Dict[str, Any]] = None,
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Writes the standard manifest.json.
|
|
90
|
+
"""
|
|
91
|
+
counts = status_counts or {
|
|
92
|
+
"total_findings": 0,
|
|
93
|
+
"confirmed": 0,
|
|
94
|
+
"high_confidence": 0,
|
|
95
|
+
"needs_review": 0,
|
|
96
|
+
"remediated": 0,
|
|
97
|
+
"verified_fixed": 0,
|
|
98
|
+
"regressed": 0,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
manifest_data = {
|
|
102
|
+
"version": "v0.6.3",
|
|
103
|
+
"run_id": self.run_id,
|
|
104
|
+
"target_name": self.target_name,
|
|
105
|
+
"command": self.command,
|
|
106
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
107
|
+
"git_commit": self.get_git_commit(),
|
|
108
|
+
"status_counts": counts,
|
|
109
|
+
"artifacts": {
|
|
110
|
+
"manifest": "manifest.json",
|
|
111
|
+
"summary": "summary.md",
|
|
112
|
+
"findings": "findings.md",
|
|
113
|
+
"remediation": "remediation.md",
|
|
114
|
+
"apply_plan": "apply-plan.md",
|
|
115
|
+
"recheck": "recheck.md",
|
|
116
|
+
"evidence": "evidence.json",
|
|
117
|
+
"diff_summary": "diff-summary.md",
|
|
118
|
+
"changed_files": "changed-files.txt",
|
|
119
|
+
"sarif": "sarif.json",
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if extra_meta:
|
|
124
|
+
manifest_data.update(extra_meta)
|
|
125
|
+
|
|
126
|
+
with open(self.manifest_file, "w", encoding="utf-8") as f:
|
|
127
|
+
json.dump(manifest_data, f, indent=2)
|
|
128
|
+
|
|
129
|
+
def write_evidence(self, evidence_data: List[Dict[str, Any]]):
|
|
130
|
+
with open(self.evidence_file, "w", encoding="utf-8") as f:
|
|
131
|
+
json.dump(evidence_data, f, indent=2)
|
|
132
|
+
|
|
133
|
+
def write_changed_files(self, files: List[str]):
|
|
134
|
+
with open(self.changed_files_file, "w", encoding="utf-8") as f:
|
|
135
|
+
f.write("\n".join(files) + "\n")
|
|
136
|
+
|
|
137
|
+
def write_summary(self, content: str):
|
|
138
|
+
with open(self.summary_file, "w", encoding="utf-8") as f:
|
|
139
|
+
f.write(content)
|
|
140
|
+
|
|
141
|
+
def write_findings(self, content: str):
|
|
142
|
+
with open(self.findings_file, "w", encoding="utf-8") as f:
|
|
143
|
+
f.write(content)
|
|
144
|
+
|
|
145
|
+
def write_remediation(self, content: str):
|
|
146
|
+
with open(self.remediation_file, "w", encoding="utf-8") as f:
|
|
147
|
+
f.write(content)
|
|
148
|
+
|
|
149
|
+
def write_apply_plan(self, content: str):
|
|
150
|
+
with open(self.apply_plan_file, "w", encoding="utf-8") as f:
|
|
151
|
+
f.write(content)
|
|
152
|
+
|
|
153
|
+
def write_diff_summary(self, content: str):
|
|
154
|
+
with open(self.diff_summary_file, "w", encoding="utf-8") as f:
|
|
155
|
+
f.write(content)
|
|
156
|
+
|
|
157
|
+
def write_recheck(self, content: str):
|
|
158
|
+
with open(self.recheck_file, "w", encoding="utf-8") as f:
|
|
159
|
+
f.write(content)
|
|
160
|
+
|
|
161
|
+
def write_sarif(self, sarif_dict: Dict[str, Any]):
|
|
162
|
+
with open(self.sarif_file, "w", encoding="utf-8") as f:
|
|
163
|
+
json.dump(sarif_dict, f, indent=2)
|