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,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), [])
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Root-Cause Clustering Engine
|
|
3
|
+
Groups individual static-analysis findings into cohesive root-cause clusters
|
|
4
|
+
so engineering teams can remediate systemic architectural issues rather than chasing isolated alerts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
import hashlib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Canonical Cluster Taxonomy Definitions
|
|
13
|
+
KNOWN_ROOT_CAUSES = {
|
|
14
|
+
"TG-DB-004": {
|
|
15
|
+
"cluster_id": "cluster-tenant-isolation",
|
|
16
|
+
"title": "Missing Multi-Tenant Query Scoping & Model Isolation",
|
|
17
|
+
"shared_remediation_path": "Implement tenant-aware BaseManager / default queryset filtering or tenancy middleware context.",
|
|
18
|
+
"shared_verification_plan": "Execute tenant cross-boundary query assertions and recheck query builders."
|
|
19
|
+
},
|
|
20
|
+
"TG-INPUT-006": {
|
|
21
|
+
"cluster_id": "cluster-path-traversal",
|
|
22
|
+
"title": "Unsafe File Upload Storage & Path Traversal Boundaries",
|
|
23
|
+
"shared_remediation_path": "Sanitize filenames using secure_filename() and enforce safe directory resolution with Path.resolve().",
|
|
24
|
+
"shared_verification_plan": "Run path traversal payload test suite and verify storage isolation."
|
|
25
|
+
},
|
|
26
|
+
"TG-INPUT-005": {
|
|
27
|
+
"cluster_id": "cluster-template-escaping",
|
|
28
|
+
"title": "Disabled Template Autoescaping & Unsafe HTML Rendering",
|
|
29
|
+
"shared_remediation_path": "Remove explicit mark_safe() / |safe filters and use autoescaped context variables.",
|
|
30
|
+
"shared_verification_plan": "Execute XSS payload injection test against rendered view outputs."
|
|
31
|
+
},
|
|
32
|
+
"TG-AUTH-008": {
|
|
33
|
+
"cluster_id": "cluster-header-trust",
|
|
34
|
+
"title": "Untrusted Client Header Trust & Role/Tenant Injection",
|
|
35
|
+
"shared_remediation_path": "Derive user identity and role scopes exclusively from cryptographically signed session tokens or trusted gateways.",
|
|
36
|
+
"shared_verification_plan": "Send spoofed client headers (X-User-Role, X-Tenant-ID) and assert rejection."
|
|
37
|
+
},
|
|
38
|
+
"TG-AUTH-007": {
|
|
39
|
+
"cluster_id": "cluster-idor-scoping",
|
|
40
|
+
"title": "Insecure Direct Object Reference (IDOR) on Primary Keys",
|
|
41
|
+
"shared_remediation_path": "Scope database queries with user_id or account ownership filters before returning model instances.",
|
|
42
|
+
"shared_verification_plan": "Run IDOR authorization matrix tests across test accounts."
|
|
43
|
+
},
|
|
44
|
+
"TG-RATE-001": {
|
|
45
|
+
"cluster_id": "cluster-rate-limiting",
|
|
46
|
+
"title": "Unbounded Resource Consumption & Missing Endpoint Throttling",
|
|
47
|
+
"shared_remediation_path": "Apply Redis/in-memory rate limiting middleware or DRF Throttling classes.",
|
|
48
|
+
"shared_verification_plan": "Execute burst traffic simulation and verify 429 Too Many Requests response."
|
|
49
|
+
},
|
|
50
|
+
"TG-SSRF-001": {
|
|
51
|
+
"cluster_id": "cluster-ssrf-network",
|
|
52
|
+
"title": "Unvalidated Outbound HTTP Requests & Network Boundary Leakage",
|
|
53
|
+
"shared_remediation_path": "Validate destination URLs against strict allowlists and block internal IP ranges (127.0.0.1, 169.254.169.254).",
|
|
54
|
+
"shared_verification_plan": "Attempt outbound requests to loopback and link-local metadata endpoints."
|
|
55
|
+
},
|
|
56
|
+
"TG-WEBHOOK-001": {
|
|
57
|
+
"cluster_id": "cluster-webhook-auth",
|
|
58
|
+
"title": "Unverified Inbound Webhook Signatures & Replay Vulnerability",
|
|
59
|
+
"shared_remediation_path": "Verify HMAC signatures using timing-safe comparisons and enforce timestamp freshness bounds.",
|
|
60
|
+
"shared_verification_plan": "Send unsigned and replay webhook payloads and confirm 401/403 rejection."
|
|
61
|
+
},
|
|
62
|
+
"TG-SEC-001": {
|
|
63
|
+
"cluster_id": "cluster-secrets",
|
|
64
|
+
"title": "Hardcoded Secrets & Sensitive Environment Configuration Exposure",
|
|
65
|
+
"shared_remediation_path": "Extract secrets into environment variables (.env / secrets manager) and exclude from version control.",
|
|
66
|
+
"shared_verification_plan": "Audit git history and scan source files for credential patterns."
|
|
67
|
+
},
|
|
68
|
+
"TG-AGENT-001": {
|
|
69
|
+
"cluster_id": "cluster-prompt-injection",
|
|
70
|
+
"title": "AI Agent User Prompt Concatenation & System Prompt Override",
|
|
71
|
+
"shared_remediation_path": "Isolate untrusted user input within explicit inert XML delimiters or structured user-role message arrays.",
|
|
72
|
+
"shared_verification_plan": "Execute prompt injection canary payloads and verify refusal to override system directives."
|
|
73
|
+
},
|
|
74
|
+
"TG-CSRF-001": {
|
|
75
|
+
"cluster_id": "cluster-csrf-missing",
|
|
76
|
+
"title": "Cross-Site Request Forgery (CSRF) & Missing SameSite Cookie Protection",
|
|
77
|
+
"shared_remediation_path": "Enforce SameSite=Lax/Strict on session cookies and validate cryptographic anti-CSRF tokens on state mutations.",
|
|
78
|
+
"shared_verification_plan": "Attempt cross-origin state-changing POST requests without CSRF token."
|
|
79
|
+
},
|
|
80
|
+
"TG-GQL-001": {
|
|
81
|
+
"cluster_id": "cluster-graphql-abuse",
|
|
82
|
+
"title": "Unbounded GraphQL Query Depth & Production Introspection Exposure",
|
|
83
|
+
"shared_remediation_path": "Configure GraphQL query depth limiting (max depth 6) and disable schema introspection in production.",
|
|
84
|
+
"shared_verification_plan": "Send deeply nested recursive query and introspection requests to production endpoint."
|
|
85
|
+
},
|
|
86
|
+
"TG-SUPPLY-001": {
|
|
87
|
+
"cluster_id": "cluster-supply-chain",
|
|
88
|
+
"title": "Supply Chain Vulnerability & Unpinned CI/CD Action Hashes",
|
|
89
|
+
"shared_remediation_path": "Pin GitHub Actions to full immutable commit SHAs and enforce lockfile integrity verification in CI.",
|
|
90
|
+
"shared_verification_plan": "Scan workflow files for mutable tag references and verify lockfile checksums."
|
|
91
|
+
},
|
|
92
|
+
"TG-BIZ-001": {
|
|
93
|
+
"cluster_id": "cluster-business-logic",
|
|
94
|
+
"title": "Business Logic Invariant Violation & Negative Quantity/Discount Bypass",
|
|
95
|
+
"shared_remediation_path": "Assert non-negative amount invariants, enforce transactional locks, and cap discounts server-side.",
|
|
96
|
+
"shared_verification_plan": "Submit negative amount and coupon boundary payloads in transactional API endpoints."
|
|
97
|
+
},
|
|
98
|
+
"TG-CACHE-001": {
|
|
99
|
+
"cluster_id": "cluster-cache-poisoning",
|
|
100
|
+
"title": "Web Cache Poisoning & Missing Cache-Control on Sensitive Responses",
|
|
101
|
+
"shared_remediation_path": "Sanitize unkeyed HTTP request headers and apply 'Cache-Control: no-store' on private or authenticated responses.",
|
|
102
|
+
"shared_verification_plan": "Send unkeyed headers (X-Forwarded-Host) and inspect cached responses."
|
|
103
|
+
},
|
|
104
|
+
"TG-WS-001": {
|
|
105
|
+
"cluster_id": "cluster-websocket-auth",
|
|
106
|
+
"title": "Missing WebSocket Handshake Authentication & Origin Verification",
|
|
107
|
+
"shared_remediation_path": "Authenticate client credentials during WebSocket upgrade and reject connections with unwhitelisted Origin headers.",
|
|
108
|
+
"shared_verification_plan": "Connect from untrusted origin and assert handshake rejection with 403 Forbidden."
|
|
109
|
+
},
|
|
110
|
+
"TG-EDGE-001": {
|
|
111
|
+
"cluster_id": "cluster-edge-abuse",
|
|
112
|
+
"title": "Serverless Subrequest Fan-Out Explosion & Missing Timeout Bounds",
|
|
113
|
+
"shared_remediation_path": "Enforce concurrency caps on edge subrequests and configure strict execution timeouts (<= 15s).",
|
|
114
|
+
"shared_verification_plan": "Simulate high subrequest fan-out and assert bounded concurrency."
|
|
115
|
+
},
|
|
116
|
+
"TG-CONT-001": {
|
|
117
|
+
"cluster_id": "cluster-container-hardening",
|
|
118
|
+
"title": "Container Security Misconfiguration & Root User Execution",
|
|
119
|
+
"shared_remediation_path": "Define non-root USER in Dockerfile, avoid mounting /var/run/docker.sock, and disallow privileged mode.",
|
|
120
|
+
"shared_verification_plan": "Inspect container image metadata and assert non-root UID."
|
|
121
|
+
},
|
|
122
|
+
"TG-GIT-001": {
|
|
123
|
+
"cluster_id": "cluster-git-secret-mining",
|
|
124
|
+
"title": "Committed Credentials & Leaked Tokens in Git Commit History",
|
|
125
|
+
"shared_remediation_path": "Rotate exposed keys immediately, purge secret commits using git-filter-repo, and install pre-commit secret hooks.",
|
|
126
|
+
"shared_verification_plan": "Mine git commit logs for high-entropy tokens and regex matches."
|
|
127
|
+
},
|
|
128
|
+
"TG-REDOS-001": {
|
|
129
|
+
"cluster_id": "cluster-regex-backtracking",
|
|
130
|
+
"title": "Regular Expression Denial of Service (ReDoS) via Nested Quantifiers",
|
|
131
|
+
"shared_remediation_path": "Refactor regex patterns to eliminate nested quantifiers (e.g. (a+)+) or enforce execution timeouts.",
|
|
132
|
+
"shared_verification_plan": "Execute polynomial/exponential adversarial string payloads against regex engine."
|
|
133
|
+
},
|
|
134
|
+
"TG-RAG-001": {
|
|
135
|
+
"cluster_id": "cluster-rag-tenant-leak",
|
|
136
|
+
"title": "RAG Pipeline Vector Database Multi-Tenant Isolation Leakage",
|
|
137
|
+
"shared_remediation_path": "Always include tenant_id or user_id in vector similarity filter metadata (e.g. filter={'tenant_id': tid}).",
|
|
138
|
+
"shared_verification_plan": "Query vector database with tenant A context requesting tenant B documents."
|
|
139
|
+
},
|
|
140
|
+
"TG-INPUT-007": {
|
|
141
|
+
"cluster_id": "cluster-open-redirect",
|
|
142
|
+
"title": "Unvalidated URL Redirection (Open Redirect)",
|
|
143
|
+
"shared_remediation_path": "Validate destination URLs against an allowlist of trusted domains and restrict to relative paths.",
|
|
144
|
+
"shared_verification_plan": "Test redirect endpoints with external and protocol-relative URLs."
|
|
145
|
+
},
|
|
146
|
+
"TG-INPUT-008": {
|
|
147
|
+
"cluster_id": "cluster-deserialization",
|
|
148
|
+
"title": "Insecure Object Deserialization via Untrusted Streams",
|
|
149
|
+
"shared_remediation_path": "Avoid pickle/yaml.load with untrusted input; use safe serialization formats (JSON) or SafeLoader.",
|
|
150
|
+
"shared_verification_plan": "Submit serialized gadget payload and confirm safe parser rejection."
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class RootCauseCluster:
|
|
158
|
+
cluster_id: str
|
|
159
|
+
title: str
|
|
160
|
+
primary_rule: str
|
|
161
|
+
affected_files: List[str] = field(default_factory=list)
|
|
162
|
+
affected_locations: List[str] = field(default_factory=list)
|
|
163
|
+
finding_ids: List[str] = field(default_factory=list)
|
|
164
|
+
shared_remediation_path: str = ""
|
|
165
|
+
shared_verification_plan: str = ""
|
|
166
|
+
risk_severity: str = "High"
|
|
167
|
+
hotspot_module: str = ""
|
|
168
|
+
is_high_density: bool = False
|
|
169
|
+
|
|
170
|
+
def add_finding(self, finding_id: str, file_path: str, location_str: str, severity: str = "High"):
|
|
171
|
+
if finding_id not in self.finding_ids:
|
|
172
|
+
self.finding_ids.append(finding_id)
|
|
173
|
+
if file_path not in self.affected_files:
|
|
174
|
+
self.affected_files.append(file_path)
|
|
175
|
+
if location_str not in self.affected_locations:
|
|
176
|
+
self.affected_locations.append(location_str)
|
|
177
|
+
|
|
178
|
+
# Update hotspot module (top directory)
|
|
179
|
+
parts = file_path.replace("\\", "/").split("/")
|
|
180
|
+
if len(parts) > 1:
|
|
181
|
+
self.hotspot_module = "/".join(parts[:2])
|
|
182
|
+
else:
|
|
183
|
+
self.hotspot_module = parts[0]
|
|
184
|
+
|
|
185
|
+
# Density threshold (> 5 findings in one cluster is high density)
|
|
186
|
+
if len(self.finding_ids) >= 5:
|
|
187
|
+
self.is_high_density = True
|
|
188
|
+
|
|
189
|
+
# Escalate cluster severity if higher
|
|
190
|
+
severity_order = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1, "Informational": 0}
|
|
191
|
+
if severity_order.get(severity, 0) > severity_order.get(self.risk_severity, 0):
|
|
192
|
+
self.risk_severity = severity
|
|
193
|
+
|
|
194
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
195
|
+
return asdict(self)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Generated / Vendor File Exclusion Patterns
|
|
199
|
+
IGNORED_PATTERNS = [
|
|
200
|
+
"migrations/", "node_modules/", "dist/", "build/", "vendor/",
|
|
201
|
+
".venv/", "venv/", ".min.js", ".min.css", ".pb.go", "_pb2.py",
|
|
202
|
+
"bundle.js", ".map"
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def is_generated_file(file_path: str) -> bool:
|
|
207
|
+
norm = file_path.replace("\\", "/").lower()
|
|
208
|
+
return any(p in norm for p in IGNORED_PATTERNS)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ClusteringEngine:
|
|
212
|
+
"""
|
|
213
|
+
Analyzes findings and groups them into root-cause clusters with scale and density metrics.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def cluster_findings(
|
|
218
|
+
findings: List[Dict[str, Any]],
|
|
219
|
+
filter_generated: bool = False
|
|
220
|
+
) -> List[RootCauseCluster]:
|
|
221
|
+
clusters: Dict[str, RootCauseCluster] = {}
|
|
222
|
+
|
|
223
|
+
for f in findings:
|
|
224
|
+
target = f.get("target", {})
|
|
225
|
+
file_path = target.get("file_path", "unknown")
|
|
226
|
+
|
|
227
|
+
if filter_generated and is_generated_file(file_path):
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
rule_id = f.get("rule_id", "TG-GENERIC")
|
|
231
|
+
finding_id = f.get("finding_id", "unknown")
|
|
232
|
+
start_line = target.get("line_start", 0)
|
|
233
|
+
end_line = target.get("line_end", 0)
|
|
234
|
+
loc_str = f"{file_path}:{start_line}-{end_line}"
|
|
235
|
+
severity = f.get("severity", "High")
|
|
236
|
+
|
|
237
|
+
# Determine cluster mapping
|
|
238
|
+
if rule_id in KNOWN_ROOT_CAUSES:
|
|
239
|
+
meta = KNOWN_ROOT_CAUSES[rule_id]
|
|
240
|
+
cid = meta["cluster_id"]
|
|
241
|
+
title = meta["title"]
|
|
242
|
+
rem_path = meta["shared_remediation_path"]
|
|
243
|
+
ver_plan = meta["shared_verification_plan"]
|
|
244
|
+
else:
|
|
245
|
+
cid = f"cluster-{rule_id.lower().replace('tg-', '')}"
|
|
246
|
+
title = f"Systemic {f.get('title', rule_id)} Issues"
|
|
247
|
+
rem_path = "Apply framework-native security controls as documented in rule reference."
|
|
248
|
+
ver_plan = "Re-audit all affected components with /torusguard recheck."
|
|
249
|
+
|
|
250
|
+
if cid not in clusters:
|
|
251
|
+
clusters[cid] = RootCauseCluster(
|
|
252
|
+
cluster_id=cid,
|
|
253
|
+
title=title,
|
|
254
|
+
primary_rule=rule_id,
|
|
255
|
+
shared_remediation_path=rem_path,
|
|
256
|
+
shared_verification_plan=ver_plan,
|
|
257
|
+
risk_severity=severity,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
clusters[cid].add_finding(
|
|
261
|
+
finding_id=finding_id,
|
|
262
|
+
file_path=file_path,
|
|
263
|
+
location_str=loc_str,
|
|
264
|
+
severity=severity,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Sort clusters by severity and finding count
|
|
268
|
+
severity_order = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1, "Informational": 0}
|
|
269
|
+
sorted_clusters = sorted(
|
|
270
|
+
clusters.values(),
|
|
271
|
+
key=lambda c: (severity_order.get(c.risk_severity, 0), len(c.finding_ids)),
|
|
272
|
+
reverse=True
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return sorted_clusters
|