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,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Autonomous Static Security Audit Engine (v1.3.0)
3
+ TorusGuard Autonomous Static Security Audit Engine (v1.3.3)
4
4
  Multi-language static pattern & AST security scanner.
5
5
  Evaluates source trees against canonical TorusGuard rule families,
6
6
  augments confidence via the persistent security memory subsystem,
@@ -39,39 +39,94 @@ def get_ist_now() -> datetime.datetime:
39
39
  """Return timezone-aware datetime strictly in Indian Standard Time (IST)."""
40
40
  return datetime.datetime.now(IST)
41
41
 
42
- # ─── ANSI Colors & Formatter ───────────────────────────────────────────────────
43
- BOLD = "\033[1m"
44
- DIM = "\033[2m"
45
- RESET = "\033[0m"
46
- CYAN = "\033[36m"
47
- GREEN = "\033[32m"
48
- YELLOW = "\033[33m"
49
- WHITE = "\033[97m"
50
- GRAY = "\033[90m"
51
- RED = "\033[31m"
52
- MAGENTA = "\033[35m"
53
-
54
- ANSI_REGEX = re.compile(r'\033\[[0-9;]*m')
55
-
56
- def get_visual_width(text: str) -> int:
57
- """Calculate the printable display width of a string (ANSI and emoji aware)."""
58
- clean = ANSI_REGEX.sub('', text)
59
- width = 0
60
- for ch in clean:
61
- ea = unicodedata.east_asian_width(ch)
62
- if ea in ('W', 'F'):
63
- width += 2
64
- elif ord(ch) >= 0x1F300:
65
- width += 2
66
- else:
67
- width += 1
68
- return width
69
-
70
- def format_box_line(content: str, width: int = 71, border: str = "│", border_color: str = CYAN) -> str:
71
- """Pad content so the right border is consistently placed at visual column 75."""
72
- vis = get_visual_width(content)
73
- pad = " " * max(0, width - vis)
74
- return f" {border_color}{border}{RESET} {content}{pad}{border_color}{border}{RESET}"
42
+ # ─── UI Formatter Bridge ──────────────────────────────────────────────────────
43
+ scripts_dir = Path(__file__).resolve().parent
44
+ if str(scripts_dir) not in sys.path:
45
+ sys.path.insert(0, str(scripts_dir))
46
+
47
+ try:
48
+ import term_ui as tui
49
+ BOLD = tui.BOLD
50
+ DIM = tui.DIM
51
+ RESET = tui.RESET
52
+ CYAN = tui.CYAN
53
+ GREEN = tui.GREEN
54
+ YELLOW = tui.YELLOW
55
+ WHITE = tui.WHITE
56
+ GRAY = tui.GRAY
57
+ RED = tui.RED
58
+ MAGENTA = tui.MAGENTA
59
+ format_box_line = tui.format_box_line
60
+ card_border_top = tui.card_border_top
61
+ card_border_bottom = tui.card_border_bottom
62
+ card_divider = tui.card_divider
63
+ card_header = tui.card_header
64
+ truncate_visual = tui.truncate_visual
65
+ get_visual_width = tui.get_visual_width
66
+ except Exception:
67
+ # Standalone fallback
68
+ BOLD = "\033[1m"
69
+ DIM = "\033[2m"
70
+ RESET = "\033[0m"
71
+ CYAN = "\033[36m"
72
+ GREEN = "\033[32m"
73
+ YELLOW = "\033[33m"
74
+ WHITE = "\033[97m"
75
+ GRAY = "\033[90m"
76
+ RED = "\033[31m"
77
+ MAGENTA = "\033[35m"
78
+ ANSI_REGEX = re.compile(r'\x1b\[[0-9;]*m|\033\[[0-9;]*m')
79
+ def strip_ansi(text: str) -> str: return ANSI_REGEX.sub('', text)
80
+ def get_visual_width(text: str) -> int:
81
+ clean = strip_ansi(text)
82
+ w = 0
83
+ for ch in clean:
84
+ cp = ord(ch)
85
+ if (0xFE00 <= cp <= 0xFE0F) or cp in (0x200B, 0x200C, 0x200D, 0x00AD): continue
86
+ ea = unicodedata.east_asian_width(ch)
87
+ w += 2 if (ea in ('W', 'F') or cp >= 0x1F300) else 1
88
+ return w
89
+ def truncate_visual(text: str, max_w: int = 67) -> str:
90
+ if get_visual_width(text) <= max_w: return text
91
+ return text[:max_w-3] + "..."
92
+ def format_box_line(content: str, width: int = 67, border: str = "│", border_color: str = CYAN) -> str:
93
+ trunc = truncate_visual(content, width)
94
+ vis = get_visual_width(trunc)
95
+ pad = " " * max(0, width - vis)
96
+ return f" {border_color}{border}{RESET} {trunc}{pad} {border_color}{border}{RESET}"
97
+ def card_border_top(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
98
+ left = "╔" if double else "┌"
99
+ right = "╗" if double else "┐"
100
+ h = "═" if double else "─"
101
+ if title:
102
+ vis = get_visual_width(title)
103
+ rem = max(0, 68 - vis)
104
+ return f" {border_color}{left}{h} {BOLD}{WHITE}{title}{RESET}{border_color} {h * rem}{right}{RESET}"
105
+ return f" {border_color}{left}{h * 71}{right}{RESET}"
106
+ def card_border_bottom(border_color: str = CYAN, double: bool = False) -> str:
107
+ left = "╚" if double else "└"
108
+ right = "╝" if double else "┘"
109
+ h = "═" if double else "─"
110
+ return f" {border_color}{left}{h * 71}{right}{RESET}"
111
+ def card_divider(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
112
+ left = "╠" if double else "├"
113
+ right = "╣" if double else "┤"
114
+ h = "═" if double else "─"
115
+ if title:
116
+ vis = get_visual_width(title)
117
+ rem = max(0, 68 - vis)
118
+ return f" {border_color}{left}{h} {BOLD}{WHITE}{title}{RESET}{border_color} {h * rem}{right}{RESET}"
119
+ return f" {border_color}{left}{h * 71}{right}{RESET}"
120
+ def card_header(title: str, subtitle: str = "", version: str = "v1.3.3", border_color: str = CYAN) -> str:
121
+ top = f" {border_color}╭{'─' * 71}╮{RESET}"
122
+ bottom = f" {border_color}╰{'─' * 71}╯{RESET}"
123
+ empty = f" {border_color}│{' ' * 71}│{RESET}"
124
+ space_count = max(1, 67 - get_visual_width(title) - get_visual_width(version))
125
+ t_str = f"{BOLD}{WHITE}{title}{RESET}{' ' * space_count}{GRAY}{version}{RESET}"
126
+ lines = [top, empty, format_box_line(t_str, 67, '│', border_color)]
127
+ if subtitle: lines.append(format_box_line(f"{DIM}{subtitle}{RESET}", 67, '│', border_color))
128
+ lines.extend([empty, bottom])
129
+ return "\n".join(lines)
75
130
 
76
131
  # ─── Rule Definitions & Sinks ─────────────────────────────────────────────────
77
132
  class RulePattern(TypedDict):
@@ -96,7 +151,9 @@ RULE_PATTERNS: List[RulePattern] = [
96
151
  (re.compile(r'\bgh[pousr]_[0-9a-zA-Z]{36}\b'), "GitHub personal access token"),
97
152
  (re.compile(r'-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----'), "Hardcoded private key block"),
98
153
  (re.compile(r'\b(?:jwt_secret|jwtSecret|JWT_SECRET)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{16,}["\']'), "Hardcoded JWT signing secret"),
99
- (re.compile(r'(?:password|passwd|api_key|apiKey|secret_key)\s*[:=]\s*["\'][A-Za-z0-9@#$%^&+=_\-]{10,}["\']', re.IGNORECASE), "Hardcoded credential or API secret string")
154
+ (re.compile(r'(?:password|passwd|api_key|apiKey|secret_key)\s*[:=]\s*["\'][A-Za-z0-9@#$%^&+=_\-]{10,}["\']', re.IGNORECASE), "Hardcoded credential or API secret string"),
155
+ (re.compile(r'\bxox[baprs]-[0-9a-zA-Z]{10,48}\b'), "Slack API or Bot token"),
156
+ (re.compile(r'\bSG\.[0-9a-zA-Z_\-]{22}\.[0-9a-zA-Z_\-]{43}\b'), "SendGrid API token")
100
157
  ]
101
158
  },
102
159
  {
@@ -109,6 +166,16 @@ RULE_PATTERNS: List[RulePattern] = [
109
166
  (re.compile(r'\b(?:NEXT_PUBLIC_|VITE_|REACT_APP_)[A-Z0-9_]*(?:SECRET|PRIVATE|KEY|PASSWORD|TOKEN)\b'), "Client-exposed environment variable containing secret naming pattern")
110
167
  ]
111
168
  },
169
+ {
170
+ "rule_id": "TG-SEC-004",
171
+ "title": "Sensitive Credential or Token Logging",
172
+ "severity": "Medium",
173
+ "category": "secrets",
174
+ "cluster": "cluster-credentials-exposure",
175
+ "patterns": [
176
+ (re.compile(r'(?:console\.log|logger\.(?:info|debug|error|warn))\s*\([^)]*(?:password|token|secret|apiKey|api_key)[^)]*\)', re.IGNORECASE), "Plaintext credential or token logged to standard output")
177
+ ]
178
+ },
112
179
  # ── Database Safety & Multi-Tenant Query Isolation (TG-DB) ────────────────
113
180
  {
114
181
  "rule_id": "TG-DB-004",
@@ -118,7 +185,8 @@ RULE_PATTERNS: List[RulePattern] = [
118
185
  "cluster": "cluster-tenant-isolation",
119
186
  "patterns": [
120
187
  (re.compile(r'\.objects\.get\s*\(\s*id\s*=\s*[^,\)]+\)'), "Django ORM .get(id=...) query lacking explicit tenant scope filter"),
121
- (re.compile(r'\.findById\s*\(\s*(?:req\.params|id)[^\)]*\)'), "Direct findById query lacking organization/tenant boundaries")
188
+ (re.compile(r'\.findById\s*\(\s*(?:req\.params|id)[^\)]*\)'), "Direct findById query lacking organization/tenant boundaries"),
189
+ (re.compile(r'prisma\.[a-zA-Z]+\.findUnique\s*\(\s*\{\s*where:\s*\{\s*id\b'), "Prisma findUnique query lacking tenantId boundary filter")
122
190
  ]
123
191
  },
124
192
  {
@@ -132,6 +200,16 @@ RULE_PATTERNS: List[RulePattern] = [
132
200
  (re.compile(r'\bimport\s+.*(?:pg|mysql2|prisma|typeorm).*\s+from'), "Server database driver imported in client file")
133
201
  ]
134
202
  },
203
+ {
204
+ "rule_id": "TG-DB-002",
205
+ "title": "Privileged Database Secret or Master Role Key in Client Code",
206
+ "severity": "Critical",
207
+ "category": "database",
208
+ "cluster": "cluster-client-leak",
209
+ "patterns": [
210
+ (re.compile(r'\b(?:SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL)\b'), "Service role key or raw database connection string reference in frontend code")
211
+ ]
212
+ },
135
213
  # ── Input Validation & Code / SQL Injection (TG-INPUT) ─────────────────────
136
214
  {
137
215
  "rule_id": "TG-INPUT-002",
@@ -158,6 +236,17 @@ RULE_PATTERNS: List[RulePattern] = [
158
236
  (re.compile(r'\.innerHTML\s*='), "Direct DOM innerHTML assignment vulnerable to XSS")
159
237
  ]
160
238
  },
239
+ {
240
+ "rule_id": "TG-INPUT-005",
241
+ "title": "Unsafe Template Rendering Without Autoescaping",
242
+ "severity": "High",
243
+ "category": "injection",
244
+ "cluster": "cluster-injection",
245
+ "patterns": [
246
+ (re.compile(r'render_template_string\s*\('), "Flask render_template_string rendering untrusted input"),
247
+ (re.compile(r'autoescape\s*=\s*False\b'), "Template engine autoescape explicitly disabled")
248
+ ]
249
+ },
161
250
  {
162
251
  "rule_id": "TG-INPUT-006",
163
252
  "title": "Unvalidated User Input in File Path Resolution",
@@ -165,7 +254,8 @@ RULE_PATTERNS: List[RulePattern] = [
165
254
  "category": "filesystem",
166
255
  "cluster": "cluster-injection",
167
256
  "patterns": [
168
- (re.compile(r'(?:open|readFile|readFileSync)\s*\([^)]*\+\s*(?:req|params|query)'), "File read operation using unsanitized user request path")
257
+ (re.compile(r'(?:open|readFile|readFileSync)\s*\([^)]*\+\s*(?:req|params|query)'), "File read operation using unsanitized user request path"),
258
+ (re.compile(r'path\.join\s*\([^)]*(?:req\.params|req\.query)\.[a-zA-Z0-9_]+\)'), "Path traversal sink in path.join without basename sanitization")
169
259
  ]
170
260
  },
171
261
  # ── Authentication & Session Safety (TG-AUTH) ──────────────────────────────
@@ -207,7 +297,8 @@ RULE_PATTERNS: List[RulePattern] = [
207
297
  "cluster": "cluster-auth-bypass",
208
298
  "patterns": [
209
299
  (re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled across entire application"),
210
- (re.compile(r'@CrossOrigin\s*\(\s*["\']\*["\']\s*\)'), "Wildcard CORS origin annotation in Java/Spring")
300
+ (re.compile(r'@CrossOrigin\s*\(\s*["\']\*["\']\s*\)'), "Wildcard CORS origin annotation in Java/Spring"),
301
+ (re.compile(r'cors\(\s*\{\s*origin\s*:\s*["\']\*["\']'), "Wildcard CORS origin configured in Express middleware")
211
302
  ]
212
303
  }
213
304
  ]
@@ -226,23 +317,27 @@ SKIP_DIRS = {
226
317
  }
227
318
 
228
319
  def is_test_file(path_str: str) -> bool:
229
- """Check if file belongs to tests, fixtures, or mocks."""
320
+ """Check if file belongs to tests, fixtures, or test harnesses."""
230
321
  clean = path_str.replace("\\", "/").lower()
231
322
  markers = ["/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
232
- "__tests__", "test_", "_test.", ".test.", ".spec."]
323
+ "/harness/", "__tests__", "test_", "_test.", ".test.", ".spec."]
233
324
  return any(m in clean for m in markers)
234
325
 
235
326
 
236
- def find_files_to_scan(target_root: Path) -> List[Path]:
327
+ def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[Path]:
237
328
  """Recursively collect source files eligible for static security audit."""
238
329
  files = []
239
330
  for root, dirs, filenames in os.walk(target_root):
240
- # Mutate dirs in-place to skip ignored folders
241
331
  dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
242
332
  for f in filenames:
243
333
  ext = os.path.splitext(f)[1].lower()
244
334
  if ext in SOURCE_EXTENSIONS:
245
- files.append(Path(root) / f)
335
+ if f in ("audit_runner.py", "diff_guard.py", "finding_scorer.py"):
336
+ continue
337
+ full_path = Path(root) / f
338
+ if not include_tests and is_test_file(str(full_path)):
339
+ continue
340
+ files.append(full_path)
246
341
  return sorted(files)
247
342
 
248
343
 
@@ -306,9 +401,6 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
306
401
  Score each finding using finding_scorer.py and persistent memory patterns.
307
402
  Returns (scored_findings, clusters_map).
308
403
  """
309
- scripts_dir = Path(__file__).resolve().parent
310
- if str(scripts_dir) not in sys.path:
311
- sys.path.insert(0, str(scripts_dir))
312
404
  try:
313
405
  import finding_scorer
314
406
  except Exception:
@@ -318,65 +410,76 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
318
410
  clusters: Dict[str, List[Dict[str, Any]]] = {}
319
411
 
320
412
  for f in findings:
321
- rule_id = f["rule_id"]
322
- file_path = f["file_path"]
323
-
324
- if finding_scorer and hasattr(finding_scorer, "compute_confidence_score"):
325
- total, band, factors = finding_scorer.compute_confidence_score(
326
- evidence_quality=35,
327
- reproduction_success=0,
328
- independent_confirmations=5,
329
- environmental_clarity=15,
330
- manual_review_status=0,
331
- rule_id=rule_id,
332
- file_path=file_path,
333
- root_dir=target_root
334
- )
335
- else:
336
- total = 70
337
- band = "High Confidence"
338
- factors = {"total_score": total, "classification_band": band}
339
-
340
- f["confidence_score"] = total
413
+ score = 70
414
+ band = "High Confidence"
415
+ factors = {}
416
+
417
+ if finding_scorer:
418
+ try:
419
+ eq = 35
420
+ rs = 0
421
+ ic = 5
422
+ ec = 15
423
+ mr = 0
424
+ s, b, facts = finding_scorer.compute_confidence_score(
425
+ evidence_quality=eq,
426
+ reproduction_success=rs,
427
+ independent_confirmations=ic,
428
+ environmental_clarity=ec,
429
+ manual_review_status=mr,
430
+ rule_id=f["rule_id"],
431
+ file_path=f["file_path"],
432
+ root_dir=target_root
433
+ )
434
+ score = s
435
+ band = b
436
+ factors = facts
437
+ except Exception:
438
+ pass
439
+
440
+ f["confidence_score"] = score
341
441
  f["confidence_band"] = band
342
442
  f["confidence_factors"] = factors
343
-
344
443
  scored.append(f)
345
- cluster_id = f.get("cluster", "cluster-general")
346
- clusters.setdefault(cluster_id, []).append(f)
347
444
 
445
+ c_id = f["cluster"]
446
+ if c_id not in clusters:
447
+ clusters[c_id] = []
448
+ clusters[c_id].append(f)
449
+
450
+ # Sort descending by severity (Critical -> High -> Medium -> Low), then score
451
+ sev_rank = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1}
452
+ scored.sort(key=lambda x: (sev_rank.get(x["severity"], 0), x["confidence_score"]), reverse=True)
348
453
  return scored, clusters
349
454
 
350
455
 
351
456
  def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], target_root: Path) -> None:
352
- """Write findings.json, findings.md, and summary.md into isolated run directory."""
353
- run_folder.mkdir(parents=True, exist_ok=True)
457
+ """Generate findings.json, findings.md, and summary.md into run folder."""
458
+ now_ist = get_ist_now()
354
459
 
355
460
  # 1. findings.json
356
- findings_file = run_folder / "findings.json"
357
- with open(findings_file, "w", encoding="utf-8") as f:
358
- json.dump(scored_findings, f, indent=2)
461
+ (run_folder / "findings.json").write_text(json.dumps(scored_findings, indent=2), encoding="utf-8")
359
462
 
360
463
  # 2. findings.md
361
- now_ist = get_ist_now()
362
464
  md_lines = [
363
- f"# TorusGuard Static Security Audit Report",
364
- f"**Run ID:** `{run_folder.name}` ",
365
- f"**Timestamp:** `{now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}` ",
366
- f"**Target:** `{target_root}` ",
367
- f"**Total Findings:** `{len(scored_findings)}` \n",
465
+ f"# TorusGuard Security Audit Findings: {run_folder.name}",
466
+ f"- Target Root: `{target_root}`",
467
+ f"- Total Scored Findings: `{len(scored_findings)}`",
468
+ f"- Identified Clusters: `{len(clusters)}`",
469
+ f"- Generated At: `{now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}`\n",
368
470
  "---",
369
- "## Findings Catalog\n"
471
+ "## Summary of Findings\n"
370
472
  ]
371
473
 
372
- for item in scored_findings:
373
- md_lines.append(f"### [{item['rule_id']}] {item['title']}")
374
- md_lines.append(f"- **Severity:** `{item['severity']}` | **Confidence:** `{item['confidence_score']}/100 ({item['confidence_band']})`")
375
- md_lines.append(f"- **File:** `{item['file_path']}:{item['line_number']}` | **Cluster:** `{item['cluster']}`")
376
- md_lines.append(f"- **Description:** {item['description']}")
474
+ for f in scored_findings:
475
+ md_lines.append(f"### [{f['severity']}] {f['rule_id']} - {f['title']}")
476
+ md_lines.append(f"- **Finding ID:** `{f['finding_id']}`")
477
+ md_lines.append(f"- **Location:** `{f['file_path']}:{f['line_number']}`")
478
+ md_lines.append(f"- **Confidence Score:** `{f['confidence_score']}/100` ({f['confidence_band']})")
479
+ md_lines.append(f"- **Cluster:** `{f['cluster']}`")
480
+ md_lines.append(f"- **Description:** {f['description']}")
377
481
  md_lines.append("```")
378
- code_snippet = str(item.get("snippet") or item.get("matched_text") or "")
379
- md_lines.append(code_snippet)
482
+ md_lines.append(f['snippet'])
380
483
  md_lines.append("```\n")
381
484
 
382
485
  (run_folder / "findings.md").write_text("\n".join(md_lines), encoding="utf-8")
@@ -415,28 +518,24 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
415
518
  if detected_stack and detected_stack.get("framework") and detected_stack.get("framework") != "None":
416
519
  stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
417
520
 
418
- print(f"""
419
- {CYAN}╭─────────────────────────────────────────────────────────────────────────╮{RESET}
420
- {CYAN}│{RESET} {CYAN}│{RESET}
421
- {CYAN}│{RESET} {BOLD}{WHITE}🛡️ TORUSGUARD STATIC SECURITY AUDIT{RESET} {GRAY}v1.3.0{RESET} {CYAN}│{RESET}
422
- {CYAN}│{RESET} {DIM}Autonomous AST & Invariant Security Scanner{RESET} {CYAN}│{RESET}
423
- {CYAN}│{RESET} {CYAN}│{RESET}
424
- {CYAN}╰─────────────────────────────────────────────────────────────────────────╯{RESET}
425
- """)
426
-
427
- print(f" {CYAN}┌─ {BOLD}Audit Execution Scope{RESET} {CYAN}─────────────────────────────────────────────────┐{RESET}")
428
- print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root):<56}{RESET}"))
429
- print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str:<56}{RESET}"))
521
+ print()
522
+ print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.3.3"))
523
+ print()
524
+
525
+ print(card_border_top("Audit Execution Scope"))
526
+ print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root)}{RESET}"))
527
+ print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str}{RESET}"))
430
528
  print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 11 canonical families{RESET}"))
431
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
529
+ print(card_border_bottom())
530
+ print()
432
531
 
433
532
  # Findings Box
434
533
  status_icon = f"{RED}✖ CRITICAL FINDINGS DETECTED{RESET}" if crit_count > 0 else (f"{YELLOW}⚠ HIGH VULNERABILITIES DETECTED{RESET}" if high_count > 0 else f"{GREEN}✔ SECURITY POSTURE CLEAN{RESET}")
435
- print(f" {CYAN}┌─ {BOLD}Findings & Confidence Scores{RESET} {CYAN}─────────────────────────────────────────┐{RESET}")
534
+ print(card_border_top("Findings & Confidence Scores"))
436
535
  print(format_box_line(f"Status: {status_icon}"))
437
536
  print(format_box_line(f"Findings: {RED}{crit_count} Critical{RESET} {YELLOW}{high_count} High{RESET} {CYAN}{med_count} Medium/Low{RESET} ({len(scored_findings)} total)"))
438
537
  print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
439
- print(f" {CYAN}├─────────────────────────────────────────────────────────────────────────┤{RESET}")
538
+ print(card_divider())
440
539
 
441
540
  if not scored_findings:
442
541
  print(format_box_line(f"{GREEN}✔ Zero security invariant violations detected.{RESET}"))
@@ -445,30 +544,33 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
445
544
  sev_color = RED if f["severity"] == "Critical" else (YELLOW if f["severity"] == "High" else CYAN)
446
545
  line_str = f"[{sev_color}{f['severity']}{RESET}] {BOLD}{f['rule_id']}{RESET} at {f['file_path']}:{f['line_number']}"
447
546
  print(format_box_line(line_str))
448
- desc_str = f" └─ {DIM}{f['description'][:60]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
547
+ desc_str = f" └─ {DIM}{f['description'][:58]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
449
548
  print(format_box_line(desc_str))
450
549
  if len(scored_findings) > 5:
451
550
  print(format_box_line(f"{DIM}... and {len(scored_findings) - 5} more findings in report file{RESET}"))
452
551
 
453
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
552
+ print(card_border_bottom())
553
+ print()
454
554
 
455
555
  # Run Artifacts & Next Steps Box
456
- rel_run = str(run_folder.relative_to(target_root)) if str(run_folder).startswith(str(target_root)) else str(run_folder)
457
- print(f" {GREEN}╔═ {BOLD}Artifacts & Next Actions{RESET} {GREEN}══════════════════════════════════════════════════╗{RESET}")
458
- print(format_box_line(f"{BOLD}Run Artifact:{RESET} {WHITE}{rel_run}/findings.md{RESET}", border="║", border_color=GREEN))
556
+ rel_run = str(run_folder.relative_to(target_root)).replace("\\", "/") if str(run_folder).startswith(str(target_root)) else str(run_folder).replace("\\", "/")
557
+ findings_artifact = f"{rel_run}/findings.md"
558
+ print(card_border_top("Artifacts & Next Actions", border_color=GREEN, double=True))
559
+ print(format_box_line(f"{BOLD}Run Artifact:{RESET} {WHITE}{findings_artifact}{RESET}", border="║", border_color=GREEN))
459
560
  print(format_box_line(f"{BOLD}Visual Dash:{RESET} {CYAN}npx torusguard report --html{RESET}", border="║", border_color=GREEN))
460
561
  print(format_box_line(f"{BOLD}Surgical Fix:{RESET} In AI Chat, run {CYAN}/torusguard-harden{RESET}", border="║", border_color=GREEN))
461
- print(f" {GREEN}╚═════════════════════════════════════════════════════════════════════════╝{RESET}\n")
562
+ print(card_border_bottom(border_color=GREEN, double=True))
563
+ print()
462
564
 
463
565
 
464
- def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False) -> Dict[str, Any]:
566
+ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
465
567
  """Execute the full TorusGuard static security audit."""
466
568
  target_root = target_root.resolve()
467
- scripts_dir = Path(__file__).resolve().parent
569
+ s_dir = Path(__file__).resolve().parent
468
570
 
469
571
  # 1. Stack detection
470
572
  detected_stack = None
471
- stack_script = scripts_dir / "stack_detect.py"
573
+ stack_script = s_dir / "stack_detect.py"
472
574
  if stack_script.is_file():
473
575
  try:
474
576
  import importlib.util
@@ -482,7 +584,7 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
482
584
  pass
483
585
 
484
586
  # 2. Collect files & scan
485
- files = find_files_to_scan(target_root)
587
+ files = find_files_to_scan(target_root, include_tests=include_tests)
486
588
  all_findings = []
487
589
  for f in files:
488
590
  all_findings.extend(scan_file(f, target_root))
@@ -494,8 +596,8 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
494
596
  runs_dir = target_root / ".torusguard" / "runs"
495
597
  runs_dir.mkdir(parents=True, exist_ok=True)
496
598
 
497
- if str(scripts_dir) not in sys.path:
498
- sys.path.insert(0, str(scripts_dir))
599
+ if str(s_dir) not in sys.path:
600
+ sys.path.insert(0, str(s_dir))
499
601
  try:
500
602
  import run_manager
501
603
  run_folder = run_manager.create_run(runs_dir, "audit", target_root.name)
@@ -539,10 +641,11 @@ def main():
539
641
  parser.add_argument("--scope", "-s", help="Alternative path to target project")
540
642
  parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
541
643
  parser.add_argument("--json", action="store_true", help="Output raw JSON")
644
+ parser.add_argument("--include-tests", action="store_true", help="Include test files and harness fixtures in scan")
542
645
  args = parser.parse_args()
543
646
 
544
647
  target = Path(args.scope or args.path).resolve()
545
- res = execute_audit(target, severity_floor=args.severity, json_output=args.json)
648
+ res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
546
649
  sys.exit(0 if res["critical_count"] == 0 else 1)
547
650
 
548
651