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.
Files changed (127) hide show
  1. package/.torusguard/runs/report-latest.html +3 -3
  2. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/metadata.json +15 -0
  3. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/minimal_patch_plan.md +21 -0
  4. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/patch.diff +9 -0
  5. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/metadata.json +15 -0
  6. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/minimal_patch_plan.md +21 -0
  7. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/patch.diff +9 -0
  8. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/metadata.json +15 -0
  9. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/minimal_patch_plan.md +21 -0
  10. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/patch.diff +9 -0
  11. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/metadata.json +15 -0
  12. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/minimal_patch_plan.md +21 -0
  13. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/patch.diff +9 -0
  14. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/metadata.json +15 -0
  15. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/minimal_patch_plan.md +21 -0
  16. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/patch.diff +9 -0
  17. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/metadata.json +15 -0
  18. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/minimal_patch_plan.md +21 -0
  19. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/patch.diff +9 -0
  20. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/metadata.json +15 -0
  21. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/minimal_patch_plan.md +21 -0
  22. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/patch.diff +9 -0
  23. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/metadata.json +15 -0
  24. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/minimal_patch_plan.md +21 -0
  25. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/patch.diff +9 -0
  26. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +15 -0
  27. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  28. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  29. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/metadata.json +15 -0
  30. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/minimal_patch_plan.md +21 -0
  31. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/patch.diff +9 -0
  32. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/metadata.json +15 -0
  33. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/minimal_patch_plan.md +21 -0
  34. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/patch.diff +9 -0
  35. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/metadata.json +15 -0
  36. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/minimal_patch_plan.md +21 -0
  37. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/patch.diff +9 -0
  38. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/metadata.json +15 -0
  39. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/minimal_patch_plan.md +21 -0
  40. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/patch.diff +9 -0
  41. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/metadata.json +15 -0
  42. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/minimal_patch_plan.md +21 -0
  43. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/patch.diff +9 -0
  44. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/metadata.json +15 -0
  45. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/minimal_patch_plan.md +21 -0
  46. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/patch.diff +9 -0
  47. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/metadata.json +15 -0
  48. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/minimal_patch_plan.md +21 -0
  49. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/patch.diff +9 -0
  50. package/.torusguard/runs/run-20260910-120238-audit/findings.json +1520 -0
  51. package/.torusguard/runs/run-20260910-120238-audit/findings.md +560 -0
  52. package/.torusguard/runs/run-20260910-120238-audit/manifest.json +13 -0
  53. package/.torusguard/runs/run-20260910-120238-audit/recheck.md +86 -0
  54. package/.torusguard/runs/run-20260910-120238-audit/remediation.md +262 -0
  55. package/.torusguard/runs/run-20260910-120238-audit/summary.md +5 -0
  56. package/.torusguard/runs/run-20260910-121238-audit/findings.json +794 -0
  57. package/.torusguard/runs/run-20260910-121238-audit/findings.md +344 -0
  58. package/.torusguard/runs/run-20260910-121238-audit/manifest.json +12 -0
  59. package/.torusguard/runs/run-20260910-121238-audit/summary.md +5 -0
  60. package/.torusguard/runs/run-20260910-130454-audit/findings.json +893 -0
  61. package/.torusguard/runs/run-20260910-130454-audit/findings.md +386 -0
  62. package/.torusguard/runs/run-20260910-130454-audit/manifest.json +12 -0
  63. package/.torusguard/runs/run-20260910-130454-audit/summary.md +5 -0
  64. package/.torusguard/runs/run-20260910-130519-audit/findings.json +563 -0
  65. package/.torusguard/runs/run-20260910-130519-audit/findings.md +246 -0
  66. package/.torusguard/runs/run-20260910-130519-audit/manifest.json +12 -0
  67. package/.torusguard/runs/run-20260910-130519-audit/summary.md +5 -0
  68. package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  69. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  70. package/.torusguard/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  71. package/.torusguard/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  72. package/.torusguard/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  73. package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  74. package/.torusguard/scripts/apply_runner.py +138 -100
  75. package/.torusguard/scripts/audit_runner.py +219 -116
  76. package/.torusguard/scripts/harden_runner.py +144 -64
  77. package/.torusguard/scripts/recheck_runner.py +80 -63
  78. package/.torusguard/scripts/recipes_runner.py +38 -31
  79. package/.torusguard/scripts/term_ui.py +162 -0
  80. package/.torusguard/skills/torusguard/SKILL.md +61 -31
  81. package/.torusguard/skills/torusguard-apply/SKILL.md +80 -46
  82. package/.torusguard/skills/torusguard-audit/SKILL.md +83 -70
  83. package/.torusguard/skills/torusguard-authorize/SKILL.md +5 -4
  84. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +6 -5
  85. package/.torusguard/skills/torusguard-full/SKILL.md +1 -1
  86. package/.torusguard/skills/torusguard-harden/SKILL.md +94 -63
  87. package/.torusguard/skills/torusguard-init/SKILL.md +64 -52
  88. package/.torusguard/skills/torusguard-recheck/SKILL.md +77 -53
  89. package/.torusguard/skills/torusguard-report/SKILL.md +72 -54
  90. package/.torusguard/skills/torusguard-status/SKILL.md +58 -42
  91. package/.torusguard/skills/torusguard-verify/SKILL.md +8 -7
  92. package/.torusguard/skills/torusguard-web-validate/SKILL.md +8 -7
  93. package/README.md +4 -3
  94. package/bin/torusguard.js +544 -437
  95. package/package.json +1 -1
  96. package/skills/torusguard/SKILL.md +61 -31
  97. package/skills/torusguard/payload/scripts/apply_runner.py +138 -100
  98. package/skills/torusguard/payload/scripts/audit_runner.py +219 -116
  99. package/skills/torusguard/payload/scripts/harden_runner.py +144 -64
  100. package/skills/torusguard/payload/scripts/recheck_runner.py +80 -63
  101. package/skills/torusguard/payload/scripts/recipes_runner.py +38 -31
  102. package/skills/torusguard/payload/scripts/term_ui.py +162 -0
  103. package/skills/torusguard/payload/skills/torusguard/SKILL.md +61 -31
  104. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +80 -46
  105. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +83 -70
  106. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +5 -4
  107. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +6 -5
  108. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
  109. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +94 -63
  110. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +64 -52
  111. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +77 -53
  112. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +72 -54
  113. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +58 -42
  114. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +8 -7
  115. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +8 -7
  116. package/skills/torusguard-apply/SKILL.md +80 -48
  117. package/skills/torusguard-audit/SKILL.md +83 -75
  118. package/skills/torusguard-authorize/SKILL.md +1 -1
  119. package/skills/torusguard-exploit-check/SKILL.md +1 -1
  120. package/skills/torusguard-full/SKILL.md +1 -1
  121. package/skills/torusguard-harden/SKILL.md +94 -67
  122. package/skills/torusguard-init/SKILL.md +64 -53
  123. package/skills/torusguard-recheck/SKILL.md +77 -54
  124. package/skills/torusguard-report/SKILL.md +72 -55
  125. package/skills/torusguard-status/SKILL.md +58 -43
  126. package/skills/torusguard-verify/SKILL.md +1 -1
  127. 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.0)
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, Tuple
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
- # ─── ANSI Colors & Formatter ───────────────────────────────────────────────────
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
- ANSI_REGEX = re.compile(r'\033\[[0-9;]*m')
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 get_visual_width(text: str) -> int:
50
- clean = ANSI_REGEX.sub('', text)
51
- width = 0
52
- for ch in clean:
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 format_box_line(content: str, width: int = 71, border: str = "│", border_color: str = CYAN) -> str:
63
- vis = get_visual_width(content)
64
- pad = " " * max(0, width - vis)
65
- return f" {border_color}{border}{RESET} {content}{pad}{border_color}{border}{RESET}"
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) # type: ignore
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
- replacement = f'{indent}const secret = process.env.JWT_SECRET || "";'
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
- # <div dangerouslySetInnerHTML={{ __html: bio }} /> -> <div>{bio}</div>
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 Card
314
- print(f"\n {CYAN}╭─────────────────────────────────────────────────────────────────────────╮{RESET}")
315
- print(f" {CYAN}│ │{RESET}")
316
- print(format_box_line(f"{BOLD}🛡️ TORUSGUARD GOVERNED REMEDIATION ENGINE v1.3.1{RESET}", border_color=CYAN))
317
- print(format_box_line(f"{DIM}Autonomous Minimal Patch Formulation & Ponytail Packaging{RESET}", border_color=CYAN))
318
- print(f" {CYAN}│ │{RESET}")
319
- print(f" {CYAN}╰─────────────────────────────────────────────────────────────────────────╯{RESET}\n")
320
-
321
- print(f" {CYAN}┌─ Remediation Scope ─────────────────────────────────────────────────────┐{RESET}")
322
- print(format_box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
323
- print(format_box_line(f"Target Scope: {WHITE}{target_root}{RESET}"))
324
- print(format_box_line(f"Total Findings: {len(findings)} evaluated | {GREEN}{len(bundles)} surgical patches formulated{RESET}"))
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" {CYAN}┌─ Formulated Candidate Patches ({len(bundles)}) ───────────────────────────────────┐{RESET}")
407
+ print(border_top(f"Formulated Candidate Patches ({len(bundles)})"))
330
408
  for b in bundles[:5]:
331
- print(format_box_line(f"{YELLOW}[{b['rule_id']}]{RESET} {WHITE}{b['target_file']}:{b['line_number']}{RESET} (+{b['additions']}/-{b['deletions']})"))
332
- print(format_box_line(f" └─ {DIM}{b['what_should_change']}{RESET}"))
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(format_box_line(f" ... and {len(bundles) - 5} more patches cataloged in remediation.md"))
335
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
336
-
337
- print(f" {GREEN}╔═ Next Governed Action ═════════════════════════════════════════════════════╗{RESET}")
338
- print(format_box_line(f"Review & Apply: {BOLD}{WHITE}npx torusguard apply{RESET} (CLI Human Gate)", border="║", border_color=GREEN))
339
- print(format_box_line(f"Auto-Apply Flag: {CYAN}npx torusguard apply --yes{RESET} (Automated mode)", border="║", border_color=GREEN))
340
- print(format_box_line(f"AI IDE Chat: Run {CYAN}/torusguard-apply{RESET} in your AI chat", border="║", border_color=GREEN))
341
- print(f" {GREEN}╚═════════════════════════════════════════════════════════════════════════════╝{RESET}\n")
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(f" {CYAN}┌─ Recommended Next Steps ────────────────────────────────────────────────┐{RESET}")
346
- print(format_box_line("1. In AI Chat (Antigravity/Cursor/Claude): Run /torusguard-harden"))
347
- print(format_box_line(" to synthesize custom architectural fixes with your AI agent."))
348
- print(format_box_line("2. View visual posture report: npx torusguard report --html"))
349
- print(format_box_line(f"3. Inspect finding details: .torusguard/runs/{run_folder.name}/findings.md"))
350
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
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.0)
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, Tuple
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
- # ─── ANSI Colors & Formatter ───────────────────────────────────────────────────
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
- ANSI_REGEX = re.compile(r'\033\[[0-9;]*m')
46
-
47
- def get_visual_width(text: str) -> int:
48
- clean = ANSI_REGEX.sub('', text)
49
- width = 0
50
- for ch in clean:
51
- ea = unicodedata.east_asian_width(ch)
52
- if ea in ('W', 'F'):
53
- width += 2
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 format_box_line(content: str, width: int = 71, border: str = "│", border_color: str = CYAN) -> str:
61
- vis = get_visual_width(content)
62
- pad = " " * max(0, width - vis)
63
- return f" {border_color}{border}{RESET} {content}{pad}{border_color}{border}{RESET}"
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
- scripts_dir = Path(__file__).resolve().parent
86
- if str(scripts_dir) not in sys.path:
87
- sys.path.insert(0, str(scripts_dir))
88
- import audit_runner
89
- import memory_engine
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(f"\n {CYAN}╭─────────────────────────────────────────────────────────────────────────╮{RESET}")
96
- print(f" {CYAN}│ │{RESET}")
97
- print(format_box_line(f"{BOLD}🛡️ TORUSGUARD DIFFERENTIAL RECHECK ENGINE v1.3.0{RESET}", border_color=CYAN))
98
- print(format_box_line(f"{DIM}Targeted Differential AST Scan & Regression Verification{RESET}", border_color=CYAN))
99
- print(f" {CYAN}│ │{RESET}")
100
- print(f" {CYAN}╰─────────────────────────────────────────────────────────────────────────╯{RESET}\n")
101
-
102
- print(f" {CYAN}┌─ Recheck Verification Scope ────────────────────────────────────────────┐{RESET}")
103
- print(format_box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
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
- file_findings = audit_runner.scan_file(target_path, target_root)
135
- matching_rule_findings = [f for f in file_findings if f.get("rule_id") == rule_id]
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
- status_color = GREEN
154
+ status_icon = f"{GREEN}✔ [Confirmed Fixed]{RESET}"
141
155
  # Record in memory
142
- try:
143
- memory_engine.record_event("fix_verified", {
144
- "rule_id": rule_id,
145
- "file_path": rel_path,
146
- "verification_result": "fixed"
147
- }, root_dir=target_root)
148
- except Exception:
149
- pass
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
- status_color = YELLOW
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" {status_color}✔ [{outcome}]{RESET} {WHITE}{rule_id}{RESET} on {rel_path}")
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(f" {CYAN}┌─ Recheck Summary ───────────────────────────────────────────────────────┐{RESET}")
186
- print(format_box_line(f"Confirmed Fixed:{GREEN}{confirmed_count} vulnerabilities verified closed{RESET}"))
187
- print(format_box_line(f"Regressed: {RED if regressed_count > 0 else GREEN}{regressed_count} regressions detected{RESET}"))
188
- print(format_box_line(f"Unresolved: {YELLOW if unresolved_count > 0 else GREEN}{unresolved_count} unresolved{RESET}"))
189
- print(format_box_line(f"Memory Sync: {GREEN}Verification events synced to persistent memory engine{RESET}"))
190
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
191
-
192
- print(f" {GREEN}╔═ Next Governed Action ═════════════════════════════════════════════════════╗{RESET}")
193
- print(format_box_line(f"Visual Dashboard: {BOLD}{WHITE}npx torusguard report --html{RESET} (Updated posture score)", border="║", border_color=GREEN))
194
- print(format_box_line(f"View Recipes: {CYAN}npx torusguard recipes{RESET} (Active golden fix recipes)", border="║", border_color=GREEN))
195
- print(f" {GREEN}╚═════════════════════════════════════════════════════════════════════════════╝{RESET}\n")
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),