torusguard 1.3.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.torusguard/.manifest.json +37 -36
- package/.torusguard/runs/report-latest.html +5862 -315
- package/.torusguard/runs/results-latest.sarif +20 -0
- package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
- package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
- package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
- package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
- package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260918-105940-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260918-105940-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260918-105940-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-145701-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-145701-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-145701-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-151729-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-151729-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260919-151729-audit/remediation.md +7 -0
- package/.torusguard/runs/run-20260919-151729-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152340-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152340-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152340-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-152345-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-152345-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-152345-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260919-153500-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260919-153500-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260919-153500-audit/summary.md +5 -0
- package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
- package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +108 -9
- package/.torusguard/scripts/audit_runner.py +336 -24
- package/.torusguard/scripts/harden_runner.py +124 -70
- package/.torusguard/scripts/html_reporter.py +3585 -648
- package/.torusguard/scripts/manifest_builder.py +2 -0
- package/.torusguard/scripts/recheck_runner.py +5 -4
- package/.torusguard/scripts/recipes_runner.py +91 -8
- package/.torusguard/scripts/report_sync.py +496 -73
- package/.torusguard/scripts/sarif_exporter.py +48 -8
- package/.torusguard/scripts/stack_detect.py +44 -2
- package/.torusguard/scripts/status_runner.py +267 -0
- package/.torusguard/scripts/term_ui.py +1 -1
- package/.torusguard/skills/torusguard/SKILL.md +75 -72
- package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
- package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
- package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
- package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
- package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
- package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
- package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
- package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
- package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
- package/.torusguard/workflows/apply.md +1 -1
- package/.torusguard/workflows/audit.md +1 -1
- package/.torusguard/workflows/authorize.md +1 -1
- package/.torusguard/workflows/exploit-check.md +1 -1
- package/.torusguard/workflows/harden.md +1 -1
- package/.torusguard/workflows/init.md +1 -1
- package/.torusguard/workflows/memory.md +1 -1
- package/.torusguard/workflows/recheck.md +1 -1
- package/.torusguard/workflows/report.md +1 -1
- package/.torusguard/workflows/status.md +1 -1
- package/.torusguard/workflows/verify.md +1 -1
- package/.torusguard/workflows/web-validate.md +1 -1
- package/README.md +115 -81
- package/bin/torusguard.js +244 -38
- package/package.json +6 -2
- package/skills/torusguard/SKILL.md +1 -1
- package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/skills/torusguard/bootstrap.py +52 -8
- package/skills/torusguard/payload/.manifest.json +51 -36
- package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
- package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
- package/skills/torusguard/payload/scripts/apply_runner.py +108 -9
- package/skills/torusguard/payload/scripts/audit_runner.py +336 -24
- package/skills/torusguard/payload/scripts/harden_runner.py +124 -70
- package/skills/torusguard/payload/scripts/html_reporter.py +3585 -648
- package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
- package/skills/torusguard/payload/scripts/recheck_runner.py +5 -4
- package/skills/torusguard/payload/scripts/recipes_runner.py +91 -8
- package/skills/torusguard/payload/scripts/report_sync.py +496 -73
- package/skills/torusguard/payload/scripts/sarif_exporter.py +48 -8
- package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
- package/skills/torusguard/payload/scripts/status_runner.py +267 -0
- package/skills/torusguard/payload/scripts/term_ui.py +1 -1
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +40 -23
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +1 -1
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +1 -1
- package/skills/torusguard/payload/workflows/apply.md +9 -1
- package/skills/torusguard/payload/workflows/audit.md +8 -2
- package/skills/torusguard/payload/workflows/authorize.md +1 -1
- package/skills/torusguard/payload/workflows/exploit-check.md +1 -1
- package/skills/torusguard/payload/workflows/harden.md +6 -1
- package/skills/torusguard/payload/workflows/init.md +7 -1
- package/skills/torusguard/payload/workflows/memory.md +1 -1
- package/skills/torusguard/payload/workflows/recheck.md +1 -1
- package/skills/torusguard/payload/workflows/recipes.md +55 -0
- package/skills/torusguard/payload/workflows/report.md +59 -55
- package/skills/torusguard/payload/workflows/status.md +4 -1
- package/skills/torusguard/payload/workflows/torusguard-apply.md +110 -0
- package/skills/torusguard/payload/workflows/torusguard-audit.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-authorize.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard-exploit-check.md +98 -0
- package/skills/torusguard/payload/workflows/torusguard-harden.md +104 -0
- package/skills/torusguard/payload/workflows/torusguard-init.md +106 -0
- package/skills/torusguard/payload/workflows/torusguard-recheck.md +101 -0
- package/skills/torusguard/payload/workflows/torusguard-recipes.md +55 -0
- package/skills/torusguard/payload/workflows/torusguard-report.md +105 -0
- package/skills/torusguard/payload/workflows/torusguard-status.md +103 -0
- package/skills/torusguard/payload/workflows/torusguard-verify.md +100 -0
- package/skills/torusguard/payload/workflows/torusguard-web-validate.md +96 -0
- package/skills/torusguard/payload/workflows/torusguard.md +19 -0
- package/skills/torusguard/payload/workflows/verify.md +1 -1
- package/skills/torusguard/payload/workflows/web-validate.md +1 -1
- package/skills/torusguard-apply/SKILL.md +1 -1
- package/skills/torusguard-audit/SKILL.md +1 -1
- package/skills/torusguard-authorize/SKILL.md +1 -1
- package/skills/torusguard-exploit-check/SKILL.md +1 -1
- package/skills/torusguard-full/SKILL.md +1 -1
- package/skills/torusguard-harden/SKILL.md +1 -1
- package/skills/torusguard-init/SKILL.md +1 -1
- package/skills/torusguard-recheck/SKILL.md +1 -1
- package/skills/torusguard-report/SKILL.md +40 -23
- package/skills/torusguard-status/SKILL.md +1 -1
- package/skills/torusguard-verify/SKILL.md +1 -1
- 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
|
+
TorusGuard Autonomous Static Security Audit Engine (v1.4.0)
|
|
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,
|
|
@@ -12,6 +12,7 @@ Pure Python 3.10+ standard library (zero external dependencies).
|
|
|
12
12
|
import sys
|
|
13
13
|
import os
|
|
14
14
|
import re
|
|
15
|
+
import time
|
|
15
16
|
import json
|
|
16
17
|
import hashlib
|
|
17
18
|
import argparse
|
|
@@ -117,7 +118,7 @@ except Exception:
|
|
|
117
118
|
rem = max(0, 68 - vis)
|
|
118
119
|
return f" {border_color}{left}{h} {BOLD}{WHITE}{title}{RESET}{border_color} {h * rem}{right}{RESET}"
|
|
119
120
|
return f" {border_color}{left}{h * 71}{right}{RESET}"
|
|
120
|
-
def card_header(title: str, subtitle: str = "", version: str = "v1.3.
|
|
121
|
+
def card_header(title: str, subtitle: str = "", version: str = "v1.3.6", border_color: str = CYAN) -> str:
|
|
121
122
|
top = f" {border_color}╭{'─' * 71}╮{RESET}"
|
|
122
123
|
bottom = f" {border_color}╰{'─' * 71}╯{RESET}"
|
|
123
124
|
empty = f" {border_color}│{' ' * 71}│{RESET}"
|
|
@@ -369,7 +370,9 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
369
370
|
"patterns": [
|
|
370
371
|
(re.compile(r'(?:query|execute|raw)\s*\(\s*f["\'].*SELECT.*\{', re.IGNORECASE), "Python f-string interpolation in raw SQL query"),
|
|
371
372
|
(re.compile(r'(?:query|execute|raw)\s*\(\s*[`"\'].*SELECT.*\$\{', re.IGNORECASE), "JavaScript template literal interpolation in SQL query"),
|
|
372
|
-
(re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query")
|
|
373
|
+
(re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query"),
|
|
374
|
+
(re.compile(r'(?:db|tx)\.(?:Query|Exec|QueryRow)\s*\(\s*fmt\.Sprintf\s*\(\s*["\'].*(?:SELECT|INSERT|UPDATE|DELETE).*%s', re.IGNORECASE), "Go database/sql query using fmt.Sprintf string interpolation instead of parameterized placeholders"),
|
|
375
|
+
(re.compile(r'(?:db|tx)\.(?:Query|Exec|QueryRow)\s*\(\s*["\'].*(?:SELECT|INSERT|UPDATE|DELETE).*\+\s*(?:r\.URL|c\.Query|input|param)', re.IGNORECASE), "Go database/sql query with string concatenation")
|
|
373
376
|
]
|
|
374
377
|
},
|
|
375
378
|
{
|
|
@@ -414,7 +417,8 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
414
417
|
"cluster": "cluster-injection",
|
|
415
418
|
"patterns": [
|
|
416
419
|
(re.compile(r'(?:open|readFile|readFileSync)\s*\([^)]*\+\s*(?:req|params|query)'), "File read operation using unsanitized user request path"),
|
|
417
|
-
(re.compile(r'path\.join\s*\([^)]*(?:req\.params|req\.query)\.[a-zA-Z0-9_]+\)'), "Path traversal sink in path.join without basename sanitization")
|
|
420
|
+
(re.compile(r'path\.join\s*\([^)]*(?:req\.params|req\.query)\.[a-zA-Z0-9_]+\)'), "Path traversal sink in path.join without basename sanitization"),
|
|
421
|
+
(re.compile(r'(?:os\.Open|os\.ReadFile|ioutil\.ReadFile)\s*\([^)]*(?:c\.Query|c\.Param|r\.URL\.Query|filepath\.Join\([^)]*(?:c\.Query|c\.Param|r\.URL\.Query))'), "Go file read operation using unsanitized user request parameter")
|
|
418
422
|
]
|
|
419
423
|
},
|
|
420
424
|
|
|
@@ -502,7 +506,8 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
502
506
|
"category": "ssrf",
|
|
503
507
|
"cluster": "cluster-ssrf",
|
|
504
508
|
"patterns": [
|
|
505
|
-
(re.compile(r'(?:fetch|axios\.get|requests\.get|urllib\.request\.urlopen)\s*\(\s*(?:req\.query|req\.body|request\.GET)\.[a-zA-Z0-9_]+'), "Outbound HTTP request directly invoking user-supplied URL parameter")
|
|
509
|
+
(re.compile(r'(?:fetch|axios\.get|requests\.get|urllib\.request\.urlopen)\s*\(\s*(?:req\.query|req\.body|request\.GET)\.[a-zA-Z0-9_]+'), "Outbound HTTP request directly invoking user-supplied URL parameter"),
|
|
510
|
+
(re.compile(r'http\.(?:Get|Post|Head)\s*\(\s*(?:r\.URL\.Query|c\.Query|c\.Param|req\.)'), "Go http.Get/Post directly invoking user-supplied URL parameter")
|
|
506
511
|
]
|
|
507
512
|
},
|
|
508
513
|
{
|
|
@@ -533,7 +538,9 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
533
538
|
"category": "ssrf",
|
|
534
539
|
"cluster": "cluster-ssrf",
|
|
535
540
|
"patterns": [
|
|
536
|
-
(re.compile(r'requests\.(?:get|post|put)\s*\((?![^)]*\btimeout\s*=)[^)]*\)'), "Python requests call without explicit timeout parameter")
|
|
541
|
+
(re.compile(r'requests\.(?:get|post|put)\s*\((?![^)]*\btimeout\s*=)[^)]*\)'), "Python requests call without explicit timeout parameter"),
|
|
542
|
+
(re.compile(r'&http\.Client\s*\{\s*\}'), "Go http.Client initialized without explicit Timeout boundary (defaults to unbounded zero)"),
|
|
543
|
+
(re.compile(r'http\.DefaultClient\.(?:Get|Post|Do)\s*\('), "Go http.DefaultClient call with zero/unbounded request timeout")
|
|
537
544
|
]
|
|
538
545
|
},
|
|
539
546
|
|
|
@@ -693,7 +700,8 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
693
700
|
"category": "supply",
|
|
694
701
|
"cluster": "cluster-supply-chain",
|
|
695
702
|
"patterns": [
|
|
696
|
-
(re.compile(r'^\s*package-lock\.json\s*$', re.MULTILINE), "Lockfile package-lock.json explicitly ignored in .gitignore")
|
|
703
|
+
(re.compile(r'^\s*package-lock\.json\s*$', re.MULTILINE), "Lockfile package-lock.json explicitly ignored in .gitignore"),
|
|
704
|
+
(re.compile(r'^\s*go\.sum\s*$', re.MULTILINE), "Go dependency checksum lockfile go.sum explicitly ignored in .gitignore")
|
|
697
705
|
]
|
|
698
706
|
},
|
|
699
707
|
{
|
|
@@ -948,6 +956,153 @@ RULE_PATTERNS: List[RulePattern] = [
|
|
|
948
956
|
}
|
|
949
957
|
]
|
|
950
958
|
|
|
959
|
+
# ─── Rule Precision Map ───────────────────────────────────────────────────────
|
|
960
|
+
# Calibrates how precise each rule's regex is (0.0 = very broad, 1.0 = definitive)
|
|
961
|
+
# Used to adjust confidence scores: broad rules get lower base evidence quality.
|
|
962
|
+
RULE_PRECISION: Dict[str, float] = {
|
|
963
|
+
"TG-SEC-001": 0.90, "TG-SEC-002": 0.85, "TG-SEC-003": 0.70,
|
|
964
|
+
"TG-SEC-004": 0.55, "TG-SEC-005": 0.80, "TG-SEC-006": 0.75,
|
|
965
|
+
"TG-SEC-007": 0.95,
|
|
966
|
+
"TG-AUTH-001": 0.85, "TG-AUTH-002": 0.50, "TG-AUTH-003": 0.70,
|
|
967
|
+
"TG-AUTH-004": 0.80, "TG-AUTH-005": 0.75, "TG-AUTH-006": 0.65,
|
|
968
|
+
"TG-AUTH-007": 0.60, "TG-AUTH-008": 0.70,
|
|
969
|
+
"TG-DB-001": 0.75, "TG-DB-002": 0.90, "TG-DB-003": 0.80, "TG-DB-004": 0.55,
|
|
970
|
+
"TG-INPUT-001": 0.40, "TG-INPUT-002": 0.85, "TG-INPUT-003": 0.35,
|
|
971
|
+
"TG-INPUT-004": 0.80, "TG-INPUT-005": 0.75, "TG-INPUT-006": 0.70,
|
|
972
|
+
"TG-RATE-001": 0.45, "TG-RATE-002": 0.45, "TG-RATE-003": 0.50,
|
|
973
|
+
"TG-AGENT-001": 0.80, "TG-AGENT-002": 0.85, "TG-AGENT-003": 0.60,
|
|
974
|
+
"TG-AGENT-004": 0.55,
|
|
975
|
+
"TG-SSRF-001": 0.75, "TG-SSRF-002": 0.90, "TG-SSRF-003": 0.50,
|
|
976
|
+
"TG-SSRF-004": 0.65,
|
|
977
|
+
"TG-WEBHOOK-001": 0.55, "TG-WEBHOOK-002": 0.50, "TG-WEBHOOK-003": 0.45,
|
|
978
|
+
"TG-WEBHOOK-004": 0.60,
|
|
979
|
+
"TG-WS-001": 0.50, "TG-WS-002": 0.55, "TG-WS-003": 0.50, "TG-WS-004": 0.55,
|
|
980
|
+
"TG-CSRF-001": 0.60, "TG-CSRF-002": 0.80,
|
|
981
|
+
"TG-GQL-001": 0.55, "TG-GQL-002": 0.50, "TG-GQL-003": 0.50, "TG-GQL-004": 0.85,
|
|
982
|
+
"TG-SUPPLY-001": 0.90, "TG-SUPPLY-002": 0.80, "TG-SUPPLY-003": 0.85,
|
|
983
|
+
"TG-SUPPLY-004": 0.60, "TG-SUPPLY-005": 0.80, "TG-SUPPLY-006": 0.85,
|
|
984
|
+
"TG-BIZ-001": 0.60, "TG-BIZ-002": 0.55, "TG-BIZ-003": 0.65, "TG-BIZ-004": 0.70,
|
|
985
|
+
"TG-CACHE-001": 0.65, "TG-CACHE-002": 0.50, "TG-CACHE-003": 0.55,
|
|
986
|
+
"TG-CLIENT-001": 0.80, "TG-CLIENT-002": 0.75,
|
|
987
|
+
"TG-PLATFORM-001": 0.70, "TG-PLATFORM-002": 0.50, "TG-PLATFORM-003": 0.75,
|
|
988
|
+
"TG-PLATFORM-004": 0.60,
|
|
989
|
+
"TG-DIFF-001": 0.90, "TG-DIFF-002": 0.85, "TG-DIFF-003": 0.80,
|
|
990
|
+
"TG-EDGE-001": 0.55, "TG-EDGE-003": 0.60,
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
# ─── File-Type Discrimination ─────────────────────────────────────────────────
|
|
994
|
+
# Maps file path patterns to sets of rule families that should be SUPPRESSED
|
|
995
|
+
FILE_TYPE_SUPPRESSIONS: List[Tuple[re.Pattern, set]] = [
|
|
996
|
+
# Config & build tool files — suppress injection rules for eval/exec
|
|
997
|
+
(re.compile(r'(?:webpack|vite|rollup|esbuild|jest|vitest|babel|eslint|prettier|postcss|tailwind)\.config', re.IGNORECASE),
|
|
998
|
+
{"TG-INPUT-003"}),
|
|
999
|
+
# Type declaration files — no runtime code
|
|
1000
|
+
(re.compile(r'\.d\.ts$'),
|
|
1001
|
+
{"TG-INPUT-001", "TG-INPUT-002", "TG-INPUT-003", "TG-INPUT-004", "TG-INPUT-005",
|
|
1002
|
+
"TG-RATE-001", "TG-RATE-002", "TG-RATE-003", "TG-DB-001", "TG-DB-004"}),
|
|
1003
|
+
# .env.example / .env.sample — template files, not real credentials
|
|
1004
|
+
(re.compile(r'\.env\.(?:example|sample|template|defaults)$', re.IGNORECASE),
|
|
1005
|
+
{"TG-SEC-003", "TG-SEC-001"}),
|
|
1006
|
+
# Lock files — suppress secret scanning
|
|
1007
|
+
(re.compile(r'(?:package-lock|yarn\.lock|pnpm-lock|Pipfile\.lock|poetry\.lock)'),
|
|
1008
|
+
{"TG-SEC-001", "TG-SEC-005"}),
|
|
1009
|
+
# Migration files — suppress input validation rules (generated SQL)
|
|
1010
|
+
(re.compile(r'/migrations?/', re.IGNORECASE),
|
|
1011
|
+
{"TG-INPUT-001", "TG-INPUT-002", "TG-RATE-001"}),
|
|
1012
|
+
# Dockerfiles — suppress frontend-only rules, keep container-specific rules
|
|
1013
|
+
(re.compile(r'Dockerfile', re.IGNORECASE),
|
|
1014
|
+
{"TG-INPUT-003", "TG-RATE-001", "TG-RATE-002", "TG-DB-001", "TG-GQL-001"}),
|
|
1015
|
+
]
|
|
1016
|
+
|
|
1017
|
+
def get_suppressed_rules_for_file(file_path: str) -> set:
|
|
1018
|
+
"""Return the set of rule IDs that should be suppressed for this file type."""
|
|
1019
|
+
suppressed: set = set()
|
|
1020
|
+
for pattern, rules in FILE_TYPE_SUPPRESSIONS:
|
|
1021
|
+
if pattern.search(file_path):
|
|
1022
|
+
suppressed.update(rules)
|
|
1023
|
+
return suppressed
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
# ─── Context-Aware Validation Engine ──────────────────────────────────────────
|
|
1027
|
+
def validate_finding_context(
|
|
1028
|
+
rule_id: str, line_idx: int, lines: List[str],
|
|
1029
|
+
file_path: str, full_content: str
|
|
1030
|
+
) -> bool:
|
|
1031
|
+
"""
|
|
1032
|
+
Post-match context validation. Returns False if the finding is a known
|
|
1033
|
+
false-positive pattern based on surrounding code context.
|
|
1034
|
+
"""
|
|
1035
|
+
# Compute lookahead (next 5 lines) and lookbehind (prev 5 lines)
|
|
1036
|
+
lookahead = "\n".join(lines[line_idx:min(line_idx + 6, len(lines))])
|
|
1037
|
+
lookbehind = "\n".join(lines[max(0, line_idx - 5):line_idx])
|
|
1038
|
+
|
|
1039
|
+
# ── TG-INPUT-001: req.body destructuring followed by schema validation ────
|
|
1040
|
+
if rule_id == "TG-INPUT-001":
|
|
1041
|
+
if re.search(r'\.(parse|validate|safeParse|check|verify|sanitize)\s*\(', lookahead):
|
|
1042
|
+
return False
|
|
1043
|
+
# Zod/Joi/Yup schema declared before destructuring
|
|
1044
|
+
if re.search(r'(?:z\.|Joi\.|yup\.|schema\.).*\.(parse|validate)', lookbehind + lookahead):
|
|
1045
|
+
return False
|
|
1046
|
+
|
|
1047
|
+
# ── TG-INPUT-003: eval/exec in sandboxed or build-tool context ────────────
|
|
1048
|
+
if rule_id == "TG-INPUT-003":
|
|
1049
|
+
current_line = lines[line_idx].strip() if line_idx < len(lines) else ""
|
|
1050
|
+
# Suppress if it's a JSON.parse (not dangerous)
|
|
1051
|
+
if "JSON.parse" in current_line or "json.loads" in current_line:
|
|
1052
|
+
return False
|
|
1053
|
+
# Suppress eval in CSS-in-JS tagged templates
|
|
1054
|
+
if re.search(r'styled|css`|keyframes', lookbehind):
|
|
1055
|
+
return False
|
|
1056
|
+
|
|
1057
|
+
# ── TG-RATE-001/002: Rate limiter already applied earlier in file ─────────
|
|
1058
|
+
if rule_id in ("TG-RATE-001", "TG-RATE-002"):
|
|
1059
|
+
if re.search(r'(?:rateLimit|rateLimiter|slowDown|throttle|express-rate-limit)\s*\(', full_content):
|
|
1060
|
+
return False
|
|
1061
|
+
# Check if middleware is applied on the same router
|
|
1062
|
+
if re.search(r'app\.use\s*\([^)]*(?:rateLimit|limiter|throttle)', full_content):
|
|
1063
|
+
return False
|
|
1064
|
+
|
|
1065
|
+
# ── TG-PLATFORM-002: Helmet used anywhere in the file or app setup ────────
|
|
1066
|
+
if rule_id == "TG-PLATFORM-002":
|
|
1067
|
+
if "helmet" in full_content and re.search(r'(?:app|server)\.use\s*\(\s*helmet', full_content):
|
|
1068
|
+
return False
|
|
1069
|
+
|
|
1070
|
+
# ── TG-SSRF-004: requests.get with timeout in kwargs or session defaults ──
|
|
1071
|
+
if rule_id == "TG-SSRF-004":
|
|
1072
|
+
# Check if a session with default timeout is set earlier
|
|
1073
|
+
if re.search(r'session\.(?:request\.)?timeout|DEFAULT_TIMEOUT', full_content):
|
|
1074
|
+
return False
|
|
1075
|
+
|
|
1076
|
+
# ── TG-AUTH-003: JWT verify with algorithms in options object ──────────────
|
|
1077
|
+
if rule_id == "TG-AUTH-003":
|
|
1078
|
+
if re.search(r'algorithms\s*:', lookahead):
|
|
1079
|
+
return False
|
|
1080
|
+
|
|
1081
|
+
# ── TG-GQL-001: ApolloServer with plugins/validation rules elsewhere ──────
|
|
1082
|
+
if rule_id == "TG-GQL-001":
|
|
1083
|
+
if re.search(r'(?:depthLimit|costAnalysis|complexityLimit|validationRules)', full_content):
|
|
1084
|
+
return False
|
|
1085
|
+
|
|
1086
|
+
# ── TG-SEC-004: Logging that redacts sensitive fields ──────────────────────
|
|
1087
|
+
if rule_id == "TG-SEC-004":
|
|
1088
|
+
if re.search(r'(?:redact|mask|sanitize|\*{3,}|\[REDACTED\])', lookahead):
|
|
1089
|
+
return False
|
|
1090
|
+
|
|
1091
|
+
# ── TG-DB-004: findById with ownership check in next few lines ────────────
|
|
1092
|
+
if rule_id == "TG-DB-004":
|
|
1093
|
+
if re.search(r'(?:tenantId|tenant_id|organization_id|user\.id|userId|ownerId)', lookahead):
|
|
1094
|
+
return False
|
|
1095
|
+
|
|
1096
|
+
# ── TG-WEBHOOK-001: Signature verification in handler body ────────────────
|
|
1097
|
+
if rule_id == "TG-WEBHOOK-001":
|
|
1098
|
+
# Check if HMAC/signature verification happens in the function body
|
|
1099
|
+
func_body = "\n".join(lines[line_idx:min(line_idx + 20, len(lines))])
|
|
1100
|
+
if re.search(r'(?:verifySignature|hmac|createHmac|verify_webhook|constructEvent)', func_body):
|
|
1101
|
+
return False
|
|
1102
|
+
|
|
1103
|
+
return True # Keep the finding
|
|
1104
|
+
|
|
1105
|
+
|
|
951
1106
|
# File extensions to scan
|
|
952
1107
|
SOURCE_EXTENSIONS = {
|
|
953
1108
|
".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
@@ -962,11 +1117,16 @@ SKIP_DIRS = {
|
|
|
962
1117
|
}
|
|
963
1118
|
|
|
964
1119
|
def is_test_file(path_str: str) -> bool:
|
|
965
|
-
"""Check if file belongs to tests, fixtures, or test harnesses."""
|
|
966
|
-
clean = path_str.replace("\\", "/").lower()
|
|
967
|
-
markers = [
|
|
968
|
-
|
|
969
|
-
|
|
1120
|
+
"""Check if file belongs to tests, fixtures, or test harnesses across any language."""
|
|
1121
|
+
clean = "/" + path_str.replace("\\", "/").strip("/").lower() + "/"
|
|
1122
|
+
markers = [
|
|
1123
|
+
"/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
|
|
1124
|
+
"/harness/", "/qa-runs/", "/test_repos/", "/__tests__/"
|
|
1125
|
+
]
|
|
1126
|
+
if any(m in clean for m in markers):
|
|
1127
|
+
return True
|
|
1128
|
+
filename = Path(path_str).name.lower()
|
|
1129
|
+
return filename.startswith("test_") or "_test." in filename or ".test." in filename or ".spec." in filename
|
|
970
1130
|
|
|
971
1131
|
|
|
972
1132
|
def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[Path]:
|
|
@@ -987,7 +1147,10 @@ def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[P
|
|
|
987
1147
|
|
|
988
1148
|
|
|
989
1149
|
def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
990
|
-
"""Scan a single source file against the TorusGuard rule families.
|
|
1150
|
+
"""Scan a single source file against the TorusGuard rule families.
|
|
1151
|
+
Enhanced with context-aware validation, file-type discrimination,
|
|
1152
|
+
developer suppression comments, canary tags, and multi-finding support.
|
|
1153
|
+
"""
|
|
991
1154
|
findings = []
|
|
992
1155
|
try:
|
|
993
1156
|
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
@@ -998,13 +1161,24 @@ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
|
998
1161
|
rel_path = str(file_path.relative_to(target_root)).replace("\\", "/")
|
|
999
1162
|
is_test = is_test_file(rel_path)
|
|
1000
1163
|
|
|
1164
|
+
# Phase 1c: File-type discrimination — get suppressed rules for this file
|
|
1165
|
+
suppressed_rules = get_suppressed_rules_for_file(rel_path)
|
|
1166
|
+
|
|
1001
1167
|
for rule in RULE_PATTERNS:
|
|
1002
1168
|
rule_id = rule["rule_id"]
|
|
1003
|
-
|
|
1169
|
+
|
|
1170
|
+
# Skip rules suppressed by file-type discrimination
|
|
1171
|
+
if rule_id in suppressed_rules:
|
|
1004
1172
|
continue
|
|
1173
|
+
|
|
1005
1174
|
patterns: List[Tuple[Pattern[str], str]] = rule["patterns"]
|
|
1175
|
+
found_lines: set = set() # Track matched lines to avoid duplicates
|
|
1176
|
+
|
|
1006
1177
|
for regex, desc in patterns:
|
|
1007
1178
|
for idx, line in enumerate(lines):
|
|
1179
|
+
if idx in found_lines:
|
|
1180
|
+
continue # Already reported a finding on this line for this rule
|
|
1181
|
+
|
|
1008
1182
|
stripped = line.strip()
|
|
1009
1183
|
if stripped.startswith(("#", "//", "/*", "*")):
|
|
1010
1184
|
if "bypass" not in stripped.lower() and "todo" not in stripped.lower():
|
|
@@ -1012,6 +1186,27 @@ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
|
1012
1186
|
|
|
1013
1187
|
match = regex.search(line)
|
|
1014
1188
|
if match:
|
|
1189
|
+
line_lower = line.lower()
|
|
1190
|
+
prev_line = lines[idx - 1].lower() if idx > 0 else ""
|
|
1191
|
+
|
|
1192
|
+
# Developer suppression annotations (e.g. # torusguard: ignore, // tg-ignore)
|
|
1193
|
+
if "torusguard: ignore" in line_lower or "torusguard: ignore" in prev_line or \
|
|
1194
|
+
"tg-ignore" in line_lower or "tg-ignore" in prev_line:
|
|
1195
|
+
if rule_id != "TG-DIFF-001":
|
|
1196
|
+
continue
|
|
1197
|
+
|
|
1198
|
+
# Developer test canary annotations (# torusguard: test-canary)
|
|
1199
|
+
is_canary = (
|
|
1200
|
+
"torusguard: test-canary" in line_lower or "torusguard: test-canary" in prev_line or
|
|
1201
|
+
"tg-canary" in line_lower or "tg-canary" in prev_line or
|
|
1202
|
+
"test-canary" in line_lower or "test-canary" in prev_line or
|
|
1203
|
+
is_test
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
# Phase 1a: Context-aware false positive suppression
|
|
1207
|
+
if not validate_finding_context(rule_id, idx, lines, rel_path, content):
|
|
1208
|
+
continue # Suppressed by context validation
|
|
1209
|
+
|
|
1015
1210
|
line_num = idx + 1
|
|
1016
1211
|
seed = f"{rule_id}:{rel_path}:{line_num}:{stripped}"
|
|
1017
1212
|
fp = f"{rule_id}-{hashlib.sha256(seed.encode('utf-8')).hexdigest()[:8]}"
|
|
@@ -1025,7 +1220,7 @@ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
|
1025
1220
|
"rule_id": rule_id,
|
|
1026
1221
|
"title": rule["title"],
|
|
1027
1222
|
"description": desc,
|
|
1028
|
-
"severity": "Low" if is_test else rule["severity"],
|
|
1223
|
+
"severity": "Low" if (is_test or is_canary) else rule["severity"],
|
|
1029
1224
|
"category": rule["category"],
|
|
1030
1225
|
"cluster": rule["cluster"],
|
|
1031
1226
|
"file_path": rel_path,
|
|
@@ -1033,13 +1228,15 @@ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
|
1033
1228
|
"matched_text": match.group(0)[:80],
|
|
1034
1229
|
"snippet": snippet,
|
|
1035
1230
|
"is_test": is_test,
|
|
1231
|
+
"is_canary": is_canary,
|
|
1232
|
+
"rule_precision": RULE_PRECISION.get(rule_id, 0.60),
|
|
1036
1233
|
"target": {
|
|
1037
1234
|
"file_path": rel_path,
|
|
1038
1235
|
"start_line": line_num,
|
|
1039
1236
|
"end_line": line_num
|
|
1040
1237
|
}
|
|
1041
1238
|
})
|
|
1042
|
-
|
|
1239
|
+
found_lines.add(idx)
|
|
1043
1240
|
return findings
|
|
1044
1241
|
|
|
1045
1242
|
|
|
@@ -1056,6 +1253,15 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
|
|
|
1056
1253
|
scored = []
|
|
1057
1254
|
clusters: Dict[str, List[Dict[str, Any]]] = {}
|
|
1058
1255
|
|
|
1256
|
+
# Phase 1d: Pre-compute cross-file corroboration bonus
|
|
1257
|
+
# If multiple rule families flag the same file, each finding gets +5 confidence
|
|
1258
|
+
file_rule_families: Dict[str, set] = {}
|
|
1259
|
+
for f in findings:
|
|
1260
|
+
fp = f["file_path"]
|
|
1261
|
+
if fp not in file_rule_families:
|
|
1262
|
+
file_rule_families[fp] = set()
|
|
1263
|
+
file_rule_families[fp].add(f["category"])
|
|
1264
|
+
|
|
1059
1265
|
for f in findings:
|
|
1060
1266
|
score = 70
|
|
1061
1267
|
band = "High Confidence"
|
|
@@ -1063,10 +1269,19 @@ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path
|
|
|
1063
1269
|
|
|
1064
1270
|
if finding_scorer:
|
|
1065
1271
|
try:
|
|
1066
|
-
|
|
1272
|
+
# Phase 1d: Dynamic evidence quality based on rule precision
|
|
1273
|
+
precision = f.get("rule_precision", RULE_PRECISION.get(f["rule_id"], 0.60))
|
|
1274
|
+
eq = max(10, int(35 * precision)) # Scale evidence quality by precision
|
|
1067
1275
|
rs = 0
|
|
1068
|
-
|
|
1069
|
-
|
|
1276
|
+
|
|
1277
|
+
# Corroboration bonus: multiple rule families in same file
|
|
1278
|
+
families_in_file = len(file_rule_families.get(f["file_path"], set()))
|
|
1279
|
+
ic = min(15, 5 + (families_in_file - 1) * 3) if families_in_file > 1 else 5
|
|
1280
|
+
|
|
1281
|
+
# Environmental clarity: production files score higher
|
|
1282
|
+
is_prod = not f.get("is_test", False)
|
|
1283
|
+
ec = 15 if is_prod else 5
|
|
1284
|
+
|
|
1070
1285
|
mr = 0
|
|
1071
1286
|
s, b, facts = finding_scorer.compute_confidence_score(
|
|
1072
1287
|
evidence_quality=eq,
|
|
@@ -1155,7 +1370,7 @@ def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]],
|
|
|
1155
1370
|
pass
|
|
1156
1371
|
|
|
1157
1372
|
|
|
1158
|
-
def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], run_folder: Path, detected_stack: Optional[Dict[str, Any]] = None) -> None:
|
|
1373
|
+
def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], run_folder: Path, detected_stack: Optional[Dict[str, Any]] = None, perf_metrics: Optional[Dict[str, Any]] = None) -> None:
|
|
1159
1374
|
"""Print the unified 75-column TorusGuard Static Audit results card."""
|
|
1160
1375
|
crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
|
|
1161
1376
|
high_count = sum(1 for f in scored_findings if f["severity"] == "High")
|
|
@@ -1166,13 +1381,17 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
|
|
|
1166
1381
|
stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
|
|
1167
1382
|
|
|
1168
1383
|
print()
|
|
1169
|
-
print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.
|
|
1384
|
+
print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.4.0"))
|
|
1170
1385
|
print()
|
|
1171
1386
|
|
|
1172
1387
|
print(card_border_top("Audit Execution Scope"))
|
|
1173
1388
|
print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root)}{RESET}"))
|
|
1174
1389
|
print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str}{RESET}"))
|
|
1175
1390
|
print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 18 rule families (74 rules){RESET}"))
|
|
1391
|
+
if perf_metrics:
|
|
1392
|
+
ms = perf_metrics.get("elapsed_ms", 0)
|
|
1393
|
+
fps = perf_metrics.get("throughput_fps", 0)
|
|
1394
|
+
print(format_box_line(f"{BOLD}Metrics:{RESET} {CYAN}{ms}ms{RESET} elapsed · {GREEN}{fps} files/sec{RESET} throughput"))
|
|
1176
1395
|
print(card_border_bottom())
|
|
1177
1396
|
print()
|
|
1178
1397
|
|
|
@@ -1182,6 +1401,17 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
|
|
|
1182
1401
|
print(format_box_line(f"Status: {status_icon}"))
|
|
1183
1402
|
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)"))
|
|
1184
1403
|
print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
|
|
1404
|
+
|
|
1405
|
+
# Rule Family Breakdown
|
|
1406
|
+
if scored_findings:
|
|
1407
|
+
fam_counts: Dict[str, int] = {}
|
|
1408
|
+
for f in scored_findings:
|
|
1409
|
+
rid = f.get("rule_id", "")
|
|
1410
|
+
parts = rid.split("-")
|
|
1411
|
+
fam = f"{parts[0]}-{parts[1]}" if len(parts) >= 2 else "OTHER"
|
|
1412
|
+
fam_counts[fam] = fam_counts.get(fam, 0) + 1
|
|
1413
|
+
fam_items = [f"{YELLOW}{k}{RESET}: {v}" for k, v in sorted(fam_counts.items(), key=lambda x: -x[1])[:4]]
|
|
1414
|
+
print(format_box_line(f"Families: {' '.join(fam_items)}"))
|
|
1185
1415
|
print(card_divider())
|
|
1186
1416
|
|
|
1187
1417
|
if not scored_findings:
|
|
@@ -1211,8 +1441,69 @@ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: L
|
|
|
1211
1441
|
print()
|
|
1212
1442
|
|
|
1213
1443
|
|
|
1444
|
+
def export_sarif(target_root: Path, run_folder_str: Optional[str], sarif_out: Optional[str] = None) -> None:
|
|
1445
|
+
"""Export audit run findings to OASIS SARIF v2.1.0 format."""
|
|
1446
|
+
s_dir = Path(__file__).resolve().parent
|
|
1447
|
+
sarif_script = s_dir / "sarif_exporter.py"
|
|
1448
|
+
if sarif_script.is_file():
|
|
1449
|
+
try:
|
|
1450
|
+
import importlib.util
|
|
1451
|
+
spec = importlib.util.spec_from_file_location("sarif_exporter", str(sarif_script))
|
|
1452
|
+
if spec and spec.loader:
|
|
1453
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1454
|
+
spec.loader.exec_module(mod)
|
|
1455
|
+
run_id = Path(run_folder_str).name if run_folder_str else "audit-run"
|
|
1456
|
+
findings = []
|
|
1457
|
+
if run_folder_str and (Path(run_folder_str) / "findings.json").is_file():
|
|
1458
|
+
with open(Path(run_folder_str) / "findings.json", "r", encoding="utf-8") as f:
|
|
1459
|
+
findings = json.load(f)
|
|
1460
|
+
sarif_data = mod.generate_sarif(findings, run_id=run_id)
|
|
1461
|
+
out_path = Path(sarif_out) if sarif_out else (target_root / ".torusguard" / "reports" / "torusguard-audit.sarif")
|
|
1462
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1463
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
1464
|
+
json.dump(sarif_data, f, indent=2)
|
|
1465
|
+
print(f" {GREEN}✔ OASIS SARIF v2.1.0 exported to:{RESET} {CYAN}{out_path}{RESET}\n")
|
|
1466
|
+
except Exception as e:
|
|
1467
|
+
print(f" {YELLOW}⚠ SARIF export notice: {e}{RESET}")
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def run_watch_mode(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False, sarif: bool = False, sarif_out: Optional[str] = None) -> None:
|
|
1471
|
+
"""Run audit continuously, re-scanning when files are modified."""
|
|
1472
|
+
print(f"\n {CYAN}👀 TorusGuard Watch Mode active.{RESET} Watching {WHITE}{target_root}{RESET} (Ctrl+C to stop)...\n")
|
|
1473
|
+
|
|
1474
|
+
def get_file_mtimes() -> Dict[str, float]:
|
|
1475
|
+
mtimes = {}
|
|
1476
|
+
for f in find_files_to_scan(target_root, include_tests=include_tests):
|
|
1477
|
+
try:
|
|
1478
|
+
mtimes[str(f)] = f.stat().st_mtime
|
|
1479
|
+
except OSError:
|
|
1480
|
+
pass
|
|
1481
|
+
return mtimes
|
|
1482
|
+
|
|
1483
|
+
last_mtimes = get_file_mtimes()
|
|
1484
|
+
res = execute_audit(target_root, severity_floor=severity_floor, json_output=json_output, include_tests=include_tests)
|
|
1485
|
+
if sarif:
|
|
1486
|
+
export_sarif(target_root, res.get("run_folder"), sarif_out)
|
|
1487
|
+
|
|
1488
|
+
try:
|
|
1489
|
+
while True:
|
|
1490
|
+
time.sleep(1.0)
|
|
1491
|
+
curr_mtimes = get_file_mtimes()
|
|
1492
|
+
if curr_mtimes != last_mtimes:
|
|
1493
|
+
changed = [f for f in curr_mtimes if f not in last_mtimes or curr_mtimes[f] != last_mtimes.get(f)]
|
|
1494
|
+
last_mtimes = curr_mtimes
|
|
1495
|
+
now_str = get_ist_now().strftime("%H:%M:%S")
|
|
1496
|
+
print(f"\n {CYAN}↻ File changes detected ({len(changed)} file(s)) at {now_str}. Re-scanning...{RESET}\n")
|
|
1497
|
+
res = execute_audit(target_root, severity_floor=severity_floor, json_output=json_output, include_tests=include_tests)
|
|
1498
|
+
if sarif:
|
|
1499
|
+
export_sarif(target_root, res.get("run_folder"), sarif_out)
|
|
1500
|
+
except KeyboardInterrupt:
|
|
1501
|
+
print(f"\n {YELLOW}🛑 Watch mode stopped.{RESET}\n")
|
|
1502
|
+
|
|
1503
|
+
|
|
1214
1504
|
def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
|
|
1215
1505
|
"""Execute the full TorusGuard static security audit."""
|
|
1506
|
+
start_time = time.perf_counter()
|
|
1216
1507
|
target_root = target_root.resolve()
|
|
1217
1508
|
s_dir = Path(__file__).resolve().parent
|
|
1218
1509
|
|
|
@@ -1267,6 +1558,13 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1267
1558
|
except Exception:
|
|
1268
1559
|
pass
|
|
1269
1560
|
|
|
1561
|
+
elapsed_ms = int((time.perf_counter() - start_time) * 1000)
|
|
1562
|
+
throughput = round(len(files) / max(0.001, (elapsed_ms / 1000.0)), 1)
|
|
1563
|
+
perf_metrics = {
|
|
1564
|
+
"elapsed_ms": elapsed_ms,
|
|
1565
|
+
"throughput_fps": throughput
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1270
1568
|
result = {
|
|
1271
1569
|
"status": "completed",
|
|
1272
1570
|
"target_root": str(target_root),
|
|
@@ -1277,13 +1575,15 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1277
1575
|
"medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
|
|
1278
1576
|
"clusters_count": len(clusters),
|
|
1279
1577
|
"run_folder": str(run_folder),
|
|
1280
|
-
"findings": scored_findings
|
|
1578
|
+
"findings": scored_findings,
|
|
1579
|
+
"elapsed_ms": elapsed_ms,
|
|
1580
|
+
"throughput_fps": throughput
|
|
1281
1581
|
}
|
|
1282
1582
|
|
|
1283
1583
|
if json_output:
|
|
1284
1584
|
print(json.dumps(result, indent=2))
|
|
1285
1585
|
else:
|
|
1286
|
-
print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
|
|
1586
|
+
print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack, perf_metrics=perf_metrics)
|
|
1287
1587
|
|
|
1288
1588
|
return result
|
|
1289
1589
|
|
|
@@ -1291,14 +1591,26 @@ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output
|
|
|
1291
1591
|
def main():
|
|
1292
1592
|
parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
|
|
1293
1593
|
parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
|
|
1594
|
+
parser.add_argument("--target", "-t", help="Target project root directory (alias for path)")
|
|
1294
1595
|
parser.add_argument("--scope", "-s", help="Alternative path to target project")
|
|
1295
1596
|
parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
|
|
1597
|
+
parser.add_argument("--watch", "-w", action="store_true", help="Continuous watch mode: re-scan on file save")
|
|
1598
|
+
parser.add_argument("--sarif", action="store_true", help="Automatically export findings to OASIS SARIF v2.1.0")
|
|
1599
|
+
parser.add_argument("--sarif-out", help="Output file path for SARIF export")
|
|
1296
1600
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
1297
1601
|
parser.add_argument("--include-tests", action="store_true", help="Include test files and harness fixtures in scan")
|
|
1298
1602
|
args = parser.parse_args()
|
|
1299
1603
|
|
|
1300
|
-
target = Path(args.scope or args.path).resolve()
|
|
1604
|
+
target = Path(args.target or args.scope or args.path).resolve()
|
|
1605
|
+
|
|
1606
|
+
if args.watch:
|
|
1607
|
+
run_watch_mode(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests, sarif=args.sarif, sarif_out=args.sarif_out)
|
|
1608
|
+
sys.exit(0)
|
|
1609
|
+
|
|
1301
1610
|
res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
|
|
1611
|
+
if args.sarif:
|
|
1612
|
+
export_sarif(target, res.get("run_folder"), args.sarif_out)
|
|
1613
|
+
|
|
1302
1614
|
sys.exit(0 if res["critical_count"] == 0 else 1)
|
|
1303
1615
|
|
|
1304
1616
|
|