torusguard 1.3.2 → 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/html_reporter.py +8 -3
- package/.torusguard/scripts/recheck_runner.py +80 -63
- package/.torusguard/scripts/recipes_runner.py +38 -31
- package/.torusguard/scripts/stack_detect.py +608 -584
- 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/html_reporter.py +8 -3
- 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/stack_detect.py +608 -584
- 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 -46
- package/skills/torusguard-audit/SKILL.md +83 -70
- package/skills/torusguard-authorize/SKILL.md +5 -4
- package/skills/torusguard-exploit-check/SKILL.md +6 -5
- package/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard-harden/SKILL.md +94 -63
- package/skills/torusguard-init/SKILL.md +64 -52
- package/skills/torusguard-recheck/SKILL.md +77 -53
- package/skills/torusguard-report/SKILL.md +72 -54
- package/skills/torusguard-status/SKILL.md +58 -42
- package/skills/torusguard-verify/SKILL.md +8 -7
- package/skills/torusguard-web-validate/SKILL.md +8 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
TorusGuard Autonomous Static Security Audit Engine (v1.3.
|
|
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
|
-
# ───
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
"""
|
|
353
|
-
|
|
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
|
-
|
|
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
|
|
364
|
-
f"
|
|
365
|
-
f"
|
|
366
|
-
f"
|
|
367
|
-
f"
|
|
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
|
|
471
|
+
"## Summary of Findings\n"
|
|
370
472
|
]
|
|
371
473
|
|
|
372
|
-
for
|
|
373
|
-
md_lines.append(f"### [{
|
|
374
|
-
md_lines.append(f"- **
|
|
375
|
-
md_lines.append(f"- **
|
|
376
|
-
md_lines.append(f"- **
|
|
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
|
-
|
|
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(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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'][:
|
|
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(
|
|
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
|
-
|
|
458
|
-
print(
|
|
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(
|
|
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
|
-
|
|
569
|
+
s_dir = Path(__file__).resolve().parent
|
|
468
570
|
|
|
469
571
|
# 1. Stack detection
|
|
470
572
|
detected_stack = None
|
|
471
|
-
stack_script =
|
|
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(
|
|
498
|
-
sys.path.insert(0, str(
|
|
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
|
|