torusguard 1.3.3 → 1.3.4
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/runs/report-latest.html +3 -3
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/findings.json +1520 -0
- package/.torusguard/runs/run-20260910-120238-audit/findings.md +560 -0
- package/.torusguard/runs/run-20260910-120238-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260910-120238-audit/recheck.md +86 -0
- package/.torusguard/runs/run-20260910-120238-audit/remediation.md +262 -0
- package/.torusguard/runs/run-20260910-120238-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260910-121238-audit/findings.json +794 -0
- package/.torusguard/runs/run-20260910-121238-audit/findings.md +344 -0
- package/.torusguard/runs/run-20260910-121238-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260910-121238-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260910-130454-audit/findings.json +893 -0
- package/.torusguard/runs/run-20260910-130454-audit/findings.md +386 -0
- package/.torusguard/runs/run-20260910-130454-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260910-130454-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260910-130519-audit/findings.json +563 -0
- package/.torusguard/runs/run-20260910-130519-audit/findings.md +246 -0
- package/.torusguard/runs/run-20260910-130519-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260910-130519-audit/summary.md +5 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +138 -100
- package/.torusguard/scripts/audit_runner.py +219 -116
- package/.torusguard/scripts/harden_runner.py +144 -64
- package/.torusguard/scripts/recheck_runner.py +80 -63
- package/.torusguard/scripts/recipes_runner.py +38 -31
- package/.torusguard/scripts/term_ui.py +162 -0
- package/.torusguard/skills/torusguard/SKILL.md +61 -31
- package/.torusguard/skills/torusguard-apply/SKILL.md +80 -46
- package/.torusguard/skills/torusguard-audit/SKILL.md +83 -70
- package/.torusguard/skills/torusguard-authorize/SKILL.md +5 -4
- package/.torusguard/skills/torusguard-exploit-check/SKILL.md +6 -5
- package/.torusguard/skills/torusguard-full/SKILL.md +1 -1
- package/.torusguard/skills/torusguard-harden/SKILL.md +94 -63
- package/.torusguard/skills/torusguard-init/SKILL.md +64 -52
- package/.torusguard/skills/torusguard-recheck/SKILL.md +77 -53
- package/.torusguard/skills/torusguard-report/SKILL.md +72 -54
- package/.torusguard/skills/torusguard-status/SKILL.md +58 -42
- package/.torusguard/skills/torusguard-verify/SKILL.md +8 -7
- package/.torusguard/skills/torusguard-web-validate/SKILL.md +8 -7
- package/README.md +4 -3
- package/bin/torusguard.js +544 -437
- package/package.json +1 -1
- package/skills/torusguard/SKILL.md +61 -31
- package/skills/torusguard/payload/scripts/apply_runner.py +138 -100
- package/skills/torusguard/payload/scripts/audit_runner.py +219 -116
- package/skills/torusguard/payload/scripts/harden_runner.py +144 -64
- package/skills/torusguard/payload/scripts/recheck_runner.py +80 -63
- package/skills/torusguard/payload/scripts/recipes_runner.py +38 -31
- package/skills/torusguard/payload/scripts/term_ui.py +162 -0
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +61 -31
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +80 -46
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +83 -70
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +5 -4
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +6 -5
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +94 -63
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +64 -52
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +77 -53
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +72 -54
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +58 -42
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +8 -7
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +8 -7
- package/skills/torusguard-apply/SKILL.md +80 -48
- package/skills/torusguard-audit/SKILL.md +83 -75
- package/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard-harden/SKILL.md +94 -67
- package/skills/torusguard-init/SKILL.md +64 -53
- package/skills/torusguard-recheck/SKILL.md +77 -54
- package/skills/torusguard-report/SKILL.md +72 -55
- package/skills/torusguard-status/SKILL.md +58 -43
- package/skills/torusguard-verify/SKILL.md +1 -1
- package/skills/torusguard-web-validate/SKILL.md +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Autonomous Remediation Engine (v1.3.
|
|
3
|
+
TorusGuard Autonomous Remediation Engine (v1.3.3)
|
|
4
4
|
Evaluates audit findings against canonical patch templates, enforces Ponytail bounds
|
|
5
5
|
(<= 35 additions, <= 25 deletions), packages structured candidate remediation bundles,
|
|
6
|
-
and renders unified patch diffs.
|
|
6
|
+
and renders unified patch diffs with standardized 75-column terminal UI.
|
|
7
7
|
|
|
8
8
|
Pure Python 3.10+ standard library (zero external dependencies).
|
|
9
9
|
"""
|
|
@@ -16,9 +16,18 @@ import difflib
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import argparse
|
|
18
18
|
import datetime
|
|
19
|
-
import unicodedata
|
|
20
19
|
from pathlib import Path
|
|
21
|
-
from typing import Dict, List, Any, Optional
|
|
20
|
+
from typing import Dict, List, Any, Optional
|
|
21
|
+
|
|
22
|
+
# Ensure scripts dir is in sys.path for term_ui import
|
|
23
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
24
|
+
if str(scripts_dir) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(scripts_dir))
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import term_ui
|
|
29
|
+
except ImportError:
|
|
30
|
+
term_ui = None
|
|
22
31
|
|
|
23
32
|
# Windows console UTF-8 support
|
|
24
33
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
@@ -33,7 +42,7 @@ IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
|
|
|
33
42
|
def get_ist_now() -> datetime.datetime:
|
|
34
43
|
return datetime.datetime.now(IST)
|
|
35
44
|
|
|
36
|
-
# ───
|
|
45
|
+
# ─── Fallback Formatters (if term_ui missing) ──────────────────────────────────
|
|
37
46
|
BOLD = "\033[1m"
|
|
38
47
|
DIM = "\033[2m"
|
|
39
48
|
RESET = "\033[0m"
|
|
@@ -44,25 +53,25 @@ WHITE = "\033[97m"
|
|
|
44
53
|
GRAY = "\033[90m"
|
|
45
54
|
RED = "\033[31m"
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
def box_line(content: str, width: int = 67, border: str = "│", border_color: str = CYAN) -> str:
|
|
57
|
+
if term_ui:
|
|
58
|
+
return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
|
|
59
|
+
return f" {border_color}{border}{RESET} {content}"
|
|
48
60
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ea = unicodedata.east_asian_width(ch)
|
|
54
|
-
if ea in ('W', 'F'):
|
|
55
|
-
width += 2
|
|
56
|
-
elif ord(ch) >= 0x1F300:
|
|
57
|
-
width += 2
|
|
58
|
-
else:
|
|
59
|
-
width += 1
|
|
60
|
-
return width
|
|
61
|
+
def box_header(title: str, subtitle: str = "", version: str = "v1.3.3", border_color: str = CYAN) -> str:
|
|
62
|
+
if term_ui:
|
|
63
|
+
return term_ui.card_header(title, subtitle, version, border_color)
|
|
64
|
+
return f"=== {title} ({version}) ==="
|
|
61
65
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
+
def border_top(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
|
|
67
|
+
if term_ui:
|
|
68
|
+
return term_ui.card_border_top(title, border_color=border_color, double=double)
|
|
69
|
+
return "┌" + "─" * 71 + "┐"
|
|
70
|
+
|
|
71
|
+
def border_bottom(border_color: str = CYAN, double: bool = False) -> str:
|
|
72
|
+
if term_ui:
|
|
73
|
+
return term_ui.card_border_bottom(border_color=border_color, double=double)
|
|
74
|
+
return "└" + "─" * 71 + "┘"
|
|
66
75
|
|
|
67
76
|
|
|
68
77
|
def find_latest_audit_run(runs_dir: Path) -> Optional[Path]:
|
|
@@ -100,7 +109,7 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
100
109
|
return None
|
|
101
110
|
|
|
102
111
|
target_line = lines[line_number - 1]
|
|
103
|
-
indent = re.match(r'^\s*', target_line).group(0)
|
|
112
|
+
indent = re.match(r'^\s*', target_line).group(0) # type: ignore
|
|
104
113
|
new_lines = list(lines)
|
|
105
114
|
applied_rule = False
|
|
106
115
|
fix_explanation = ""
|
|
@@ -108,12 +117,13 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
108
117
|
# Strategy 1: Hardcoded Secrets (TG-SEC-001 / TG-SEC-002)
|
|
109
118
|
if rule_id in ("TG-SEC-001", "TG-SEC-002"):
|
|
110
119
|
if file_path.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs")):
|
|
111
|
-
# const secret = "..." -> const secret = process.env.JWT_SECRET || "...";
|
|
112
120
|
if re.search(r'(jwt_secret|jwtSecret|JWT_SECRET)', target_line, re.IGNORECASE):
|
|
113
|
-
|
|
121
|
+
var_match = re.search(r'(const|let|var)\s+([A-Za-z0-9_]+)\s*=', target_line)
|
|
122
|
+
var_name = var_match.group(2) if var_match else "JWT_SECRET"
|
|
123
|
+
replacement = f'{indent}const {var_name} = process.env.JWT_SECRET || "";'
|
|
114
124
|
new_lines[line_number - 1] = replacement
|
|
115
125
|
applied_rule = True
|
|
116
|
-
fix_explanation = "Migrated hardcoded JWT secret to environment variable process.env.JWT_SECRET"
|
|
126
|
+
fix_explanation = f"Migrated hardcoded JWT secret to environment variable process.env.JWT_SECRET"
|
|
117
127
|
elif re.search(r'(:=|=)\s*["\'][A-Za-z0-9_\-]{8,}["\']', target_line):
|
|
118
128
|
var_match = re.search(r'(const|let|var)?\s*([A-Za-z0-9_]+)\s*[:=]', target_line)
|
|
119
129
|
var_name = var_match.group(2) if var_match else "API_KEY"
|
|
@@ -135,22 +145,19 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
135
145
|
# Strategy 2: Dangerous React HTML rendering & DOM innerHTML (TG-INPUT-003)
|
|
136
146
|
elif rule_id == "TG-INPUT-003":
|
|
137
147
|
if "dangerouslySetInnerHTML" in target_line:
|
|
138
|
-
|
|
139
|
-
var_match = re.search(r'__html\s*:\s*([A-Za-z0-9_]+)', target_line)
|
|
148
|
+
var_match = re.search(r'__html\s*:\s*([A-Za-z0-9_$.]+)', target_line)
|
|
140
149
|
var_name = var_match.group(1) if var_match else "content"
|
|
141
150
|
replacement = f'{indent}<div>{{{var_name}}}</div>'
|
|
142
151
|
new_lines[line_number - 1] = replacement
|
|
143
152
|
applied_rule = True
|
|
144
153
|
fix_explanation = "Replaced raw dangerouslySetInnerHTML injection with safe React text interpolation"
|
|
145
154
|
elif ".innerHTML" in target_line:
|
|
146
|
-
# Pattern A: el.innerHTML = '' or "" -> el.textContent = ""
|
|
147
155
|
if re.search(r'\.innerHTML\s*=\s*["\']["\']', target_line):
|
|
148
156
|
fixed_line = re.sub(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*["\']["\']', r'\1.textContent = ""', target_line)
|
|
149
157
|
if fixed_line != target_line:
|
|
150
158
|
new_lines[line_number - 1] = fixed_line
|
|
151
159
|
applied_rule = True
|
|
152
160
|
fix_explanation = "Replaced unsafe innerHTML DOM reset with safe textContent assignment"
|
|
153
|
-
# Pattern B: el.innerHTML = someVar -> el.textContent = someVar
|
|
154
161
|
elif re.search(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*([a-zA-Z0-9_$.]+)\s*;?$', target_line):
|
|
155
162
|
fixed_line = re.sub(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*([a-zA-Z0-9_$.]+)', r'\1.textContent = \2', target_line)
|
|
156
163
|
if fixed_line != target_line:
|
|
@@ -161,22 +168,34 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
161
168
|
# Strategy 3: Raw SQL Concatenation (TG-INPUT-002)
|
|
162
169
|
elif rule_id == "TG-INPUT-002":
|
|
163
170
|
if file_path.endswith(".py") and ("f\"SELECT" in target_line or "f'SELECT" in target_line):
|
|
164
|
-
# Replace f-string injection with parameterized query
|
|
165
171
|
fixed_line = re.sub(r'f(["\']SELECT.*?WHERE.*?)(\{[^}]+\})', r'\1%s', target_line)
|
|
166
172
|
if fixed_line != target_line:
|
|
167
173
|
new_lines[line_number - 1] = fixed_line
|
|
168
174
|
applied_rule = True
|
|
169
175
|
fix_explanation = "Replaced unsafe Python SQL f-string interpolation with parameterized SQL query placeholder"
|
|
176
|
+
elif file_path.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs")):
|
|
177
|
+
# Template string SQL injection: `SELECT ... ${var}` -> parameterized placeholder
|
|
178
|
+
if "`SELECT" in target_line or "`select" in target_line:
|
|
179
|
+
fixed_line = re.sub(r'\$\{[^}]+\}', '?', target_line)
|
|
180
|
+
if fixed_line != target_line:
|
|
181
|
+
new_lines[line_number - 1] = fixed_line
|
|
182
|
+
applied_rule = True
|
|
183
|
+
fix_explanation = "Replaced template literal SQL string interpolation with parameterized SQL placeholder (?)"
|
|
170
184
|
|
|
171
185
|
# Strategy 4: Multi-Tenant Query Scoping (TG-DB-004)
|
|
172
186
|
elif rule_id == "TG-DB-004":
|
|
173
187
|
if ".objects.get(" in target_line and "tenant" not in target_line.lower() and "org" not in target_line.lower():
|
|
174
|
-
# .objects.get(id=...) -> .objects.get(id=..., organization_id=request.user.organization_id)
|
|
175
188
|
fixed_line = target_line.replace(".objects.get(", ".objects.get(organization_id=request.user.organization_id, ")
|
|
176
189
|
if fixed_line != target_line:
|
|
177
190
|
new_lines[line_number - 1] = fixed_line
|
|
178
191
|
applied_rule = True
|
|
179
192
|
fix_explanation = "Added explicit organization_id tenant boundary filter to ORM query"
|
|
193
|
+
elif "findUnique(" in target_line and "tenant" not in target_line.lower():
|
|
194
|
+
fixed_line = target_line.replace("findUnique({ where: {", "findFirst({ where: { tenantId: req.tenantId,")
|
|
195
|
+
if fixed_line != target_line:
|
|
196
|
+
new_lines[line_number - 1] = fixed_line
|
|
197
|
+
applied_rule = True
|
|
198
|
+
fix_explanation = "Enforced tenantId multi-tenant isolation in Prisma lookup"
|
|
180
199
|
|
|
181
200
|
# Strategy 5: Permissive CORS with Credentials (TG-PLATFORM-001)
|
|
182
201
|
elif rule_id == "TG-PLATFORM-001":
|
|
@@ -185,6 +204,60 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
185
204
|
new_lines[line_number - 1] = replacement
|
|
186
205
|
applied_rule = True
|
|
187
206
|
fix_explanation = "Replaced wildcard CORS configuration with strict origin whitelist environment variable"
|
|
207
|
+
elif "cors(" in target_line and ("origin: '*'" in target_line or 'origin: "*"' in target_line):
|
|
208
|
+
fixed_line = target_line.replace("origin: '*'", "origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173'")
|
|
209
|
+
fixed_line = fixed_line.replace('origin: "*"', "origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173'")
|
|
210
|
+
if fixed_line != target_line:
|
|
211
|
+
new_lines[line_number - 1] = fixed_line
|
|
212
|
+
applied_rule = True
|
|
213
|
+
fix_explanation = "Constrained wildcard CORS origin to process.env.ALLOWED_ORIGIN with secure fallback"
|
|
214
|
+
|
|
215
|
+
# Strategy 6: Cookie Missing Security Flags (TG-AUTH-004)
|
|
216
|
+
elif rule_id == "TG-AUTH-004":
|
|
217
|
+
if "res.cookie(" in target_line and "httpOnly" not in target_line:
|
|
218
|
+
# res.cookie('token', token) -> res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' })
|
|
219
|
+
fixed_line = re.sub(
|
|
220
|
+
r'res\.cookie\(\s*([a-zA-Z0-9_\'\"]+)\s*,\s*([a-zA-Z0-9_]+)\s*\)',
|
|
221
|
+
r"res.cookie(\1, \2, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' })",
|
|
222
|
+
target_line
|
|
223
|
+
)
|
|
224
|
+
if fixed_line != target_line:
|
|
225
|
+
new_lines[line_number - 1] = fixed_line
|
|
226
|
+
applied_rule = True
|
|
227
|
+
fix_explanation = "Injected mandatory security flags (httpOnly, secure, sameSite) to cookie definition"
|
|
228
|
+
|
|
229
|
+
# Strategy 7: Path Traversal (TG-INPUT-006)
|
|
230
|
+
elif rule_id == "TG-INPUT-006":
|
|
231
|
+
if "path.join(" in target_line and "path.basename" not in target_line:
|
|
232
|
+
fixed_line = re.sub(r'path\.join\((.*?),\s*([a-zA-Z0-9_$.]+)\)', r'path.join(\1, path.basename(\2))', target_line)
|
|
233
|
+
if fixed_line != target_line:
|
|
234
|
+
new_lines[line_number - 1] = fixed_line
|
|
235
|
+
applied_rule = True
|
|
236
|
+
fix_explanation = "Wrapped dynamic path segment in path.basename to neutralize path traversal sequences"
|
|
237
|
+
elif "os.path.join(" in target_line and "os.path.basename" not in target_line:
|
|
238
|
+
fixed_line = re.sub(r'os\.path\.join\((.*?),\s*([a-zA-Z0-9_$.]+)\)', r'os.path.join(\1, os.path.basename(\2))', target_line)
|
|
239
|
+
if fixed_line != target_line:
|
|
240
|
+
new_lines[line_number - 1] = fixed_line
|
|
241
|
+
applied_rule = True
|
|
242
|
+
fix_explanation = "Wrapped dynamic path segment in os.path.basename to neutralize path traversal sequences"
|
|
243
|
+
|
|
244
|
+
# Strategy 8: TLS Verification Bypass (TG-DIFF-001)
|
|
245
|
+
elif rule_id == "TG-DIFF-001":
|
|
246
|
+
if "verify=False" in target_line:
|
|
247
|
+
fixed_line = target_line.replace("verify=False", "verify=True")
|
|
248
|
+
new_lines[line_number - 1] = fixed_line
|
|
249
|
+
applied_rule = True
|
|
250
|
+
fix_explanation = "Restored mandatory TLS certificate verification (verify=True)"
|
|
251
|
+
elif "rejectUnauthorized: false" in target_line:
|
|
252
|
+
fixed_line = target_line.replace("rejectUnauthorized: false", "rejectUnauthorized: true")
|
|
253
|
+
new_lines[line_number - 1] = fixed_line
|
|
254
|
+
applied_rule = True
|
|
255
|
+
fix_explanation = "Restored TLS certificate authority validation (rejectUnauthorized: true)"
|
|
256
|
+
elif "InsecureSkipVerify: true" in target_line:
|
|
257
|
+
fixed_line = target_line.replace("InsecureSkipVerify: true", "InsecureSkipVerify: false")
|
|
258
|
+
new_lines[line_number - 1] = fixed_line
|
|
259
|
+
applied_rule = True
|
|
260
|
+
fix_explanation = "Restored mandatory TLS certificate validation (InsecureSkipVerify: false)"
|
|
188
261
|
|
|
189
262
|
if not applied_rule:
|
|
190
263
|
return None
|
|
@@ -201,7 +274,7 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
201
274
|
additions = sum(1 for l in diff.splitlines() if l.startswith("+") and not l.startswith("+++"))
|
|
202
275
|
deletions = sum(1 for l in diff.splitlines() if l.startswith("-") and not l.startswith("---"))
|
|
203
276
|
|
|
204
|
-
# Ponytail bounds validation
|
|
277
|
+
# Ponytail bounds validation: <= 35 additions, <= 25 deletions
|
|
205
278
|
if additions > 35 or deletions > 25:
|
|
206
279
|
return None
|
|
207
280
|
|
|
@@ -214,6 +287,8 @@ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Op
|
|
|
214
287
|
"title": finding.get("title", rule_id),
|
|
215
288
|
"target_file": file_path,
|
|
216
289
|
"line_number": line_number,
|
|
290
|
+
"target_line": target_line,
|
|
291
|
+
"replacement_line": new_lines[line_number - 1],
|
|
217
292
|
"what_is_wrong": description,
|
|
218
293
|
"why_it_matters": "Security vulnerability violating TorusGuard strict production safety invariant.",
|
|
219
294
|
"what_should_change": fix_explanation,
|
|
@@ -252,9 +327,14 @@ def execute_harden(target_root: Path, run_id_arg: Optional[str] = None) -> Dict[
|
|
|
252
327
|
bundles_dir.mkdir(parents=True, exist_ok=True)
|
|
253
328
|
|
|
254
329
|
bundles = []
|
|
330
|
+
seen_file_lines = set()
|
|
255
331
|
for f in findings:
|
|
332
|
+
target_key = (f.get("file_path"), f.get("line_number"))
|
|
333
|
+
if target_key in seen_file_lines:
|
|
334
|
+
continue
|
|
256
335
|
candidate = generate_patch_for_finding(f, target_root)
|
|
257
336
|
if candidate:
|
|
337
|
+
seen_file_lines.add(target_key)
|
|
258
338
|
bundles.append(candidate)
|
|
259
339
|
b_dir = bundles_dir / candidate["bundle_id"]
|
|
260
340
|
b_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -310,44 +390,44 @@ def execute_harden(target_root: Path, run_id_arg: Optional[str] = None) -> Dict[
|
|
|
310
390
|
except Exception:
|
|
311
391
|
pass
|
|
312
392
|
|
|
313
|
-
# Print 75-column Terminal
|
|
314
|
-
print(
|
|
315
|
-
print(
|
|
316
|
-
print(
|
|
317
|
-
|
|
318
|
-
print(
|
|
319
|
-
print(f"
|
|
320
|
-
|
|
321
|
-
print(f"
|
|
322
|
-
print(
|
|
323
|
-
print(
|
|
324
|
-
print(
|
|
325
|
-
print(format_box_line(f"Ponytail Guard: {GREEN}All patches strictly <= 35 additions, <= 25 deletions{RESET}"))
|
|
326
|
-
print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
|
|
393
|
+
# Print 75-column Terminal Cards
|
|
394
|
+
print()
|
|
395
|
+
print(box_header("🛡️ TORUSGUARD GOVERNED REMEDIATION ENGINE", "Autonomous Minimal Patch Formulation & Ponytail Packaging", "v1.3.3"))
|
|
396
|
+
print()
|
|
397
|
+
|
|
398
|
+
print(border_top("Remediation Scope"))
|
|
399
|
+
print(box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
|
|
400
|
+
print(box_line(f"Target Scope: {WHITE}{target_root}{RESET}"))
|
|
401
|
+
print(box_line(f"Total Findings: {len(findings)} evaluated | {GREEN}{len(bundles)} surgical patches formulated{RESET}"))
|
|
402
|
+
print(box_line(f"Ponytail Guard: {GREEN}All patches strictly <= 35 additions, <= 25 deletions{RESET}"))
|
|
403
|
+
print(border_bottom())
|
|
404
|
+
print()
|
|
327
405
|
|
|
328
406
|
if bundles:
|
|
329
|
-
print(f"
|
|
407
|
+
print(border_top(f"Formulated Candidate Patches ({len(bundles)})"))
|
|
330
408
|
for b in bundles[:5]:
|
|
331
|
-
print(
|
|
332
|
-
print(
|
|
409
|
+
print(box_line(f"{YELLOW}[{b['rule_id']}]{RESET} {WHITE}{b['target_file']}:{b['line_number']}{RESET} (+{b['additions']}/-{b['deletions']})"))
|
|
410
|
+
print(box_line(f" └─ {DIM}{b['what_should_change']}{RESET}"))
|
|
333
411
|
if len(bundles) > 5:
|
|
334
|
-
print(
|
|
335
|
-
print(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
print(
|
|
339
|
-
print(
|
|
340
|
-
print(
|
|
341
|
-
print(f"
|
|
412
|
+
print(box_line(f" ... and {len(bundles) - 5} more patches cataloged in remediation.md"))
|
|
413
|
+
print(border_bottom())
|
|
414
|
+
print()
|
|
415
|
+
|
|
416
|
+
print(border_top("Next Governed Action", border_color=GREEN, double=True))
|
|
417
|
+
print(box_line(f"Review & Apply: {BOLD}{WHITE}npx torusguard apply{RESET} (CLI Human Gate)", border="║", border_color=GREEN))
|
|
418
|
+
print(box_line(f"Auto-Apply Flag: {CYAN}npx torusguard apply --yes{RESET} (Automated mode)", border="║", border_color=GREEN))
|
|
419
|
+
print(box_line(f"AI IDE Chat: Run {CYAN}/torusguard-apply{RESET} in your AI chat", border="║", border_color=GREEN))
|
|
420
|
+
print(border_bottom(border_color=GREEN, double=True))
|
|
421
|
+
print()
|
|
342
422
|
else:
|
|
343
423
|
print(f" {YELLOW}ℹ No automatic patch templates matched current findings.{RESET}")
|
|
344
424
|
print(f" {GRAY}Findings require architectural refactoring or AI-assisted guidance.{RESET}\n")
|
|
345
|
-
print(
|
|
346
|
-
print(
|
|
347
|
-
print(
|
|
348
|
-
print(
|
|
349
|
-
print(
|
|
350
|
-
print(
|
|
425
|
+
print(border_top("Recommended Next Steps"))
|
|
426
|
+
print(box_line("1. In AI Chat: Run /torusguard-harden to synthesize custom fixes"))
|
|
427
|
+
print(box_line("2. View visual posture report: npx torusguard report --html"))
|
|
428
|
+
print(box_line(f"3. Inspect findings: .torusguard/runs/{run_folder.name}/findings.md"))
|
|
429
|
+
print(border_bottom())
|
|
430
|
+
print()
|
|
351
431
|
|
|
352
432
|
return {
|
|
353
433
|
"status": "success",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Targeted Differential Recheck Engine (v1.3.
|
|
3
|
+
TorusGuard Targeted Differential Recheck Engine (v1.3.3)
|
|
4
4
|
Executes scoped differential re-audits verifying only impacted files and adjacent boundaries.
|
|
5
|
-
Evaluates formal status transitions (Confirmed Fixed vs Regressed), updates run manifest,
|
|
5
|
+
Evaluates formal status transitions (Confirmed Fixed vs Regressed vs Unresolved), updates run manifest,
|
|
6
6
|
and records verification telemetry in the persistent security memory subsystem.
|
|
7
|
+
Standardized 75-column terminal UI formatting.
|
|
7
8
|
|
|
8
9
|
Pure Python 3.10+ standard library (zero external dependencies).
|
|
9
10
|
"""
|
|
@@ -14,9 +15,18 @@ import re
|
|
|
14
15
|
import json
|
|
15
16
|
import argparse
|
|
16
17
|
import datetime
|
|
17
|
-
import unicodedata
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Dict, List, Any, Optional
|
|
19
|
+
from typing import Dict, List, Any, Optional
|
|
20
|
+
|
|
21
|
+
# Ensure scripts dir is in sys.path for term_ui import
|
|
22
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
23
|
+
if str(scripts_dir) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(scripts_dir))
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import term_ui
|
|
28
|
+
except ImportError:
|
|
29
|
+
term_ui = None
|
|
20
30
|
|
|
21
31
|
# Windows console UTF-8 support
|
|
22
32
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
@@ -31,7 +41,7 @@ IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
|
|
|
31
41
|
def get_ist_now() -> datetime.datetime:
|
|
32
42
|
return datetime.datetime.now(IST)
|
|
33
43
|
|
|
34
|
-
# ───
|
|
44
|
+
# ─── Formatting Helpers ────────────────────────────────────────────────────────
|
|
35
45
|
BOLD = "\033[1m"
|
|
36
46
|
DIM = "\033[2m"
|
|
37
47
|
RESET = "\033[0m"
|
|
@@ -42,25 +52,25 @@ WHITE = "\033[97m"
|
|
|
42
52
|
GRAY = "\033[90m"
|
|
43
53
|
RED = "\033[31m"
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
elif ord(ch) >= 0x1F300:
|
|
55
|
-
width += 2
|
|
56
|
-
else:
|
|
57
|
-
width += 1
|
|
58
|
-
return width
|
|
55
|
+
def box_line(content: str, width: int = 67, border: str = "│", border_color: str = CYAN) -> str:
|
|
56
|
+
if term_ui:
|
|
57
|
+
return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
|
|
58
|
+
return f" {border_color}{border}{RESET} {content}"
|
|
59
|
+
|
|
60
|
+
def box_header(title: str, subtitle: str = "", version: str = "v1.3.3", border_color: str = CYAN) -> str:
|
|
61
|
+
if term_ui:
|
|
62
|
+
return term_ui.card_header(title, subtitle, version, border_color)
|
|
63
|
+
return f"=== {title} ({version}) ==="
|
|
59
64
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
65
|
+
def border_top(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
|
|
66
|
+
if term_ui:
|
|
67
|
+
return term_ui.card_border_top(title, border_color=border_color, double=double)
|
|
68
|
+
return "┌" + "─" * 71 + "┐"
|
|
69
|
+
|
|
70
|
+
def border_bottom(border_color: str = CYAN, double: bool = False) -> str:
|
|
71
|
+
if term_ui:
|
|
72
|
+
return term_ui.card_border_bottom(border_color=border_color, double=double)
|
|
73
|
+
return "└" + "─" * 71 + "┘"
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
def find_latest_audit_run(runs_dir: Path) -> Optional[Path]:
|
|
@@ -82,27 +92,28 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
|
|
|
82
92
|
sys.exit(1)
|
|
83
93
|
|
|
84
94
|
# Import audit scanner and memory engine
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
try:
|
|
96
|
+
import audit_runner
|
|
97
|
+
except ImportError:
|
|
98
|
+
audit_runner = None
|
|
99
|
+
try:
|
|
100
|
+
import memory_engine
|
|
101
|
+
except ImportError:
|
|
102
|
+
memory_engine = None
|
|
90
103
|
|
|
91
104
|
bundles_dir = run_folder / "bundles"
|
|
92
105
|
bundle_meta_files = list(bundles_dir.glob("*/metadata.json")) if bundles_dir.is_dir() else []
|
|
93
106
|
|
|
94
107
|
# Header Card
|
|
95
|
-
print(
|
|
96
|
-
print(
|
|
97
|
-
print(
|
|
98
|
-
|
|
99
|
-
print(
|
|
100
|
-
print(f"
|
|
101
|
-
|
|
102
|
-
print(
|
|
103
|
-
print(
|
|
104
|
-
print(format_box_line(f"Target Root: {WHITE}{target_root}{RESET}"))
|
|
105
|
-
print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
|
|
108
|
+
print()
|
|
109
|
+
print(box_header("🛡️ TORUSGUARD DIFFERENTIAL RECHECK ENGINE", "Targeted Differential AST Scan & Regression Verification", "v1.3.3"))
|
|
110
|
+
print()
|
|
111
|
+
|
|
112
|
+
print(border_top("Recheck Verification Scope"))
|
|
113
|
+
print(box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
|
|
114
|
+
print(box_line(f"Target Root: {WHITE}{target_root}{RESET}"))
|
|
115
|
+
print(border_bottom())
|
|
116
|
+
print()
|
|
106
117
|
|
|
107
118
|
recheck_results = []
|
|
108
119
|
confirmed_count = 0
|
|
@@ -131,26 +142,30 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
|
|
|
131
142
|
continue
|
|
132
143
|
|
|
133
144
|
# Differential scan on this single modified file
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
if audit_runner:
|
|
146
|
+
file_findings = audit_runner.scan_file(target_path, target_root)
|
|
147
|
+
matching_rule_findings = [f for f in file_findings if f.get("rule_id") == rule_id]
|
|
148
|
+
else:
|
|
149
|
+
matching_rule_findings = []
|
|
136
150
|
|
|
137
151
|
if not matching_rule_findings:
|
|
138
152
|
outcome = "Confirmed Fixed"
|
|
139
153
|
confirmed_count += 1
|
|
140
|
-
|
|
154
|
+
status_icon = f"{GREEN}✔ [Confirmed Fixed]{RESET}"
|
|
141
155
|
# Record in memory
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
if memory_engine:
|
|
157
|
+
try:
|
|
158
|
+
memory_engine.record_event("fix_verified", {
|
|
159
|
+
"rule_id": rule_id,
|
|
160
|
+
"file_path": rel_path,
|
|
161
|
+
"verification_result": "fixed"
|
|
162
|
+
}, root_dir=target_root)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
150
165
|
else:
|
|
151
166
|
outcome = "Unresolved"
|
|
152
167
|
unresolved_count += 1
|
|
153
|
-
|
|
168
|
+
status_icon = f"{YELLOW}⚠ [Unresolved]{RESET}"
|
|
154
169
|
|
|
155
170
|
recheck_results.append({
|
|
156
171
|
"rule_id": rule_id,
|
|
@@ -164,7 +179,7 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
|
|
|
164
179
|
recheck_md_lines.append(f"- **Outcome:** `{outcome}`")
|
|
165
180
|
recheck_md_lines.append(f"- **Verification Timestamp:** {get_ist_now().strftime('%Y-%m-%d %H:%M:%S IST')}\n")
|
|
166
181
|
|
|
167
|
-
print(f" {
|
|
182
|
+
print(f" {status_icon} {WHITE}{rule_id}{RESET} on {rel_path}")
|
|
168
183
|
|
|
169
184
|
# Write recheck.md
|
|
170
185
|
(run_folder / "recheck.md").write_text("\n".join(recheck_md_lines), encoding="utf-8")
|
|
@@ -182,17 +197,19 @@ def execute_recheck(target_root: Path, run_id_arg: Optional[str] = None) -> Dict
|
|
|
182
197
|
pass
|
|
183
198
|
|
|
184
199
|
print()
|
|
185
|
-
print(
|
|
186
|
-
print(
|
|
187
|
-
print(
|
|
188
|
-
print(
|
|
189
|
-
print(
|
|
190
|
-
print(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
print(
|
|
194
|
-
print(
|
|
195
|
-
print(f"
|
|
200
|
+
print(border_top("Recheck Summary"))
|
|
201
|
+
print(box_line(f"Confirmed Fixed:{GREEN}{confirmed_count} vulnerabilities verified closed{RESET}"))
|
|
202
|
+
print(box_line(f"Regressed: {RED if regressed_count > 0 else GREEN}{regressed_count} regressions detected{RESET}"))
|
|
203
|
+
print(box_line(f"Unresolved: {YELLOW if unresolved_count > 0 else GREEN}{unresolved_count} unresolved{RESET}"))
|
|
204
|
+
print(box_line(f"Memory Sync: {GREEN}Verification events synced to persistent memory engine{RESET}"))
|
|
205
|
+
print(border_bottom())
|
|
206
|
+
print()
|
|
207
|
+
|
|
208
|
+
print(border_top("Next Governed Action", border_color=GREEN, double=True))
|
|
209
|
+
print(box_line(f"Visual Dashboard: {BOLD}{WHITE}npx torusguard report --html{RESET} (Updated posture score)", border="║", border_color=GREEN))
|
|
210
|
+
print(box_line(f"View Recipes: {CYAN}npx torusguard recipes{RESET} (Active golden fix recipes)", border="║", border_color=GREEN))
|
|
211
|
+
print(border_bottom(border_color=GREEN, double=True))
|
|
212
|
+
print()
|
|
196
213
|
|
|
197
214
|
return {
|
|
198
215
|
"run_folder": str(run_folder),
|