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.
Files changed (194) hide show
  1. package/.torusguard/.manifest.json +37 -36
  2. package/.torusguard/runs/report-latest.html +5862 -315
  3. package/.torusguard/runs/results-latest.sarif +20 -0
  4. package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
  5. package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
  6. package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
  7. package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
  8. package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
  9. package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
  10. package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
  11. package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
  12. package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
  13. package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
  14. package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
  15. package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
  16. package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
  17. package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
  18. package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
  19. package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
  20. package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
  21. package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
  22. package/.torusguard/runs/run-20260918-105940-audit/manifest.json +13 -0
  23. package/.torusguard/runs/run-20260918-105940-audit/remediation.md +7 -0
  24. package/.torusguard/runs/run-20260918-105940-audit/summary.md +5 -0
  25. package/.torusguard/runs/run-20260919-145701-audit/findings.json +1 -0
  26. package/.torusguard/runs/run-20260919-145701-audit/findings.md +8 -0
  27. package/.torusguard/runs/run-20260919-145701-audit/manifest.json +12 -0
  28. package/.torusguard/runs/run-20260919-145701-audit/summary.md +5 -0
  29. package/.torusguard/runs/run-20260919-151729-audit/findings.json +1 -0
  30. package/.torusguard/runs/run-20260919-151729-audit/findings.md +8 -0
  31. package/.torusguard/runs/run-20260919-151729-audit/manifest.json +13 -0
  32. package/.torusguard/runs/run-20260919-151729-audit/remediation.md +7 -0
  33. package/.torusguard/runs/run-20260919-151729-audit/summary.md +5 -0
  34. package/.torusguard/runs/run-20260919-152340-audit/findings.json +1 -0
  35. package/.torusguard/runs/run-20260919-152340-audit/findings.md +8 -0
  36. package/.torusguard/runs/run-20260919-152340-audit/manifest.json +12 -0
  37. package/.torusguard/runs/run-20260919-152340-audit/summary.md +5 -0
  38. package/.torusguard/runs/run-20260919-152345-audit/findings.json +1 -0
  39. package/.torusguard/runs/run-20260919-152345-audit/findings.md +8 -0
  40. package/.torusguard/runs/run-20260919-152345-audit/manifest.json +12 -0
  41. package/.torusguard/runs/run-20260919-152345-audit/summary.md +5 -0
  42. package/.torusguard/runs/run-20260919-153500-audit/findings.json +1 -0
  43. package/.torusguard/runs/run-20260919-153500-audit/findings.md +8 -0
  44. package/.torusguard/runs/run-20260919-153500-audit/manifest.json +12 -0
  45. package/.torusguard/runs/run-20260919-153500-audit/summary.md +5 -0
  46. package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  47. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  48. package/.torusguard/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  49. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  50. package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  51. package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  52. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  53. package/.torusguard/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  54. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  55. package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  56. package/.torusguard/scripts/__pycache__/recipes_runner.cpython-314.pyc +0 -0
  57. package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  58. package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  59. package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  60. package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  61. package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  62. package/.torusguard/scripts/apply_runner.py +108 -9
  63. package/.torusguard/scripts/audit_runner.py +336 -24
  64. package/.torusguard/scripts/harden_runner.py +124 -70
  65. package/.torusguard/scripts/html_reporter.py +3585 -648
  66. package/.torusguard/scripts/manifest_builder.py +2 -0
  67. package/.torusguard/scripts/recheck_runner.py +5 -4
  68. package/.torusguard/scripts/recipes_runner.py +91 -8
  69. package/.torusguard/scripts/report_sync.py +496 -73
  70. package/.torusguard/scripts/sarif_exporter.py +48 -8
  71. package/.torusguard/scripts/stack_detect.py +44 -2
  72. package/.torusguard/scripts/status_runner.py +267 -0
  73. package/.torusguard/scripts/term_ui.py +1 -1
  74. package/.torusguard/skills/torusguard/SKILL.md +75 -72
  75. package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
  76. package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
  77. package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
  78. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
  79. package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
  80. package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
  81. package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
  82. package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
  83. package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
  84. package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
  85. package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
  86. package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
  87. package/.torusguard/workflows/apply.md +1 -1
  88. package/.torusguard/workflows/audit.md +1 -1
  89. package/.torusguard/workflows/authorize.md +1 -1
  90. package/.torusguard/workflows/exploit-check.md +1 -1
  91. package/.torusguard/workflows/harden.md +1 -1
  92. package/.torusguard/workflows/init.md +1 -1
  93. package/.torusguard/workflows/memory.md +1 -1
  94. package/.torusguard/workflows/recheck.md +1 -1
  95. package/.torusguard/workflows/report.md +1 -1
  96. package/.torusguard/workflows/status.md +1 -1
  97. package/.torusguard/workflows/verify.md +1 -1
  98. package/.torusguard/workflows/web-validate.md +1 -1
  99. package/README.md +115 -81
  100. package/bin/torusguard.js +244 -38
  101. package/package.json +6 -2
  102. package/skills/torusguard/SKILL.md +1 -1
  103. package/skills/torusguard/__pycache__/bootstrap.cpython-314.pyc +0 -0
  104. package/skills/torusguard/bootstrap.py +52 -8
  105. package/skills/torusguard/payload/.manifest.json +51 -36
  106. package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  107. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  108. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  109. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  110. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  111. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  112. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  113. package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  114. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  115. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  116. package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  117. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  118. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  119. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  120. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  121. package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  122. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  123. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  124. package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  125. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
  126. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  127. package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  128. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  129. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  130. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  131. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  132. package/skills/torusguard/payload/scripts/apply_runner.py +108 -9
  133. package/skills/torusguard/payload/scripts/audit_runner.py +336 -24
  134. package/skills/torusguard/payload/scripts/harden_runner.py +124 -70
  135. package/skills/torusguard/payload/scripts/html_reporter.py +3585 -648
  136. package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
  137. package/skills/torusguard/payload/scripts/recheck_runner.py +5 -4
  138. package/skills/torusguard/payload/scripts/recipes_runner.py +91 -8
  139. package/skills/torusguard/payload/scripts/report_sync.py +496 -73
  140. package/skills/torusguard/payload/scripts/sarif_exporter.py +48 -8
  141. package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
  142. package/skills/torusguard/payload/scripts/status_runner.py +267 -0
  143. package/skills/torusguard/payload/scripts/term_ui.py +1 -1
  144. package/skills/torusguard/payload/skills/torusguard/SKILL.md +1 -1
  145. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +1 -1
  146. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +1 -1
  147. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +1 -1
  148. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +1 -1
  149. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
  150. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +1 -1
  151. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +1 -1
  152. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +1 -1
  153. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +40 -23
  154. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +1 -1
  155. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +1 -1
  156. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +1 -1
  157. package/skills/torusguard/payload/workflows/apply.md +9 -1
  158. package/skills/torusguard/payload/workflows/audit.md +8 -2
  159. package/skills/torusguard/payload/workflows/authorize.md +1 -1
  160. package/skills/torusguard/payload/workflows/exploit-check.md +1 -1
  161. package/skills/torusguard/payload/workflows/harden.md +6 -1
  162. package/skills/torusguard/payload/workflows/init.md +7 -1
  163. package/skills/torusguard/payload/workflows/memory.md +1 -1
  164. package/skills/torusguard/payload/workflows/recheck.md +1 -1
  165. package/skills/torusguard/payload/workflows/recipes.md +55 -0
  166. package/skills/torusguard/payload/workflows/report.md +59 -55
  167. package/skills/torusguard/payload/workflows/status.md +4 -1
  168. package/skills/torusguard/payload/workflows/torusguard-apply.md +110 -0
  169. package/skills/torusguard/payload/workflows/torusguard-audit.md +105 -0
  170. package/skills/torusguard/payload/workflows/torusguard-authorize.md +96 -0
  171. package/skills/torusguard/payload/workflows/torusguard-exploit-check.md +98 -0
  172. package/skills/torusguard/payload/workflows/torusguard-harden.md +104 -0
  173. package/skills/torusguard/payload/workflows/torusguard-init.md +106 -0
  174. package/skills/torusguard/payload/workflows/torusguard-recheck.md +101 -0
  175. package/skills/torusguard/payload/workflows/torusguard-recipes.md +55 -0
  176. package/skills/torusguard/payload/workflows/torusguard-report.md +105 -0
  177. package/skills/torusguard/payload/workflows/torusguard-status.md +103 -0
  178. package/skills/torusguard/payload/workflows/torusguard-verify.md +100 -0
  179. package/skills/torusguard/payload/workflows/torusguard-web-validate.md +96 -0
  180. package/skills/torusguard/payload/workflows/torusguard.md +19 -0
  181. package/skills/torusguard/payload/workflows/verify.md +1 -1
  182. package/skills/torusguard/payload/workflows/web-validate.md +1 -1
  183. package/skills/torusguard-apply/SKILL.md +1 -1
  184. package/skills/torusguard-audit/SKILL.md +1 -1
  185. package/skills/torusguard-authorize/SKILL.md +1 -1
  186. package/skills/torusguard-exploit-check/SKILL.md +1 -1
  187. package/skills/torusguard-full/SKILL.md +1 -1
  188. package/skills/torusguard-harden/SKILL.md +1 -1
  189. package/skills/torusguard-init/SKILL.md +1 -1
  190. package/skills/torusguard-recheck/SKILL.md +1 -1
  191. package/skills/torusguard-report/SKILL.md +40 -23
  192. package/skills/torusguard-status/SKILL.md +1 -1
  193. package/skills/torusguard-verify/SKILL.md +1 -1
  194. 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.5)
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.5", border_color: str = CYAN) -> str:
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 = ["/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
968
- "/harness/", "__tests__", "test_", "_test.", ".test.", ".spec."]
969
- return any(m in clean for m in markers)
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
- if rule_id == "TG-PLATFORM-002" and "helmet" in content and "app.use(helmet" in content:
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
- break
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
- eq = 35
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
- ic = 5
1069
- ec = 15
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.3.5"))
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