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,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Minimal Patch Governance Engine
|
|
3
|
+
Enforces strict policy boundaries around automated code modifications:
|
|
4
|
+
- Line churn bounding (max added/removed lines)
|
|
5
|
+
- File count limits (single-file preference)
|
|
6
|
+
- High-risk file escalation (auth, crypto, tenant isolation, DB, uploads, workflows)
|
|
7
|
+
- Comment/boilerplate checks
|
|
8
|
+
- Zero unrelated file changes
|
|
9
|
+
- Automatic application blocking for oversized or high-risk diffs
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field, asdict
|
|
13
|
+
from typing import List, Dict, Any, Tuple, Optional
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Sensitive Path Categories
|
|
19
|
+
SENSITIVE_CATEGORIES = {
|
|
20
|
+
"auth": ["auth", "login", "password", "token", "jwt", "session", "oauth", "oidc", "credential"],
|
|
21
|
+
"tenancy": ["tenant", "tenant_id", "organization_id", "org_id", "workspace_id"],
|
|
22
|
+
"secrets": ["secret", "api_key", "private_key", "ssh_key", "access_key"],
|
|
23
|
+
"crypto": ["crypto", "cipher", "encrypt", "decrypt", "hashlib", "hmac"],
|
|
24
|
+
"uploads": ["upload", "storage", "filepath", "save_file", "download"],
|
|
25
|
+
"workflows": [".github/workflows", "Dockerfile", "Containerfile", "compose.yaml", "docker-compose"]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
HIGH_RISK_KEYWORDS = [kw for kws in SENSITIVE_CATEGORIES.values() for kw in kws]
|
|
29
|
+
|
|
30
|
+
HIGH_RISK_DIRECTORIES = [
|
|
31
|
+
"auth", "authentication", "authorization", "security",
|
|
32
|
+
"crypto", "migrations", ".github/workflows"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class PatchPolicyDecision:
|
|
38
|
+
allowed_auto_apply: bool
|
|
39
|
+
escalation_required: bool
|
|
40
|
+
review_level: str = "Automatic" # "Automatic" | "Peer Review Recommended" | "Mandatory Security Sign-Off"
|
|
41
|
+
rejection_reasons: List[str] = field(default_factory=list)
|
|
42
|
+
risk_factors: List[str] = field(default_factory=list)
|
|
43
|
+
line_additions: int = 0
|
|
44
|
+
line_deletions: int = 0
|
|
45
|
+
files_touched: int = 0
|
|
46
|
+
file_list: List[str] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
49
|
+
return asdict(self)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PatchGovernor:
|
|
53
|
+
"""
|
|
54
|
+
Evaluates proposed diffs against minimal patch governance rules.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
max_additions_per_file: int = 35,
|
|
60
|
+
max_deletions_per_file: int = 25,
|
|
61
|
+
max_total_files: int = 2,
|
|
62
|
+
strict_high_risk_escalation: bool = True,
|
|
63
|
+
):
|
|
64
|
+
self.max_additions_per_file = max_additions_per_file
|
|
65
|
+
self.max_deletions_per_file = max_deletions_per_file
|
|
66
|
+
self.max_total_files = max_total_files
|
|
67
|
+
self.strict_high_risk_escalation = strict_high_risk_escalation
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _parse_diff(diff_content: str, target_file: Optional[str] = None) -> Tuple[set[str], int, int, int]:
|
|
71
|
+
"""Parses unified diff lines to extract modified files, churn counts, and filler comment lines."""
|
|
72
|
+
lines = diff_content.splitlines()
|
|
73
|
+
additions = 0
|
|
74
|
+
deletions = 0
|
|
75
|
+
files: set[str] = set()
|
|
76
|
+
unnecessary_comment_lines = 0
|
|
77
|
+
|
|
78
|
+
for line in lines:
|
|
79
|
+
if line.startswith("+++ b/"):
|
|
80
|
+
files.add(line[6:].strip())
|
|
81
|
+
elif line.startswith("--- a/"):
|
|
82
|
+
old_file = line[6:].strip()
|
|
83
|
+
if old_file != "/dev/null":
|
|
84
|
+
files.add(old_file)
|
|
85
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
86
|
+
additions += 1
|
|
87
|
+
stripped = line[1:].strip()
|
|
88
|
+
if stripped.startswith("# TODO:") or stripped.startswith("// Added by AI"):
|
|
89
|
+
unnecessary_comment_lines += 1
|
|
90
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
91
|
+
deletions += 1
|
|
92
|
+
|
|
93
|
+
if target_file and not files:
|
|
94
|
+
files.add(target_file)
|
|
95
|
+
|
|
96
|
+
return files, additions, deletions, unnecessary_comment_lines
|
|
97
|
+
|
|
98
|
+
def enforce_file_bounds(self, files_count: int) -> Optional[str]:
|
|
99
|
+
"""Checks if total files modified exceed policy threshold."""
|
|
100
|
+
if files_count > self.max_total_files:
|
|
101
|
+
return f"Patch modifies {files_count} files (maximum allowed is {self.max_total_files})."
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def enforce_line_bounds(self, additions: int, deletions: int) -> List[str]:
|
|
105
|
+
"""Checks if line additions or deletions exceed policy thresholds."""
|
|
106
|
+
reasons = []
|
|
107
|
+
if additions > self.max_additions_per_file:
|
|
108
|
+
reasons.append(f"Line additions ({additions}) exceed threshold ({self.max_additions_per_file}).")
|
|
109
|
+
if deletions > self.max_deletions_per_file:
|
|
110
|
+
reasons.append(f"Line deletions ({deletions}) exceed threshold ({self.max_deletions_per_file}).")
|
|
111
|
+
return reasons
|
|
112
|
+
|
|
113
|
+
def check_filler_comments(self, unnecessary_comment_lines: int) -> Optional[str]:
|
|
114
|
+
"""Checks for excessive boilerplate or AI commentary in diff."""
|
|
115
|
+
if unnecessary_comment_lines > 2:
|
|
116
|
+
return "Patch contains excessive boilerplate or commentary."
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def escalate_sensitive_paths(self, files: set[str], diff_content: str = "") -> Tuple[bool, List[str]]:
|
|
120
|
+
"""Identifies if any touched file or diff content matches high-risk sensitive domain keywords."""
|
|
121
|
+
risk_factors = []
|
|
122
|
+
escalation_required = False
|
|
123
|
+
for f in files:
|
|
124
|
+
norm_f = f.lower().replace("\\", "/")
|
|
125
|
+
for kw in HIGH_RISK_KEYWORDS:
|
|
126
|
+
if kw in norm_f:
|
|
127
|
+
risk_factors.append(f"File `{f}` touches high-risk domain keyword `{kw}`.")
|
|
128
|
+
escalation_required = True
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
if diff_content:
|
|
132
|
+
for line in diff_content.splitlines():
|
|
133
|
+
if (line.startswith("+") and not line.startswith("+++")) or (line.startswith("-") and not line.startswith("---")):
|
|
134
|
+
line_lower = line.lower()
|
|
135
|
+
for kw in HIGH_RISK_KEYWORDS:
|
|
136
|
+
if kw in line_lower:
|
|
137
|
+
risk_factors.append(f"Diff line touches high-risk domain keyword `{kw}`.")
|
|
138
|
+
escalation_required = True
|
|
139
|
+
break
|
|
140
|
+
if escalation_required:
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
return escalation_required, risk_factors
|
|
144
|
+
|
|
145
|
+
def determine_review_level(
|
|
146
|
+
self, escalation_required: bool, additions: int, deletions: int, files_count: int
|
|
147
|
+
) -> Tuple[str, bool, List[str]]:
|
|
148
|
+
"""Determines review escalation level and whether auto-apply is blocked."""
|
|
149
|
+
reasons = []
|
|
150
|
+
review_level = "Automatic"
|
|
151
|
+
allowed = True
|
|
152
|
+
|
|
153
|
+
if escalation_required:
|
|
154
|
+
if additions > 10 or deletions > 10 or files_count > 1:
|
|
155
|
+
review_level = "Mandatory Security Sign-Off"
|
|
156
|
+
if self.strict_high_risk_escalation:
|
|
157
|
+
allowed = False
|
|
158
|
+
reasons.append("High-risk file modifications with non-trivial churn require explicit human approval.")
|
|
159
|
+
else:
|
|
160
|
+
review_level = "Peer Review Recommended"
|
|
161
|
+
|
|
162
|
+
return review_level, allowed, reasons
|
|
163
|
+
|
|
164
|
+
def evaluate_patch(self, diff_content: str, target_file: Optional[str] = None) -> PatchPolicyDecision:
|
|
165
|
+
"""
|
|
166
|
+
Parses unified diff and evaluates all governance policy compliance rules.
|
|
167
|
+
"""
|
|
168
|
+
files, additions, deletions, comment_lines = self._parse_diff(diff_content, target_file)
|
|
169
|
+
|
|
170
|
+
rejection_reasons: List[str] = []
|
|
171
|
+
|
|
172
|
+
# 1. File Count Check
|
|
173
|
+
file_err = self.enforce_file_bounds(len(files))
|
|
174
|
+
if file_err:
|
|
175
|
+
rejection_reasons.append(file_err)
|
|
176
|
+
|
|
177
|
+
# 2. Line Churn Check
|
|
178
|
+
rejection_reasons.extend(self.enforce_line_bounds(additions, deletions))
|
|
179
|
+
|
|
180
|
+
# 3. Filler Comment Check
|
|
181
|
+
comment_err = self.check_filler_comments(comment_lines)
|
|
182
|
+
if comment_err:
|
|
183
|
+
rejection_reasons.append(comment_err)
|
|
184
|
+
|
|
185
|
+
# 4. Sensitive Path Escalation
|
|
186
|
+
escalation_required, risk_factors = self.escalate_sensitive_paths(files, diff_content)
|
|
187
|
+
|
|
188
|
+
# 5. Review Level Determination
|
|
189
|
+
review_level, allowed_by_escalation, escalation_reasons = self.determine_review_level(
|
|
190
|
+
escalation_required, additions, deletions, len(files)
|
|
191
|
+
)
|
|
192
|
+
rejection_reasons.extend(escalation_reasons)
|
|
193
|
+
|
|
194
|
+
allowed = (len(rejection_reasons) == 0) and allowed_by_escalation
|
|
195
|
+
|
|
196
|
+
return PatchPolicyDecision(
|
|
197
|
+
allowed_auto_apply=allowed,
|
|
198
|
+
escalation_required=escalation_required,
|
|
199
|
+
review_level=review_level,
|
|
200
|
+
rejection_reasons=rejection_reasons,
|
|
201
|
+
risk_factors=risk_factors,
|
|
202
|
+
line_additions=additions,
|
|
203
|
+
line_deletions=deletions,
|
|
204
|
+
files_touched=len(files),
|
|
205
|
+
file_list=list(files),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def evaluate_diff(self, diff_content: str, target_file: Optional[str] = None) -> PatchPolicyDecision:
|
|
209
|
+
"""Backward-compatible alias for evaluate_patch."""
|
|
210
|
+
return self.evaluate_patch(diff_content, target_file)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard v6 Stable Finding Identity & Fingerprinting Engine
|
|
3
|
+
Generates deterministic finding identifiers that survive line-number shifts,
|
|
4
|
+
minor refactorings, and file relocations within the same logical scope.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
from dataclasses import dataclass, asdict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_code_snippet(code: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Normalizes a code snippet to be whitespace- and comment-tolerant
|
|
17
|
+
so small styling edits or empty lines don't change the region hash.
|
|
18
|
+
"""
|
|
19
|
+
if not code:
|
|
20
|
+
return ""
|
|
21
|
+
lines = []
|
|
22
|
+
for line in code.strip().splitlines():
|
|
23
|
+
stripped = line.strip()
|
|
24
|
+
# Remove single-line comments for hash stability if they are pure comment lines
|
|
25
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
26
|
+
continue
|
|
27
|
+
# Normalize internal whitespace
|
|
28
|
+
normalized = re.sub(r"\s+", " ", stripped)
|
|
29
|
+
if normalized:
|
|
30
|
+
lines.append(normalized)
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class FindingFingerprint:
|
|
36
|
+
rule_id: str
|
|
37
|
+
normalized_path: str
|
|
38
|
+
region_hash: str
|
|
39
|
+
sink_signature: Optional[str] = None
|
|
40
|
+
framework_marker: Optional[str] = None
|
|
41
|
+
fingerprint_id: str = ""
|
|
42
|
+
taint_depth: Optional[int] = None
|
|
43
|
+
sanitizer_present: bool = False
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
if not self.fingerprint_id:
|
|
47
|
+
self.fingerprint_id = self.compute_fingerprint_id()
|
|
48
|
+
|
|
49
|
+
def compute_fingerprint_id(self) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Computes a stable hash based on (rule_id, normalized_path, region_hash, sink_signature).
|
|
52
|
+
Format: TG-FND-<hash12>
|
|
53
|
+
"""
|
|
54
|
+
data = f"{self.rule_id}|{self.normalized_path}|{self.region_hash}|{self.sink_signature or ''}|{self.framework_marker or ''}"
|
|
55
|
+
h = hashlib.sha256(data.encode("utf-8")).hexdigest()[:12]
|
|
56
|
+
# Clean rule id component for readable prefix
|
|
57
|
+
rule_prefix = self.rule_id.replace("TG-", "").split("-")[0]
|
|
58
|
+
return f"TG-{rule_prefix}-{h}"
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
return asdict(self)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class IdentityEngine:
|
|
65
|
+
"""
|
|
66
|
+
Computes stable finding identities and persists fingerprint maps.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def generate_identity(
|
|
71
|
+
rule_id: str,
|
|
72
|
+
file_path: str,
|
|
73
|
+
code_snippet: str,
|
|
74
|
+
sink_signature: Optional[str] = None,
|
|
75
|
+
framework_marker: Optional[str] = None,
|
|
76
|
+
root_path: Optional[Path] = None,
|
|
77
|
+
taint_depth: Optional[int] = None,
|
|
78
|
+
sanitizer_present: bool = False,
|
|
79
|
+
) -> FindingFingerprint:
|
|
80
|
+
"""
|
|
81
|
+
Generates a stable FindingFingerprint.
|
|
82
|
+
"""
|
|
83
|
+
# Normalize file path relative to root
|
|
84
|
+
norm_path = file_path.replace("\\", "/").lstrip("./")
|
|
85
|
+
if root_path:
|
|
86
|
+
try:
|
|
87
|
+
norm_path = str(Path(file_path).resolve().relative_to(root_path.resolve())).replace("\\", "/")
|
|
88
|
+
except Exception:
|
|
89
|
+
norm_path = file_path.replace("\\", "/").lstrip("./")
|
|
90
|
+
|
|
91
|
+
# Compute normalized region hash
|
|
92
|
+
norm_code = normalize_code_snippet(code_snippet)
|
|
93
|
+
region_hash = hashlib.sha256(norm_code.encode("utf-8")).hexdigest()[:16]
|
|
94
|
+
|
|
95
|
+
return FindingFingerprint(
|
|
96
|
+
rule_id=rule_id,
|
|
97
|
+
normalized_path=norm_path,
|
|
98
|
+
region_hash=region_hash,
|
|
99
|
+
sink_signature=sink_signature,
|
|
100
|
+
framework_marker=framework_marker,
|
|
101
|
+
taint_depth=taint_depth,
|
|
102
|
+
sanitizer_present=sanitizer_present,
|
|
103
|
+
)
|
|
104
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard Import Resolver
|
|
3
|
+
Resolves language-specific import specifiers to canonical file system paths.
|
|
4
|
+
Supports Python, JavaScript, TypeScript, and Go.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
COMMON_EXTENSIONS = [
|
|
12
|
+
".py", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".go"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ImportResolver:
|
|
17
|
+
"""Resolves relative and package imports to on-disk source files."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, project_root: Path):
|
|
20
|
+
self.project_root = project_root.resolve()
|
|
21
|
+
|
|
22
|
+
def resolve(self, importing_file: Path, import_module: str) -> Optional[Path]:
|
|
23
|
+
"""
|
|
24
|
+
Resolves `import_module` referenced by `importing_file` to an absolute Path on disk.
|
|
25
|
+
"""
|
|
26
|
+
if not import_module:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
importing_file = importing_file.resolve()
|
|
30
|
+
current_dir = importing_file.parent
|
|
31
|
+
|
|
32
|
+
# 1. Relative import (e.g. ./utils, ../db, .services)
|
|
33
|
+
if import_module.startswith("."):
|
|
34
|
+
# Python style: .models or ..utils
|
|
35
|
+
if import_module.startswith(".."):
|
|
36
|
+
rel_parts = import_module.lstrip(".")
|
|
37
|
+
target_dir = current_dir.parent
|
|
38
|
+
cand = target_dir / rel_parts.replace(".", "/")
|
|
39
|
+
elif import_module.startswith("."):
|
|
40
|
+
rel_parts = import_module.lstrip(".")
|
|
41
|
+
cand = current_dir / rel_parts.replace(".", "/")
|
|
42
|
+
else:
|
|
43
|
+
cand = current_dir / import_module
|
|
44
|
+
|
|
45
|
+
resolved = self._try_extensions(cand)
|
|
46
|
+
if resolved:
|
|
47
|
+
return resolved
|
|
48
|
+
|
|
49
|
+
# 2. Direct path relative to current directory
|
|
50
|
+
direct_cand = current_dir / import_module
|
|
51
|
+
resolved = self._try_extensions(direct_cand)
|
|
52
|
+
if resolved:
|
|
53
|
+
return resolved
|
|
54
|
+
|
|
55
|
+
# 3. Project root relative import (e.g. core.taint, src.services.auth)
|
|
56
|
+
dotted_cand = self.project_root / import_module.replace(".", "/")
|
|
57
|
+
resolved = self._try_extensions(dotted_cand)
|
|
58
|
+
if resolved:
|
|
59
|
+
return resolved
|
|
60
|
+
|
|
61
|
+
# 4. Check under common subdirectories (src, app, lib, internal)
|
|
62
|
+
for sub in ("src", "app", "lib", "internal"):
|
|
63
|
+
sub_cand = self.project_root / sub / import_module.replace(".", "/")
|
|
64
|
+
resolved = self._try_extensions(sub_cand)
|
|
65
|
+
if resolved:
|
|
66
|
+
return resolved
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _try_extensions(self, base_path: Path) -> Optional[Path]:
|
|
71
|
+
# Check direct file with extension already
|
|
72
|
+
if base_path.is_file():
|
|
73
|
+
return base_path
|
|
74
|
+
|
|
75
|
+
# Check with each extension
|
|
76
|
+
for ext in COMMON_EXTENSIONS:
|
|
77
|
+
cand = base_path.with_suffix(ext)
|
|
78
|
+
if cand.is_file():
|
|
79
|
+
return cand
|
|
80
|
+
|
|
81
|
+
# Check directory index / __init__
|
|
82
|
+
if base_path.is_dir():
|
|
83
|
+
py_init = base_path / "__init__.py"
|
|
84
|
+
if py_init.is_file():
|
|
85
|
+
return py_init
|
|
86
|
+
for ext in (".ts", ".js", ".tsx"):
|
|
87
|
+
idx = base_path / f"index{ext}"
|
|
88
|
+
if idx.is_file():
|
|
89
|
+
return idx
|
|
90
|
+
|
|
91
|
+
return None
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard Incremental Static Scanner & Hash Cache
|
|
3
|
+
Identifies modified or newly added source files using cryptographic content hashes
|
|
4
|
+
to enable sub-second re-scans on large codebases.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Set, Optional, Any, Tuple
|
|
9
|
+
import json
|
|
10
|
+
import hashlib
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IncrementalScanner:
|
|
15
|
+
"""Manages file modification tracking and AST cache persistence."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, target_root: Path, cache_file: Optional[Path] = None):
|
|
18
|
+
self.target_root = target_root.resolve()
|
|
19
|
+
if cache_file:
|
|
20
|
+
self.cache_file = cache_file.resolve()
|
|
21
|
+
else:
|
|
22
|
+
cache_dir = self.target_root / ".torusguard" / "cache"
|
|
23
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
self.cache_file = cache_dir / "ast_cache.json"
|
|
25
|
+
|
|
26
|
+
self.cache_data: Dict[str, Dict[str, Any]] = self._load_cache()
|
|
27
|
+
|
|
28
|
+
def _load_cache(self) -> Dict[str, Dict[str, Any]]:
|
|
29
|
+
if self.cache_file.is_file():
|
|
30
|
+
try:
|
|
31
|
+
with open(self.cache_file, "r", encoding="utf-8") as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
except Exception:
|
|
34
|
+
return {}
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
def save_cache(self) -> None:
|
|
38
|
+
try:
|
|
39
|
+
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
with open(self.cache_file, "w", encoding="utf-8") as f:
|
|
41
|
+
json.dump(self.cache_data, f, indent=2)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def compute_file_hash(file_path: Path) -> str:
|
|
47
|
+
"""Computes SHA256 of file content."""
|
|
48
|
+
h = hashlib.sha256()
|
|
49
|
+
try:
|
|
50
|
+
with open(file_path, "rb") as f:
|
|
51
|
+
while chunk := f.read(65536):
|
|
52
|
+
h.update(chunk)
|
|
53
|
+
return h.hexdigest()
|
|
54
|
+
except Exception:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
def get_changed_files(self, all_files: List[Path]) -> Tuple[List[Path], List[Path]]:
|
|
58
|
+
"""
|
|
59
|
+
Compares current file hashes to cache.
|
|
60
|
+
Returns (changed_files, unchanged_files).
|
|
61
|
+
"""
|
|
62
|
+
changed: List[Path] = []
|
|
63
|
+
unchanged: List[Path] = []
|
|
64
|
+
|
|
65
|
+
for f in all_files:
|
|
66
|
+
try:
|
|
67
|
+
rel_path = str(f.resolve().relative_to(self.target_root)).replace("\\", "/")
|
|
68
|
+
except Exception:
|
|
69
|
+
rel_path = str(f).replace("\\", "/")
|
|
70
|
+
|
|
71
|
+
current_hash = self.compute_file_hash(f)
|
|
72
|
+
cached_entry = self.cache_data.get(rel_path)
|
|
73
|
+
|
|
74
|
+
if cached_entry and cached_entry.get("hash") == current_hash:
|
|
75
|
+
unchanged.append(f)
|
|
76
|
+
else:
|
|
77
|
+
changed.append(f)
|
|
78
|
+
|
|
79
|
+
return changed, unchanged
|
|
80
|
+
|
|
81
|
+
def get_cached_findings(self, file_path: Path) -> List[Dict[str, Any]]:
|
|
82
|
+
try:
|
|
83
|
+
rel_path = str(file_path.resolve().relative_to(self.target_root)).replace("\\", "/")
|
|
84
|
+
except Exception:
|
|
85
|
+
rel_path = str(file_path).replace("\\", "/")
|
|
86
|
+
entry = self.cache_data.get(rel_path)
|
|
87
|
+
if entry:
|
|
88
|
+
return entry.get("findings", [])
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
def update_file_cache(self, file_path: Path, findings: List[Dict[str, Any]]) -> None:
|
|
92
|
+
try:
|
|
93
|
+
rel_path = str(file_path.resolve().relative_to(self.target_root)).replace("\\", "/")
|
|
94
|
+
except Exception:
|
|
95
|
+
rel_path = str(file_path).replace("\\", "/")
|
|
96
|
+
current_hash = self.compute_file_hash(file_path)
|
|
97
|
+
self.cache_data[rel_path] = {
|
|
98
|
+
"hash": current_hash,
|
|
99
|
+
"timestamp": time.time(),
|
|
100
|
+
"findings_count": len(findings),
|
|
101
|
+
"findings": findings
|
|
102
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TorusGuard Finding Lifecycle State Machine (v0.5.1)
|
|
3
|
+
Manages progression across Detect -> Classify -> Verify -> Remediate -> Recheck -> Archive.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Tuple, Optional
|
|
7
|
+
import datetime
|
|
8
|
+
import hashlib
|
|
9
|
+
from .models import (
|
|
10
|
+
Finding,
|
|
11
|
+
LifecycleStage,
|
|
12
|
+
FindingStatus,
|
|
13
|
+
ConfidenceBand,
|
|
14
|
+
RetestRecord,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LifecycleTransitionError(Exception):
|
|
19
|
+
"""Raised when an invalid lifecycle state transition is attempted."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FindingLifecycleManager:
|
|
24
|
+
"""
|
|
25
|
+
Implements formal lifecycle progression and validation rules for TorusGuard v0.5.1 findings.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
ALLOWED_TRANSITIONS = {
|
|
29
|
+
LifecycleStage.DETECT: [LifecycleStage.CLASSIFY, LifecycleStage.ARCHIVE],
|
|
30
|
+
LifecycleStage.CLASSIFY: [LifecycleStage.VERIFY, LifecycleStage.ARCHIVE],
|
|
31
|
+
LifecycleStage.VERIFY: [LifecycleStage.REMEDIATE, LifecycleStage.CLASSIFY, LifecycleStage.ARCHIVE],
|
|
32
|
+
LifecycleStage.REMEDIATE: [LifecycleStage.RECHECK, LifecycleStage.VERIFY],
|
|
33
|
+
LifecycleStage.RECHECK: [LifecycleStage.ARCHIVE, LifecycleStage.VERIFY, LifecycleStage.REMEDIATE],
|
|
34
|
+
LifecycleStage.ARCHIVE: [], # Terminal state
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def transition(finding: Finding, target_stage: LifecycleStage, note: Optional[str] = None) -> Finding:
|
|
39
|
+
current = finding.lifecycle_stage
|
|
40
|
+
allowed = FindingLifecycleManager.ALLOWED_TRANSITIONS.get(current, [])
|
|
41
|
+
|
|
42
|
+
if target_stage not in allowed:
|
|
43
|
+
raise LifecycleTransitionError(
|
|
44
|
+
f"Invalid lifecycle transition from '{current.value}' to '{target_stage.value}'. "
|
|
45
|
+
f"Allowed target stages: {[s.value for s in allowed]}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
now = datetime.datetime.utcnow().isoformat() + "Z"
|
|
49
|
+
|
|
50
|
+
if target_stage == LifecycleStage.CLASSIFY:
|
|
51
|
+
if not finding.rule_id or not finding.category or not finding.severity:
|
|
52
|
+
raise LifecycleTransitionError("Classification requires rule_id, category, and severity.")
|
|
53
|
+
finding.status = FindingStatus.UNCONFIRMED
|
|
54
|
+
|
|
55
|
+
elif target_stage == LifecycleStage.VERIFY:
|
|
56
|
+
# Verification assertion: If no source/test evidence or evidence is insufficient, force Needs Review
|
|
57
|
+
has_sufficient_evidence = any(
|
|
58
|
+
e.is_sufficient_for_confirmed for e in finding.evidence
|
|
59
|
+
)
|
|
60
|
+
if not has_sufficient_evidence and finding.confidence.band == ConfidenceBand.CONFIRMED:
|
|
61
|
+
finding.confidence.band = ConfidenceBand.NEEDS_REVIEW
|
|
62
|
+
finding.status = FindingStatus.NEEDS_REVIEW
|
|
63
|
+
elif finding.confidence.band == ConfidenceBand.CONFIRMED:
|
|
64
|
+
finding.status = FindingStatus.CONFIRMED
|
|
65
|
+
else:
|
|
66
|
+
finding.status = FindingStatus.HIGH_CONFIDENCE if finding.confidence.score >= 70 else FindingStatus.NEEDS_REVIEW
|
|
67
|
+
|
|
68
|
+
finding.timestamps.verified_at = now
|
|
69
|
+
|
|
70
|
+
elif target_stage == LifecycleStage.REMEDIATE:
|
|
71
|
+
if not finding.remediation or not finding.remediation.recommended_fix:
|
|
72
|
+
raise LifecycleTransitionError("Remediation stage requires a complete remediation proposal.")
|
|
73
|
+
finding.status = FindingStatus.REMEDIATED
|
|
74
|
+
finding.timestamps.remediated_at = now
|
|
75
|
+
|
|
76
|
+
elif target_stage == LifecycleStage.RECHECK:
|
|
77
|
+
# Recheck requires retest record verification
|
|
78
|
+
if not finding.retest_result.retest_performed:
|
|
79
|
+
raise LifecycleTransitionError("Recheck stage requires an explicit retest to have been executed.")
|
|
80
|
+
if finding.retest_result.closure_status == FindingStatus.VERIFIED_FIXED:
|
|
81
|
+
finding.status = FindingStatus.VERIFIED_FIXED
|
|
82
|
+
finding.timestamps.retested_at = now
|
|
83
|
+
|
|
84
|
+
elif target_stage == LifecycleStage.ARCHIVE:
|
|
85
|
+
if finding.status not in (FindingStatus.VERIFIED_FIXED, FindingStatus.SUPPRESSED):
|
|
86
|
+
# Allow archive but note state
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
finding.lifecycle_stage = target_stage
|
|
90
|
+
finding.timestamps.updated_at = now
|
|
91
|
+
return finding
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def execute_retest(
|
|
95
|
+
finding: Finding,
|
|
96
|
+
post_fix_code: str,
|
|
97
|
+
safe_pattern_verified: bool,
|
|
98
|
+
retest_method: str = "Differential Static Re-audit",
|
|
99
|
+
verifier_notes: str = "Verified safe via AST/pattern inspection."
|
|
100
|
+
) -> Tuple[bool, str]:
|
|
101
|
+
"""
|
|
102
|
+
Executes a formal retest on post-fix source code, hashing the evidence and updating closure status.
|
|
103
|
+
"""
|
|
104
|
+
now = datetime.datetime.utcnow().isoformat() + "Z"
|
|
105
|
+
evidence_hash = hashlib.sha256(post_fix_code.strip().encode("utf-8")).hexdigest()
|
|
106
|
+
|
|
107
|
+
if safe_pattern_verified:
|
|
108
|
+
finding.retest_result = RetestRecord(
|
|
109
|
+
retest_performed=True,
|
|
110
|
+
closure_status=FindingStatus.VERIFIED_FIXED,
|
|
111
|
+
fix_applied=finding.remediation.recommended_fix,
|
|
112
|
+
retest_method=retest_method,
|
|
113
|
+
retest_evidence_hash=evidence_hash,
|
|
114
|
+
residual_risk=finding.remediation.residual_risk_notes,
|
|
115
|
+
verifier_notes=verifier_notes,
|
|
116
|
+
retest_timestamp=now,
|
|
117
|
+
)
|
|
118
|
+
finding.status = FindingStatus.VERIFIED_FIXED
|
|
119
|
+
finding.lifecycle_stage = LifecycleStage.RECHECK
|
|
120
|
+
finding.timestamps.retested_at = now
|
|
121
|
+
finding.timestamps.updated_at = now
|
|
122
|
+
return True, f"Finding {finding.finding_id} ({finding.rule_id}) successfully verified fixed. Evidence SHA256: {evidence_hash[:12]}..."
|
|
123
|
+
else:
|
|
124
|
+
finding.retest_result = RetestRecord(
|
|
125
|
+
retest_performed=True,
|
|
126
|
+
closure_status=FindingStatus.OPEN,
|
|
127
|
+
fix_applied=finding.remediation.recommended_fix,
|
|
128
|
+
retest_method=retest_method,
|
|
129
|
+
retest_evidence_hash=evidence_hash,
|
|
130
|
+
residual_risk="Remediation pattern missing or incomplete.",
|
|
131
|
+
verifier_notes="Unsafe pattern still detected in post-fix code.",
|
|
132
|
+
retest_timestamp=now,
|
|
133
|
+
)
|
|
134
|
+
finding.status = FindingStatus.OPEN
|
|
135
|
+
finding.lifecycle_stage = LifecycleStage.VERIFY
|
|
136
|
+
finding.timestamps.updated_at = now
|
|
137
|
+
return False, f"Retest failed for {finding.finding_id}: unsafe pattern remains."
|