torusguard 1.3.4 → 1.3.6

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 (319) hide show
  1. package/.torusguard/.manifest.json +66 -63
  2. package/.torusguard/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
  3. package/.torusguard/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
  4. package/.torusguard/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
  5. package/.torusguard/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
  6. package/.torusguard/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
  7. package/.torusguard/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
  8. package/.torusguard/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
  9. package/.torusguard/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
  10. package/.torusguard/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
  11. package/.torusguard/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
  12. package/.torusguard/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
  13. package/.torusguard/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
  14. package/.torusguard/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
  15. package/.torusguard/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
  16. package/.torusguard/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
  17. package/.torusguard/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
  18. package/.torusguard/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
  19. package/.torusguard/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
  20. package/.torusguard/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
  21. package/.torusguard/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
  22. package/.torusguard/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
  23. package/.torusguard/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
  24. package/.torusguard/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
  25. package/.torusguard/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
  26. package/.torusguard/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
  27. package/.torusguard/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
  28. package/.torusguard/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
  29. package/.torusguard/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
  30. package/.torusguard/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
  31. package/.torusguard/rules_catalog.json +520 -0
  32. package/.torusguard/runs/report-latest.html +2700 -232
  33. package/.torusguard/runs/results-latest.sarif +20 -0
  34. package/.torusguard/runs/run-20260912-214110-audit/findings.json +1256 -0
  35. package/.torusguard/runs/run-20260912-214110-audit/findings.md +534 -0
  36. package/.torusguard/runs/run-20260912-214110-audit/manifest.json +12 -0
  37. package/.torusguard/runs/run-20260912-214110-audit/summary.md +5 -0
  38. package/.torusguard/runs/run-20260912-214813-audit/findings.json +992 -0
  39. package/.torusguard/runs/run-20260912-214813-audit/findings.md +422 -0
  40. package/.torusguard/runs/run-20260912-214813-audit/manifest.json +12 -0
  41. package/.torusguard/runs/run-20260912-214813-audit/summary.md +5 -0
  42. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/metadata.json +17 -0
  43. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/minimal_patch_plan.md +21 -0
  44. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/patch.diff +9 -0
  45. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
  46. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  47. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  48. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
  49. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
  50. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
  51. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
  52. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
  53. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
  54. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
  55. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
  56. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
  57. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
  58. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
  59. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
  60. package/.torusguard/runs/run-20260912-215555-audit/findings.json +530 -0
  61. package/.torusguard/runs/run-20260912-215555-audit/findings.md +232 -0
  62. package/.torusguard/runs/run-20260912-215555-audit/manifest.json +13 -0
  63. package/.torusguard/runs/run-20260912-215555-audit/remediation.md +102 -0
  64. package/.torusguard/runs/run-20260912-215555-audit/summary.md +5 -0
  65. package/.torusguard/runs/run-20260912-215731-audit/apply_plan.md +6 -0
  66. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/metadata.json +17 -0
  67. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/minimal_patch_plan.md +21 -0
  68. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/patch.diff +9 -0
  69. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/metadata.json +17 -0
  70. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/minimal_patch_plan.md +21 -0
  71. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/patch.diff +9 -0
  72. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/metadata.json +17 -0
  73. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/minimal_patch_plan.md +22 -0
  74. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/patch.diff +10 -0
  75. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
  76. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  77. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  78. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
  79. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
  80. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
  81. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/metadata.json +17 -0
  82. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/minimal_patch_plan.md +23 -0
  83. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/patch.diff +11 -0
  84. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/metadata.json +17 -0
  85. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/minimal_patch_plan.md +23 -0
  86. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/patch.diff +11 -0
  87. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
  88. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
  89. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
  90. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/metadata.json +17 -0
  91. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/minimal_patch_plan.md +21 -0
  92. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/patch.diff +9 -0
  93. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/metadata.json +17 -0
  94. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/minimal_patch_plan.md +21 -0
  95. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/patch.diff +9 -0
  96. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
  97. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
  98. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
  99. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/metadata.json +17 -0
  100. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/minimal_patch_plan.md +21 -0
  101. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/patch.diff +9 -0
  102. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/metadata.json +17 -0
  103. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/minimal_patch_plan.md +21 -0
  104. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/patch.diff +9 -0
  105. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
  106. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
  107. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
  108. package/.torusguard/runs/run-20260912-215731-audit/diff_summary.md +186 -0
  109. package/.torusguard/runs/run-20260912-215731-audit/findings.json +497 -0
  110. package/.torusguard/runs/run-20260912-215731-audit/findings.md +218 -0
  111. package/.torusguard/runs/run-20260912-215731-audit/manifest.json +14 -0
  112. package/.torusguard/runs/run-20260912-215731-audit/recheck.md +76 -0
  113. package/.torusguard/runs/run-20260912-215731-audit/remediation.md +234 -0
  114. package/.torusguard/runs/run-20260912-215731-audit/summary.md +5 -0
  115. package/.torusguard/runs/run-20260912-220134-audit/findings.json +68 -0
  116. package/.torusguard/runs/run-20260912-220134-audit/findings.md +36 -0
  117. package/.torusguard/runs/run-20260912-220134-audit/manifest.json +12 -0
  118. package/.torusguard/runs/run-20260912-220134-audit/summary.md +5 -0
  119. package/.torusguard/runs/run-20260912-220200-audit/findings.json +1 -0
  120. package/.torusguard/runs/run-20260912-220200-audit/findings.md +8 -0
  121. package/.torusguard/runs/run-20260912-220200-audit/manifest.json +12 -0
  122. package/.torusguard/runs/run-20260912-220200-audit/recheck.md +6 -0
  123. package/.torusguard/runs/run-20260912-220200-audit/summary.md +5 -0
  124. package/.torusguard/runs/run-20260912-220227-audit/findings.json +1 -0
  125. package/.torusguard/runs/run-20260912-220227-audit/findings.md +8 -0
  126. package/.torusguard/runs/run-20260912-220227-audit/manifest.json +12 -0
  127. package/.torusguard/runs/run-20260912-220227-audit/recheck.md +6 -0
  128. package/.torusguard/runs/run-20260912-220227-audit/summary.md +5 -0
  129. package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
  130. package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
  131. package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
  132. package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
  133. package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
  134. package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
  135. package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
  136. package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
  137. package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
  138. package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
  139. package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
  140. package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
  141. package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
  142. package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
  143. package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
  144. package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
  145. package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
  146. package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
  147. package/.torusguard/runs/run-20260918-105940-audit/manifest.json +12 -0
  148. package/.torusguard/runs/run-20260918-105940-audit/summary.md +5 -0
  149. package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  150. package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  151. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  152. package/.torusguard/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  153. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  154. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  155. package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  156. package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  157. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  158. package/.torusguard/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  159. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  160. package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  161. package/.torusguard/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  162. package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  163. package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  164. package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  165. package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  166. package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  167. package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  168. package/.torusguard/scripts/apply_runner.py +389 -377
  169. package/.torusguard/scripts/audit_runner.py +1513 -653
  170. package/.torusguard/scripts/finding_scorer.py +14 -2
  171. package/.torusguard/scripts/harden_runner.py +628 -455
  172. package/.torusguard/scripts/html_reporter.py +2668 -648
  173. package/.torusguard/scripts/recheck_runner.py +248 -238
  174. package/.torusguard/scripts/recipes_runner.py +137 -137
  175. package/.torusguard/scripts/report_sync.py +719 -0
  176. package/.torusguard/scripts/sarif_exporter.py +156 -116
  177. package/.torusguard/scripts/stack_detect.py +44 -2
  178. package/.torusguard/scripts/term_ui.py +162 -162
  179. package/.torusguard/skills/torusguard/SKILL.md +75 -72
  180. package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
  181. package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
  182. package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
  183. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
  184. package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
  185. package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
  186. package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
  187. package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
  188. package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
  189. package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
  190. package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
  191. package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
  192. package/.torusguard/snapshots/run-20260912-215731-audit/demo/playground/vulnerable_nextjs/actions.ts.bak +24 -0
  193. package/.torusguard/snapshots/run-20260912-215731-audit/examples/hardened-react-express/server/index.js.bak +121 -0
  194. package/.torusguard/snapshots/run-20260912-215731-audit/examples/python/fastapi-vuln/main.py.bak +20 -0
  195. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/src/App.jsx.bak +63 -0
  196. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/vite.config.js.bak +12 -0
  197. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/server/index.js.bak +82 -0
  198. package/.torusguard/workflows/apply.md +11 -4
  199. package/.torusguard/workflows/audit.md +7 -1
  200. package/.torusguard/workflows/authorize.md +2 -1
  201. package/.torusguard/workflows/exploit-check.md +2 -1
  202. package/.torusguard/workflows/harden.md +7 -1
  203. package/.torusguard/workflows/init.md +6 -1
  204. package/.torusguard/workflows/memory.md +2 -1
  205. package/.torusguard/workflows/recheck.md +7 -1
  206. package/.torusguard/workflows/report.md +7 -1
  207. package/.torusguard/workflows/status.md +6 -1
  208. package/.torusguard/workflows/verify.md +6 -1
  209. package/.torusguard/workflows/web-validate.md +2 -1
  210. package/README.md +476 -345
  211. package/bin/torusguard.js +586 -545
  212. package/package.json +1 -1
  213. package/skills/torusguard/SKILL.md +73 -72
  214. package/skills/torusguard/bootstrap.py +7 -2
  215. package/skills/torusguard/payload/.manifest.json +66 -63
  216. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
  217. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
  218. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
  219. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
  220. package/skills/torusguard/payload/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
  221. package/skills/torusguard/payload/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
  222. package/skills/torusguard/payload/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
  223. package/skills/torusguard/payload/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
  224. package/skills/torusguard/payload/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
  225. package/skills/torusguard/payload/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
  226. package/skills/torusguard/payload/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
  227. package/skills/torusguard/payload/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
  228. package/skills/torusguard/payload/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
  229. package/skills/torusguard/payload/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
  230. package/skills/torusguard/payload/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
  231. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
  232. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
  233. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
  234. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
  235. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
  236. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
  237. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
  238. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
  239. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
  240. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
  241. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
  242. package/skills/torusguard/payload/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
  243. package/skills/torusguard/payload/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
  244. package/skills/torusguard/payload/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
  245. package/skills/torusguard/payload/rules_catalog.json +520 -0
  246. package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  247. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  248. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  249. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  250. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  251. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  252. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  253. package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  254. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  255. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  256. package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  257. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  258. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  259. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  260. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  261. package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  262. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  263. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  264. package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  265. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
  266. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  267. package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  268. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  269. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  270. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  271. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  272. package/skills/torusguard/payload/scripts/apply_runner.py +389 -377
  273. package/skills/torusguard/payload/scripts/audit_runner.py +1513 -653
  274. package/skills/torusguard/payload/scripts/finding_scorer.py +14 -2
  275. package/skills/torusguard/payload/scripts/harden_runner.py +628 -455
  276. package/skills/torusguard/payload/scripts/html_reporter.py +2668 -648
  277. package/skills/torusguard/payload/scripts/recheck_runner.py +248 -238
  278. package/skills/torusguard/payload/scripts/recipes_runner.py +137 -137
  279. package/skills/torusguard/payload/scripts/report_sync.py +719 -0
  280. package/skills/torusguard/payload/scripts/sarif_exporter.py +156 -116
  281. package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
  282. package/skills/torusguard/payload/scripts/term_ui.py +162 -162
  283. package/skills/torusguard/payload/skills/torusguard/SKILL.md +75 -72
  284. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +84 -80
  285. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +87 -83
  286. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +7 -2
  287. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +7 -2
  288. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +7 -2
  289. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +98 -94
  290. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +68 -64
  291. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +81 -77
  292. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +93 -72
  293. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +62 -58
  294. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +7 -2
  295. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +7 -2
  296. package/skills/torusguard/payload/workflows/apply.md +11 -4
  297. package/skills/torusguard/payload/workflows/audit.md +7 -1
  298. package/skills/torusguard/payload/workflows/authorize.md +2 -1
  299. package/skills/torusguard/payload/workflows/exploit-check.md +2 -1
  300. package/skills/torusguard/payload/workflows/harden.md +7 -1
  301. package/skills/torusguard/payload/workflows/init.md +6 -1
  302. package/skills/torusguard/payload/workflows/memory.md +2 -1
  303. package/skills/torusguard/payload/workflows/recheck.md +7 -1
  304. package/skills/torusguard/payload/workflows/report.md +7 -1
  305. package/skills/torusguard/payload/workflows/status.md +6 -1
  306. package/skills/torusguard/payload/workflows/verify.md +6 -1
  307. package/skills/torusguard/payload/workflows/web-validate.md +2 -1
  308. package/skills/torusguard-apply/SKILL.md +84 -80
  309. package/skills/torusguard-audit/SKILL.md +87 -83
  310. package/skills/torusguard-authorize/SKILL.md +7 -2
  311. package/skills/torusguard-exploit-check/SKILL.md +7 -2
  312. package/skills/torusguard-full/SKILL.md +7 -2
  313. package/skills/torusguard-harden/SKILL.md +98 -94
  314. package/skills/torusguard-init/SKILL.md +68 -64
  315. package/skills/torusguard-recheck/SKILL.md +81 -77
  316. package/skills/torusguard-report/SKILL.md +93 -72
  317. package/skills/torusguard-status/SKILL.md +62 -58
  318. package/skills/torusguard-verify/SKILL.md +7 -2
  319. package/skills/torusguard-web-validate/SKILL.md +7 -2
@@ -1,653 +1,1513 @@
1
- #!/usr/bin/env python3
2
- """
3
- TorusGuard Autonomous Static Security Audit Engine (v1.3.3)
4
- Multi-language static pattern & AST security scanner.
5
- Evaluates source trees against canonical TorusGuard rule families,
6
- augments confidence via the persistent security memory subsystem,
7
- groups findings into root-cause clusters, and emits isolated run folders.
8
-
9
- Pure Python 3.10+ standard library (zero external dependencies).
10
- """
11
-
12
- import sys
13
- import os
14
- import re
15
- import json
16
- import hashlib
17
- import argparse
18
- import datetime
19
- import unicodedata
20
- from pathlib import Path
21
- from typing import Dict, List, Any, Optional, Tuple, TypedDict, Pattern
22
-
23
- # Windows console UTF-8 support
24
- if sys.stdout and hasattr(sys.stdout, "reconfigure"):
25
- try:
26
- getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
27
- except Exception:
28
- pass
29
- if sys.stderr and hasattr(sys.stderr, "reconfigure"):
30
- try:
31
- getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
32
- except Exception:
33
- pass
34
-
35
- # ─── Timezone Configuration: Indian Standard Time (IST, UTC+05:30) ───────────
36
- IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
37
-
38
- def get_ist_now() -> datetime.datetime:
39
- """Return timezone-aware datetime strictly in Indian Standard Time (IST)."""
40
- return datetime.datetime.now(IST)
41
-
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)
130
-
131
- # ─── Rule Definitions & Sinks ─────────────────────────────────────────────────
132
- class RulePattern(TypedDict):
133
- rule_id: str
134
- title: str
135
- severity: str
136
- category: str
137
- cluster: str
138
- patterns: List[Tuple[Pattern[str], str]]
139
-
140
-
141
- RULE_PATTERNS: List[RulePattern] = [
142
- # ── Secrets & Credentials (TG-SEC) ──────────────────────────────────────────
143
- {
144
- "rule_id": "TG-SEC-001",
145
- "title": "Hardcoded Secret or API Key in Tracked Source",
146
- "severity": "Critical",
147
- "category": "secrets",
148
- "cluster": "cluster-credentials-exposure",
149
- "patterns": [
150
- (re.compile(r'\b(?:sk_live|ak_live)_[0-9a-zA-Z]{20,}\b'), "Live payment or cloud API secret key"),
151
- (re.compile(r'\bgh[pousr]_[0-9a-zA-Z]{36}\b'), "GitHub personal access token"),
152
- (re.compile(r'-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----'), "Hardcoded private key block"),
153
- (re.compile(r'\b(?:jwt_secret|jwtSecret|JWT_SECRET)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{16,}["\']'), "Hardcoded JWT signing secret"),
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")
157
- ]
158
- },
159
- {
160
- "rule_id": "TG-SEC-002",
161
- "title": "Public Client Environment Variable Secret Exposure",
162
- "severity": "High",
163
- "category": "secrets",
164
- "cluster": "cluster-credentials-exposure",
165
- "patterns": [
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")
167
- ]
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
- },
179
- # ── Database Safety & Multi-Tenant Query Isolation (TG-DB) ────────────────
180
- {
181
- "rule_id": "TG-DB-004",
182
- "title": "Missing Multi-Tenant Isolation in Scoped Query",
183
- "severity": "High",
184
- "category": "database",
185
- "cluster": "cluster-tenant-isolation",
186
- "patterns": [
187
- (re.compile(r'\.objects\.get\s*\(\s*id\s*=\s*[^,\)]+\)'), "Django ORM .get(id=...) query lacking explicit tenant scope filter"),
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")
190
- ]
191
- },
192
- {
193
- "rule_id": "TG-DB-001",
194
- "title": "Frontend / Client-Side Direct Database Query",
195
- "severity": "Critical",
196
- "category": "database",
197
- "cluster": "cluster-client-leak",
198
- "patterns": [
199
- (re.compile(r'\b(?:createClient|supabase)\.from\([^)]+\)\.delete\(\)'), "Dangerous direct database mutation in client component"),
200
- (re.compile(r'\bimport\s+.*(?:pg|mysql2|prisma|typeorm).*\s+from'), "Server database driver imported in client file")
201
- ]
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
- },
213
- # ── Input Validation & Code / SQL Injection (TG-INPUT) ─────────────────────
214
- {
215
- "rule_id": "TG-INPUT-002",
216
- "title": "Unescaped Raw SQL Query Concatenation",
217
- "severity": "Critical",
218
- "category": "injection",
219
- "cluster": "cluster-injection",
220
- "patterns": [
221
- (re.compile(r'(?:query|execute|raw)\s*\(\s*f["\'].*SELECT.*\{', re.IGNORECASE), "Python f-string interpolation in raw SQL query"),
222
- (re.compile(r'(?:query|execute|raw)\s*\(\s*[`"\'].*SELECT.*\$\{', re.IGNORECASE), "JavaScript template literal interpolation in SQL query"),
223
- (re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query")
224
- ]
225
- },
226
- {
227
- "rule_id": "TG-INPUT-003",
228
- "title": "Unsafe Dynamic Code or HTML Execution",
229
- "severity": "High",
230
- "category": "injection",
231
- "cluster": "cluster-injection",
232
- "patterns": [
233
- (re.compile(r'\beval\s*\('), "Direct eval() execution on untrusted input"),
234
- (re.compile(r'\bexec\s*\('), "Direct exec() execution on untrusted input"),
235
- (re.compile(r'\bdangerouslySetInnerHTML\s*='), "React dangerouslySetInnerHTML rendering raw unescaped HTML"),
236
- (re.compile(r'\.innerHTML\s*='), "Direct DOM innerHTML assignment vulnerable to XSS")
237
- ]
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
- },
250
- {
251
- "rule_id": "TG-INPUT-006",
252
- "title": "Unvalidated User Input in File Path Resolution",
253
- "severity": "High",
254
- "category": "filesystem",
255
- "cluster": "cluster-injection",
256
- "patterns": [
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")
259
- ]
260
- },
261
- # ── Authentication & Session Safety (TG-AUTH) ──────────────────────────────
262
- {
263
- "rule_id": "TG-AUTH-004",
264
- "title": "Insecure Session Cookie or Disabled CSRF Protection",
265
- "severity": "High",
266
- "category": "auth",
267
- "cluster": "cluster-auth-bypass",
268
- "patterns": [
269
- (re.compile(r'\bSESSION_COOKIE_SECURE\s*=\s*False\b'), "Session cookie secure flag disabled in Django settings"),
270
- (re.compile(r'\bsecure\s*:\s*false\b', re.IGNORECASE), "Cookie secure attribute set to false"),
271
- (re.compile(r'\bhttpOnly\s*:\s*false\b', re.IGNORECASE), "Cookie httpOnly attribute set to false (XSS readable)"),
272
- (re.compile(r'@(?:csrf_exempt|allow_anonymous)\b'), "Decorator disabling route-level authentication or CSRF protection"),
273
- (re.compile(r'\[(?:AllowAnonymous)\]'), "Attribute disabling ASP.NET authentication"),
274
- (re.compile(r'\.csrf\(\)\.disable\(\)'), "Spring Security CSRF protection explicitly disabled")
275
- ]
276
- },
277
- # ── Security Bypasses & Transport Invariants (TG-DIFF / TG-PLATFORM) ───────
278
- {
279
- "rule_id": "TG-DIFF-001",
280
- "title": "Disabled TLS / SSL Certificate Validation",
281
- "severity": "Critical",
282
- "category": "transport",
283
- "cluster": "cluster-transport-security",
284
- "patterns": [
285
- (re.compile(r'\bverify\s*=\s*False\b'), "Disabled TLS certificate verification (verify=False)"),
286
- (re.compile(r'\bInsecureSkipVerify\s*:\s*true\b'), "Disabled Go TLS certificate verification (InsecureSkipVerify: true)"),
287
- (re.compile(r'\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*[\'"]?0[\'"]?'), "Disabled Node.js TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0)"),
288
- (re.compile(r'\brejectUnauthorized\s*:\s*false\b'), "Disabled Node.js TLS rejectUnauthorized"),
289
- (re.compile(r'\bCURLOPT_SSL_VERIFYPEER\s*(?:=>|,)\s*(?:false|0)\b', re.IGNORECASE), "Disabled PHP curl SSL verification")
290
- ]
291
- },
292
- {
293
- "rule_id": "TG-PLATFORM-001",
294
- "title": "Wildcard Permissive CORS Configuration with Credentials",
295
- "severity": "High",
296
- "category": "platform",
297
- "cluster": "cluster-auth-bypass",
298
- "patterns": [
299
- (re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled across entire application"),
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")
302
- ]
303
- }
304
- ]
305
-
306
- # File extensions to scan
307
- SOURCE_EXTENSIONS = {
308
- ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
309
- ".go", ".java", ".cs", ".php", ".rb", ".json", ".yaml", ".yml"
310
- }
311
-
312
- # Directories to skip
313
- SKIP_DIRS = {
314
- ".git", "node_modules", ".venv", "venv", "env", "__pycache__",
315
- "dist", "build", ".next", ".nuxt", "coverage", ".torusguard",
316
- ".idea", ".vscode", "target", "bin", "obj"
317
- }
318
-
319
- def is_test_file(path_str: str) -> bool:
320
- """Check if file belongs to tests, fixtures, or test harnesses."""
321
- clean = path_str.replace("\\", "/").lower()
322
- markers = ["/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
323
- "/harness/", "__tests__", "test_", "_test.", ".test.", ".spec."]
324
- return any(m in clean for m in markers)
325
-
326
-
327
- def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[Path]:
328
- """Recursively collect source files eligible for static security audit."""
329
- files = []
330
- for root, dirs, filenames in os.walk(target_root):
331
- dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
332
- for f in filenames:
333
- ext = os.path.splitext(f)[1].lower()
334
- if ext in SOURCE_EXTENSIONS:
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)
341
- return sorted(files)
342
-
343
-
344
- def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
345
- """Scan a single source file against the TorusGuard rule families."""
346
- findings = []
347
- try:
348
- content = file_path.read_text(encoding="utf-8", errors="replace")
349
- except Exception:
350
- return findings
351
-
352
- lines = content.splitlines()
353
- rel_path = str(file_path.relative_to(target_root)).replace("\\", "/")
354
- is_test = is_test_file(rel_path)
355
-
356
- for rule in RULE_PATTERNS:
357
- rule_id = rule["rule_id"]
358
- patterns: List[Tuple[Pattern[str], str]] = rule["patterns"]
359
- for regex, desc in patterns:
360
- for idx, line in enumerate(lines):
361
- stripped = line.strip()
362
- if stripped.startswith(("#", "//", "/*", "*")):
363
- if "bypass" not in stripped.lower() and "todo" not in stripped.lower():
364
- continue
365
-
366
- match = regex.search(line)
367
- if match:
368
- line_num = idx + 1
369
- seed = f"{rule_id}:{rel_path}:{line_num}:{stripped}"
370
- fp = f"{rule_id}-{hashlib.sha256(seed.encode('utf-8')).hexdigest()[:8]}"
371
-
372
- start_ctx = max(0, idx - 2)
373
- end_ctx = min(len(lines), idx + 3)
374
- snippet = "\n".join(lines[start_ctx:end_ctx])
375
-
376
- findings.append({
377
- "finding_id": fp,
378
- "rule_id": rule_id,
379
- "title": rule["title"],
380
- "description": desc,
381
- "severity": "Low" if is_test else rule["severity"],
382
- "category": rule["category"],
383
- "cluster": rule["cluster"],
384
- "file_path": rel_path,
385
- "line_number": line_num,
386
- "matched_text": match.group(0)[:80],
387
- "snippet": snippet,
388
- "is_test": is_test,
389
- "target": {
390
- "file_path": rel_path,
391
- "start_line": line_num,
392
- "end_line": line_num
393
- }
394
- })
395
- break
396
- return findings
397
-
398
-
399
- def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
400
- """
401
- Score each finding using finding_scorer.py and persistent memory patterns.
402
- Returns (scored_findings, clusters_map).
403
- """
404
- try:
405
- import finding_scorer
406
- except Exception:
407
- finding_scorer = None
408
-
409
- scored = []
410
- clusters: Dict[str, List[Dict[str, Any]]] = {}
411
-
412
- for f in findings:
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
441
- f["confidence_band"] = band
442
- f["confidence_factors"] = factors
443
- scored.append(f)
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)
453
- return scored, clusters
454
-
455
-
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:
457
- """Generate findings.json, findings.md, and summary.md into run folder."""
458
- now_ist = get_ist_now()
459
-
460
- # 1. findings.json
461
- (run_folder / "findings.json").write_text(json.dumps(scored_findings, indent=2), encoding="utf-8")
462
-
463
- # 2. findings.md
464
- md_lines = [
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",
470
- "---",
471
- "## Summary of Findings\n"
472
- ]
473
-
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']}")
481
- md_lines.append("```")
482
- md_lines.append(f['snippet'])
483
- md_lines.append("```\n")
484
-
485
- (run_folder / "findings.md").write_text("\n".join(md_lines), encoding="utf-8")
486
-
487
- # 3. summary.md
488
- summary_lines = [
489
- f"# TorusGuard Audit Summary: {run_folder.name}",
490
- f"- Scanned Scope: {target_root}",
491
- f"- Total Findings: {len(scored_findings)}",
492
- f"- Root-Cause Clusters: {len(clusters)}",
493
- f"- Generated: {now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}"
494
- ]
495
- (run_folder / "summary.md").write_text("\n".join(summary_lines), encoding="utf-8")
496
-
497
- # 4. Update manifest.json status
498
- manifest_file = run_folder / "manifest.json"
499
- if manifest_file.exists():
500
- try:
501
- with open(manifest_file, "r", encoding="utf-8") as mf:
502
- mdata = json.load(mf)
503
- mdata["status"] = "completed"
504
- mdata["findings_count"] = len(scored_findings)
505
- with open(manifest_file, "w", encoding="utf-8") as mf:
506
- json.dump(mdata, mf, indent=2)
507
- except Exception:
508
- pass
509
-
510
-
511
- 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:
512
- """Print the unified 75-column TorusGuard Static Audit results card."""
513
- crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
514
- high_count = sum(1 for f in scored_findings if f["severity"] == "High")
515
- med_count = sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low"))
516
-
517
- stack_str = "Auto-detected"
518
- if detected_stack and detected_stack.get("framework") and detected_stack.get("framework") != "None":
519
- stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
520
-
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}"))
528
- print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 11 canonical families{RESET}"))
529
- print(card_border_bottom())
530
- print()
531
-
532
- # Findings Box
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}")
534
- print(card_border_top("Findings & Confidence Scores"))
535
- print(format_box_line(f"Status: {status_icon}"))
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)"))
537
- print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
538
- print(card_divider())
539
-
540
- if not scored_findings:
541
- print(format_box_line(f"{GREEN}✔ Zero security invariant violations detected.{RESET}"))
542
- else:
543
- for idx, f in enumerate(scored_findings[:5]):
544
- sev_color = RED if f["severity"] == "Critical" else (YELLOW if f["severity"] == "High" else CYAN)
545
- line_str = f"[{sev_color}{f['severity']}{RESET}] {BOLD}{f['rule_id']}{RESET} at {f['file_path']}:{f['line_number']}"
546
- print(format_box_line(line_str))
547
- desc_str = f" └─ {DIM}{f['description'][:58]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
548
- print(format_box_line(desc_str))
549
- if len(scored_findings) > 5:
550
- print(format_box_line(f"{DIM}... and {len(scored_findings) - 5} more findings in report file{RESET}"))
551
-
552
- print(card_border_bottom())
553
- print()
554
-
555
- # Run Artifacts & Next Steps Box
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))
560
- print(format_box_line(f"{BOLD}Visual Dash:{RESET} {CYAN}npx torusguard report --html{RESET}", border="║", border_color=GREEN))
561
- print(format_box_line(f"{BOLD}Surgical Fix:{RESET} In AI Chat, run {CYAN}/torusguard-harden{RESET}", border="║", border_color=GREEN))
562
- print(card_border_bottom(border_color=GREEN, double=True))
563
- print()
564
-
565
-
566
- def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
567
- """Execute the full TorusGuard static security audit."""
568
- target_root = target_root.resolve()
569
- s_dir = Path(__file__).resolve().parent
570
-
571
- # 1. Stack detection
572
- detected_stack = None
573
- stack_script = s_dir / "stack_detect.py"
574
- if stack_script.is_file():
575
- try:
576
- import importlib.util
577
- spec = importlib.util.spec_from_file_location("stack_detect", str(stack_script))
578
- if spec and spec.loader:
579
- sd = importlib.util.module_from_spec(spec)
580
- spec.loader.exec_module(sd)
581
- if hasattr(sd, "detect_stack"):
582
- detected_stack = sd.detect_stack(target_root)
583
- except Exception:
584
- pass
585
-
586
- # 2. Collect files & scan
587
- files = find_files_to_scan(target_root, include_tests=include_tests)
588
- all_findings = []
589
- for f in files:
590
- all_findings.extend(scan_file(f, target_root))
591
-
592
- # 3. Score & Cluster
593
- scored_findings, clusters = score_and_cluster_findings(all_findings, target_root)
594
-
595
- # 4. Allocate run folder
596
- runs_dir = target_root / ".torusguard" / "runs"
597
- runs_dir.mkdir(parents=True, exist_ok=True)
598
-
599
- if str(s_dir) not in sys.path:
600
- sys.path.insert(0, str(s_dir))
601
- try:
602
- import run_manager
603
- run_folder = run_manager.create_run(runs_dir, "audit", target_root.name)
604
- except Exception:
605
- timestamp = get_ist_now().strftime("%Y%m%d-%H%M%S")
606
- run_folder = runs_dir / f"run-{timestamp}-audit"
607
- run_folder.mkdir(parents=True, exist_ok=True)
608
-
609
- # 5. Emit artifacts & sync memory
610
- emit_run_artifacts(run_folder, scored_findings, clusters, target_root)
611
- try:
612
- if "run_manager" in sys.modules:
613
- sys.modules["run_manager"].sync_run_to_memory(run_folder, scored_findings)
614
- except Exception:
615
- pass
616
-
617
- result = {
618
- "status": "completed",
619
- "target_root": str(target_root),
620
- "files_scanned": len(files),
621
- "total_findings": len(scored_findings),
622
- "critical_count": sum(1 for f in scored_findings if f["severity"] == "Critical"),
623
- "high_count": sum(1 for f in scored_findings if f["severity"] == "High"),
624
- "medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
625
- "clusters_count": len(clusters),
626
- "run_folder": str(run_folder),
627
- "findings": scored_findings
628
- }
629
-
630
- if json_output:
631
- print(json.dumps(result, indent=2))
632
- else:
633
- print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
634
-
635
- return result
636
-
637
-
638
- def main():
639
- parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
640
- parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
641
- parser.add_argument("--scope", "-s", help="Alternative path to target project")
642
- parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
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")
645
- args = parser.parse_args()
646
-
647
- target = Path(args.scope or args.path).resolve()
648
- res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
649
- sys.exit(0 if res["critical_count"] == 0 else 1)
650
-
651
-
652
- if __name__ == "__main__":
653
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Autonomous Static Security Audit Engine (v1.3.6)
4
+ Multi-language static pattern & AST security scanner.
5
+ Evaluates source trees against canonical TorusGuard rule families,
6
+ augments confidence via the persistent security memory subsystem,
7
+ groups findings into root-cause clusters, and emits isolated run folders.
8
+
9
+ Pure Python 3.10+ standard library (zero external dependencies).
10
+ """
11
+
12
+ import sys
13
+ import os
14
+ import re
15
+ import json
16
+ import hashlib
17
+ import argparse
18
+ import datetime
19
+ import unicodedata
20
+ from pathlib import Path
21
+ from typing import Dict, List, Any, Optional, Tuple, TypedDict, Pattern
22
+
23
+ # Windows console UTF-8 support
24
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
25
+ try:
26
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
27
+ except Exception:
28
+ pass
29
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
30
+ try:
31
+ getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
32
+ except Exception:
33
+ pass
34
+
35
+ # ─── Timezone Configuration: Indian Standard Time (IST, UTC+05:30) ───────────
36
+ IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
37
+
38
+ def get_ist_now() -> datetime.datetime:
39
+ """Return timezone-aware datetime strictly in Indian Standard Time (IST)."""
40
+ return datetime.datetime.now(IST)
41
+
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.6", 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)
130
+
131
+ # ─── Rule Definitions & Sinks ─────────────────────────────────────────────────
132
+ class RulePattern(TypedDict):
133
+ rule_id: str
134
+ title: str
135
+ severity: str
136
+ category: str
137
+ cluster: str
138
+ patterns: List[Tuple[Pattern[str], str]]
139
+
140
+
141
+ RULE_PATTERNS: List[RulePattern] = [
142
+ # ── TG-SEC ───────────────────────────────────────────────────────────────
143
+ {
144
+ "rule_id": "TG-SEC-001",
145
+ "title": "Hardcoded Secret or API Key in Tracked Source",
146
+ "severity": "Critical",
147
+ "category": "secrets",
148
+ "cluster": "cluster-credentials-exposure",
149
+ "patterns": [
150
+ (re.compile(r'\b(?:sk_live|ak_live)_[0-9a-zA-Z]{20,}\b'), "Live payment or cloud API secret key"),
151
+ (re.compile(r'\bgh[pousr]_[0-9a-zA-Z]{36}\b'), "GitHub personal access token"),
152
+ (re.compile(r'-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----'), "Hardcoded private key block"),
153
+ (re.compile(r'\b(?:jwt_secret|jwtSecret|JWT_SECRET)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{16,}["\']'), "Hardcoded JWT signing secret"),
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")
157
+ ]
158
+ },
159
+ {
160
+ "rule_id": "TG-SEC-002",
161
+ "title": "Public Client Environment Variable Secret Exposure",
162
+ "severity": "Critical",
163
+ "category": "secrets",
164
+ "cluster": "cluster-credentials-exposure",
165
+ "patterns": [
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")
167
+ ]
168
+ },
169
+ {
170
+ "rule_id": "TG-SEC-003",
171
+ "title": "Tracked Environment File",
172
+ "severity": "High",
173
+ "category": "secrets",
174
+ "cluster": "cluster-credentials-exposure",
175
+ "patterns": [
176
+ (re.compile(r'(?:DATABASE_URL|POSTGRES_PASSWORD|MYSQL_PWD|AWS_SECRET_ACCESS_KEY)\s*=\s*["\']?[^\s"\'#]+["\']?'), "Sensitive production credentials stored in tracked environment configuration")
177
+ ]
178
+ },
179
+ {
180
+ "rule_id": "TG-SEC-004",
181
+ "title": "Sensitive Information in Logs",
182
+ "severity": "Medium",
183
+ "category": "secrets",
184
+ "cluster": "cluster-credentials-exposure",
185
+ "patterns": [
186
+ (re.compile(r'(?:console\.log|logger\.(?:info|debug|error|warn)|logging\.(?:info|debug|error))\s*\([^)]*(?:password|token|secret|apiKey|api_key)[^)]*\)', re.IGNORECASE), "Plaintext credential or token logged to output sink")
187
+ ]
188
+ },
189
+ {
190
+ "rule_id": "TG-SEC-005",
191
+ "title": "Secret in Git History or Query URL",
192
+ "severity": "Critical",
193
+ "category": "secrets",
194
+ "cluster": "cluster-credentials-exposure",
195
+ "patterns": [
196
+ (re.compile(r'[?&](?:token|access_token|api_key|apiKey|secret|password)=[a-zA-Z0-9_\-\.]{8,}', re.IGNORECASE), "Sensitive credential passed via cleartext URL query parameter")
197
+ ]
198
+ },
199
+ {
200
+ "rule_id": "TG-SEC-006",
201
+ "title": "Secret in Build Artifact or Container Layer",
202
+ "severity": "Critical",
203
+ "category": "secrets",
204
+ "cluster": "cluster-credentials-exposure",
205
+ "patterns": [
206
+ (re.compile(r'(?:ENV|ARG)\s+(?:[A-Z0-9_]*SECRET[A-Z0-9_]*|[A-Z0-9_]*KEY[A-Z0-9_]*|PASSWORD)\s*=\s*["\']?[a-zA-Z0-9_\-]{8,}["\']?', re.IGNORECASE), "Hardcoded build secret baked into Docker or container image layer")
207
+ ]
208
+ },
209
+ {
210
+ "rule_id": "TG-SEC-007",
211
+ "title": "Secret in Test or Example Fixture Without Mock Marker",
212
+ "severity": "Critical",
213
+ "category": "secrets",
214
+ "cluster": "cluster-credentials-exposure",
215
+ "patterns": [
216
+ (re.compile(r'(?:AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16})'), "Live AWS access key ID in source or fixture code")
217
+ ]
218
+ },
219
+
220
+ # ── TG-AUTH ──────────────────────────────────────────────────────────────
221
+ {
222
+ "rule_id": "TG-AUTH-001",
223
+ "title": "Weak Password Storage or Deprecated Hashing",
224
+ "severity": "Critical",
225
+ "category": "auth",
226
+ "cluster": "cluster-auth-bypass",
227
+ "patterns": [
228
+ (re.compile(r'\b(?:hashlib\.md5|hashlib\.sha1|createHash\(["\']md5["\']\)|createHash\(["\']sha1["\']\))\s*\([^)]*password', re.IGNORECASE), "Cryptographically broken hash function (MD5/SHA1) used for password processing")
229
+ ]
230
+ },
231
+ {
232
+ "rule_id": "TG-AUTH-002",
233
+ "title": "Client-Only Authorization Enforcement",
234
+ "severity": "High",
235
+ "category": "auth",
236
+ "cluster": "cluster-auth-bypass",
237
+ "patterns": [
238
+ (re.compile(r'if\s*\(\s*(?:!user\.isAdmin|user\.role\s*!==\s*["\']admin["\'])\s*\)\s*(?:return|history\.push|navigate)', re.IGNORECASE), "Client-only role authorization without guaranteed backend enforcement")
239
+ ]
240
+ },
241
+ {
242
+ "rule_id": "TG-AUTH-003",
243
+ "title": "Missing Object-Level Authorization (IDOR)",
244
+ "severity": "High",
245
+ "category": "auth",
246
+ "cluster": "cluster-auth-bypass",
247
+ "patterns": [
248
+ (re.compile(r'jwt\.verify\s*\(\s*[a-zA-Z0-9_$.\s,]+,\s*[a-zA-Z0-9_$.\s,]+\s*\)(?!\s*,\s*\{[^}]*algorithms)', re.IGNORECASE), "JWT verification missing explicit algorithms restriction parameter")
249
+ ]
250
+ },
251
+ {
252
+ "rule_id": "TG-AUTH-004",
253
+ "title": "Insecure Session Cookie Configuration",
254
+ "severity": "High",
255
+ "category": "auth",
256
+ "cluster": "cluster-auth-bypass",
257
+ "patterns": [
258
+ (re.compile(r'\bSESSION_COOKIE_SECURE\s*=\s*False\b'), "Session cookie secure flag disabled in Django settings"),
259
+ (re.compile(r'\bsecure\s*:\s*false\b', re.IGNORECASE), "Cookie secure attribute set to false"),
260
+ (re.compile(r'\bhttpOnly\s*:\s*false\b', re.IGNORECASE), "Cookie httpOnly attribute set to false (XSS readable)"),
261
+ (re.compile(r'@(?:csrf_exempt|allow_anonymous)\b'), "Decorator disabling route-level authentication or CSRF protection"),
262
+ (re.compile(r'\[(?:AllowAnonymous)\]'), "Attribute disabling ASP.NET authentication"),
263
+ (re.compile(r'\.csrf\(\)\.disable\(\)'), "Spring Security CSRF protection explicitly disabled")
264
+ ]
265
+ },
266
+ {
267
+ "rule_id": "TG-AUTH-005",
268
+ "title": "Unsafe Password Reset Token Generation",
269
+ "severity": "High",
270
+ "category": "auth",
271
+ "cluster": "cluster-auth-bypass",
272
+ "patterns": [
273
+ (re.compile(r'(?:token|resetCode|resetToken)\s*=\s*(?:Math\.random\(\)|random\.randint\(1000|random\.random\(\))', re.IGNORECASE), "Weak pseudorandom generator used for password reset token or OTP")
274
+ ]
275
+ },
276
+ {
277
+ "rule_id": "TG-AUTH-006",
278
+ "title": "Mass Assignment & Privilege Escalation via Payloads",
279
+ "severity": "Critical",
280
+ "category": "auth",
281
+ "cluster": "cluster-auth-bypass",
282
+ "patterns": [
283
+ (re.compile(r'(?:User\.create|User\.update|user\.update)\s*\(\s*(?:req\.body|request\.data)\s*\)'), "Direct mass assignment of unverified request body onto user model")
284
+ ]
285
+ },
286
+ {
287
+ "rule_id": "TG-AUTH-007",
288
+ "title": "Missing Object-Level and Property-Level Authorization (IDOR)",
289
+ "severity": "Critical",
290
+ "category": "auth",
291
+ "cluster": "cluster-auth-bypass",
292
+ "patterns": [
293
+ (re.compile(r'(?:db\.[a-zA-Z]+\.find(?:One)?|Model\.find(?:One)?)\s*\(\s*\{\s*_?id\s*:\s*(?:req\.params|req\.query)\.id\s*\}\s*\)'), "Direct database lookup by user-controlled ID without tenant or ownership check")
294
+ ]
295
+ },
296
+ {
297
+ "rule_id": "TG-AUTH-008",
298
+ "title": "Untrusted Role or Tenant Header Injection",
299
+ "severity": "Critical",
300
+ "category": "auth",
301
+ "cluster": "cluster-auth-bypass",
302
+ "patterns": [
303
+ (re.compile(r'(?:req\.headers|request\.headers)\s*\[?\s*["\']x-(?:user-role|role|is-admin|tenant-id)["\']\s*\]?', re.IGNORECASE), "Directly trusting unverified client HTTP header for role or tenant assignment")
304
+ ]
305
+ },
306
+
307
+ # ── TG-DB ────────────────────────────────────────────────────────────────
308
+ {
309
+ "rule_id": "TG-DB-001",
310
+ "title": "Frontend Database Query Logic",
311
+ "severity": "High",
312
+ "category": "database",
313
+ "cluster": "cluster-client-leak",
314
+ "patterns": [
315
+ (re.compile(r'\b(?:createClient|supabase)\.from\([^)]+\)\.delete\(\)'), "Dangerous direct database mutation in client component"),
316
+ (re.compile(r'\bimport\s+.*(?:pg|mysql2|prisma|typeorm).*\s+from'), "Server database driver imported in client file")
317
+ ]
318
+ },
319
+ {
320
+ "rule_id": "TG-DB-002",
321
+ "title": "Privileged Database Credential in Browser Context",
322
+ "severity": "Critical",
323
+ "category": "database",
324
+ "cluster": "cluster-client-leak",
325
+ "patterns": [
326
+ (re.compile(r'\b(?:SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL)\b'), "Service role key or raw database connection string reference in frontend code")
327
+ ]
328
+ },
329
+ {
330
+ "rule_id": "TG-DB-003",
331
+ "title": "Frontend Use of Admin or Server-Only Database SDKs",
332
+ "severity": "Critical",
333
+ "category": "database",
334
+ "cluster": "cluster-client-leak",
335
+ "patterns": [
336
+ (re.compile(r'import\s+.*(?:firebase-admin|@prisma/client|mongoose).*from\s+["\']', re.IGNORECASE), "Server-only database ORM/SDK imported in client code")
337
+ ]
338
+ },
339
+ {
340
+ "rule_id": "TG-DB-004",
341
+ "title": "Missing Tenant Query Isolation in Multi-Tenant Models",
342
+ "severity": "Critical",
343
+ "category": "database",
344
+ "cluster": "cluster-tenant-isolation",
345
+ "patterns": [
346
+ (re.compile(r'\.objects\.get\s*\(\s*id\s*=\s*[^,\)]+\)'), "Django ORM .get(id=...) query lacking explicit tenant scope filter"),
347
+ (re.compile(r'\.findById\s*\(\s*(?:req\.params|id)[^\)]*\)'), "Direct findById query lacking organization/tenant boundaries"),
348
+ (re.compile(r'prisma\.[a-zA-Z]+\.findUnique\s*\(\s*\{\s*where:\s*\{\s*id\b'), "Prisma findUnique query lacking tenantId boundary filter")
349
+ ]
350
+ },
351
+
352
+ # ── TG-INPUT ─────────────────────────────────────────────────────────────
353
+ {
354
+ "rule_id": "TG-INPUT-001",
355
+ "title": "Missing Server-Side Request Validation",
356
+ "severity": "High",
357
+ "category": "injection",
358
+ "cluster": "cluster-injection",
359
+ "patterns": [
360
+ (re.compile(r'(?:const|let|var)\s+\{[^}]+\}\s*=\s*req\.body\s*;?\s*(?!\s*(?:const|let|var|[a-zA-Z0-9_]+\.parse|\.validate))'), "Destructuring unvalidated req.body without schema validation")
361
+ ]
362
+ },
363
+ {
364
+ "rule_id": "TG-INPUT-002",
365
+ "title": "Raw SQL Concatenation",
366
+ "severity": "Critical",
367
+ "category": "injection",
368
+ "cluster": "cluster-injection",
369
+ "patterns": [
370
+ (re.compile(r'(?:query|execute|raw)\s*\(\s*f["\'].*SELECT.*\{', re.IGNORECASE), "Python f-string interpolation in raw SQL query"),
371
+ (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
+ ]
374
+ },
375
+ {
376
+ "rule_id": "TG-INPUT-003",
377
+ "title": "Unsafe Dynamic Code or HTML Execution",
378
+ "severity": "High",
379
+ "category": "injection",
380
+ "cluster": "cluster-injection",
381
+ "patterns": [
382
+ (re.compile(r'\beval\s*\('), "Direct eval() execution on untrusted input"),
383
+ (re.compile(r'\bexec\s*\('), "Direct exec() execution on untrusted input"),
384
+ (re.compile(r'\bdangerouslySetInnerHTML\s*='), "React dangerouslySetInnerHTML rendering raw unescaped HTML"),
385
+ (re.compile(r'\.innerHTML\s*='), "Direct DOM innerHTML assignment vulnerable to XSS")
386
+ ]
387
+ },
388
+ {
389
+ "rule_id": "TG-INPUT-004",
390
+ "title": "Command Injection via Shell Execution",
391
+ "severity": "High",
392
+ "category": "injection",
393
+ "cluster": "cluster-injection",
394
+ "patterns": [
395
+ (re.compile(r'(?:child_process\.exec|execSync|subprocess\.Popen|os\.system)\s*\([^)]*\+\s*(?:req|params|cmd|input)'), "Untrusted user parameter concatenated into shell execution command")
396
+ ]
397
+ },
398
+ {
399
+ "rule_id": "TG-INPUT-005",
400
+ "title": "Unsafe Template Rendering & Disabled Autoescaping",
401
+ "severity": "High",
402
+ "category": "injection",
403
+ "cluster": "cluster-injection",
404
+ "patterns": [
405
+ (re.compile(r'render_template_string\s*\('), "Flask render_template_string rendering untrusted input"),
406
+ (re.compile(r'autoescape\s*=\s*False\b'), "Template engine autoescape explicitly disabled")
407
+ ]
408
+ },
409
+ {
410
+ "rule_id": "TG-INPUT-006",
411
+ "title": "Path Traversal and Unsafe Upload Storage",
412
+ "severity": "Critical",
413
+ "category": "filesystem",
414
+ "cluster": "cluster-injection",
415
+ "patterns": [
416
+ (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")
418
+ ]
419
+ },
420
+
421
+ # ── TG-RATE ──────────────────────────────────────────────────────────────
422
+ {
423
+ "rule_id": "TG-RATE-001",
424
+ "title": "Unlimited Authentication Endpoint",
425
+ "severity": "High",
426
+ "category": "rate",
427
+ "cluster": "cluster-rate-limiting",
428
+ "patterns": [
429
+ (re.compile(r'(?:app|router)\.post\s*\(\s*["\']/(?:api/)?(?:auth/)?(?:login|signin|register|reset-password)["\'],\s*(?:async\s*)?\('), "Authentication POST endpoint defined without rate-limiting middleware")
430
+ ]
431
+ },
432
+ {
433
+ "rule_id": "TG-RATE-002",
434
+ "title": "Unlimited Public Write Endpoint",
435
+ "severity": "Medium",
436
+ "category": "rate",
437
+ "cluster": "cluster-rate-limiting",
438
+ "patterns": [
439
+ (re.compile(r'(?:app|router)\.post\s*\(\s*["\']/(?:api/)?(?:contact|feedback|comments|messages)["\'],\s*(?:async\s*)?\('), "Public submission endpoint defined without rate-limiting or captcha defense")
440
+ ]
441
+ },
442
+ {
443
+ "rule_id": "TG-RATE-003",
444
+ "title": "Unbounded Resource Consumption",
445
+ "severity": "High",
446
+ "category": "rate",
447
+ "cluster": "cluster-rate-limiting",
448
+ "patterns": [
449
+ (re.compile(r'(?:findMany|find)\s*\(\s*\{\s*\}\s*\)'), "Database query returning all records without pagination take/limit"),
450
+ (re.compile(r'SELECT\s+\*\s+FROM\s+[a-zA-Z0-9_]+\s*;', re.IGNORECASE), "Raw unbounded SELECT * query lacking LIMIT clause")
451
+ ]
452
+ },
453
+
454
+ # ── TG-AGENT ─────────────────────────────────────────────────────────────
455
+ {
456
+ "rule_id": "TG-AGENT-001",
457
+ "title": "Prompt Injection in System Context Files",
458
+ "severity": "Critical",
459
+ "category": "agent",
460
+ "cluster": "cluster-agent-security",
461
+ "patterns": [
462
+ (re.compile(r'\{[^{}]*role\s*:\s*["\']system["\'][^{}]*content\s*:[^{}]*\$\{'), "User-controlled template string concatenated into LLM system prompt"),
463
+ (re.compile(r'\[\s*\{\s*["\']role["\']\s*:\s*["\']system["\']\s*,\s*["\']content["\']\s*:\s*f["\']'), "Python f-string interpolation inside LLM system prompt message")
464
+ ]
465
+ },
466
+ {
467
+ "rule_id": "TG-AGENT-002",
468
+ "title": "Unsafe Tool Dispatch & Shell Execution without Sandboxing",
469
+ "severity": "Critical",
470
+ "category": "agent",
471
+ "cluster": "cluster-agent-security",
472
+ "patterns": [
473
+ (re.compile(r'(?:exec|spawnSync|run_command|os\.system)\s*\([^)]*tool_call\.(?:arguments|args)'), "AI agent tool call executing unsandboxed shell command")
474
+ ]
475
+ },
476
+ {
477
+ "rule_id": "TG-AGENT-003",
478
+ "title": "Overly Broad MCP Tool Scoping & Credential Access",
479
+ "severity": "High",
480
+ "category": "agent",
481
+ "cluster": "cluster-agent-security",
482
+ "patterns": [
483
+ (re.compile(r'env\s*:\s*process\.env\b'), "Exposing entire host process.env to sub-agent or MCP tool environment")
484
+ ]
485
+ },
486
+ {
487
+ "rule_id": "TG-AGENT-004",
488
+ "title": "Persistent Memory & Cross-Session Information Leakage",
489
+ "severity": "High",
490
+ "category": "agent",
491
+ "cluster": "cluster-agent-security",
492
+ "patterns": [
493
+ (re.compile(r'globalMemory\s*\[\s*["\'][a-zA-Z0-9_]+["\']\s*\]\s*='), "Global shared memory variable persisting cross-session user conversations")
494
+ ]
495
+ },
496
+
497
+ # ── TG-SSRF ──────────────────────────────────────────────────────────────
498
+ {
499
+ "rule_id": "TG-SSRF-001",
500
+ "title": "User-Controlled Server-Side URL Fetch",
501
+ "severity": "Critical",
502
+ "category": "ssrf",
503
+ "cluster": "cluster-ssrf",
504
+ "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")
506
+ ]
507
+ },
508
+ {
509
+ "rule_id": "TG-SSRF-002",
510
+ "title": "Missing Internal Network Protection",
511
+ "severity": "Critical",
512
+ "category": "ssrf",
513
+ "cluster": "cluster-ssrf",
514
+ "patterns": [
515
+ (re.compile(r'(?:fetch|axios(?:\.get|\.post)?|requests\.(?:get|post)|urllib\.request\.urlopen|http\.get)\s*\(\s*["\']https?://(?:localhost|127\.0\.0\.1|169\.254\.169\.254|metadata\.google)'), "Server-side HTTP request targeting internal loopback or cloud metadata endpoint"),
516
+ (re.compile(r'(?:target_url|dest_url|request_url|backend_url|url)\s*=\s*["\']https?://(?:169\.254\.169\.254|metadata\.google)'), "Target URL assigned to internal cloud metadata endpoint")
517
+ ]
518
+ },
519
+ {
520
+ "rule_id": "TG-SSRF-003",
521
+ "title": "Unsafe Redirect Following",
522
+ "severity": "Critical",
523
+ "category": "ssrf",
524
+ "cluster": "cluster-ssrf",
525
+ "patterns": [
526
+ (re.compile(r'(?:maxRedirects\s*:\s*(?:[5-9]|[1-9][0-9])|allow_redirects\s*=\s*True)'), "HTTP client configured to follow redirects without domain revalidation")
527
+ ]
528
+ },
529
+ {
530
+ "rule_id": "TG-SSRF-004",
531
+ "title": "Unbounded Outbound Request Without Timeout",
532
+ "severity": "Critical",
533
+ "category": "ssrf",
534
+ "cluster": "cluster-ssrf",
535
+ "patterns": [
536
+ (re.compile(r'requests\.(?:get|post|put)\s*\((?![^)]*\btimeout\s*=)[^)]*\)'), "Python requests call without explicit timeout parameter")
537
+ ]
538
+ },
539
+
540
+ # ── TG-WEBHOOK ───────────────────────────────────────────────────────────
541
+ {
542
+ "rule_id": "TG-WEBHOOK-001",
543
+ "title": "Missing Webhook Signature Verification",
544
+ "severity": "Critical",
545
+ "category": "webhook",
546
+ "cluster": "cluster-webhook",
547
+ "patterns": [
548
+ (re.compile(r'(?:app|router)\.post\s*\(\s*["\']/(?:api/)?(?:stripe|github|webhooks?)(?:/[a-zA-Z0-9_]+)?["\'],\s*(?:async\s*)?\(\s*(?:req|request)'), "Webhook endpoint handler defined without cryptographic signature verification middleware")
549
+ ]
550
+ },
551
+ {
552
+ "rule_id": "TG-WEBHOOK-002",
553
+ "title": "Replayable Webhook Without Timestamp Validation",
554
+ "severity": "Critical",
555
+ "category": "webhook",
556
+ "cluster": "cluster-webhook",
557
+ "patterns": [
558
+ (re.compile(r'verifyWebhook\s*\([^)]*\)(?!.*timestamp)'), "Webhook signature check without tolerance or timestamp replay defense")
559
+ ]
560
+ },
561
+ {
562
+ "rule_id": "TG-WEBHOOK-003",
563
+ "title": "Missing Webhook Idempotency",
564
+ "severity": "Critical",
565
+ "category": "webhook",
566
+ "cluster": "cluster-webhook",
567
+ "patterns": [
568
+ (re.compile(r'(?:handleWebhook|processWebhookEvent)\s*\(\s*event\s*\)\s*\{(?!.*(?:hasProcessed|processedEvents|idempotency))'), "Webhook processing function without event ID deduplication record")
569
+ ]
570
+ },
571
+ {
572
+ "rule_id": "TG-WEBHOOK-004",
573
+ "title": "Unvalidated Webhook Event Type",
574
+ "severity": "Critical",
575
+ "category": "webhook",
576
+ "cluster": "cluster-webhook",
577
+ "patterns": [
578
+ (re.compile(r'handlers\s*\[\s*event\.type\s*\]\s*\('), "Dynamic invocation of webhook handler via unverified event.type index")
579
+ ]
580
+ },
581
+
582
+ # ── TG-WS ────────────────────────────────────────────────────────────────
583
+ {
584
+ "rule_id": "TG-WS-001",
585
+ "title": "Unauthenticated WebSocket Handshake",
586
+ "severity": "High",
587
+ "category": "ws",
588
+ "cluster": "cluster-websocket",
589
+ "patterns": [
590
+ (re.compile(r'wss?\.on\s*\(\s*["\']connection["\'],\s*\(\s*ws\s*,\s*req\s*\)\s*=>\s*\{(?!.*(?:token|auth|verify|session))'), "WebSocket server accepting connections without token verification in handshake")
591
+ ]
592
+ },
593
+ {
594
+ "rule_id": "TG-WS-002",
595
+ "title": "Missing Channel-Level Authorization",
596
+ "severity": "Critical",
597
+ "category": "ws",
598
+ "cluster": "cluster-websocket",
599
+ "patterns": [
600
+ (re.compile(r'socket\.join\s*\(\s*data\.channel\s*\)(?!.*(?:canAccess|hasPermission|isMember))'), "Socket joining room or channel without user permission verification")
601
+ ]
602
+ },
603
+ {
604
+ "rule_id": "TG-WS-003",
605
+ "title": "Unvalidated WebSocket Message",
606
+ "severity": "Critical",
607
+ "category": "ws",
608
+ "cluster": "cluster-websocket",
609
+ "patterns": [
610
+ (re.compile(r'ws\.on\s*\(\s*["\']message["\'],\s*(?:async\s*)?\(\s*data\s*\)\s*=>\s*\{\s*const\s+[a-zA-Z0-9_]+\s*=\s*JSON\.parse'), "Parsing WebSocket message payload without schema validation")
611
+ ]
612
+ },
613
+ {
614
+ "rule_id": "TG-WS-004",
615
+ "title": "Unbounded WebSocket Connections",
616
+ "severity": "Critical",
617
+ "category": "ws",
618
+ "cluster": "cluster-websocket",
619
+ "patterns": [
620
+ (re.compile(r'new\s+WebSocketServer\s*\(\s*\{\s*server\s*\}\s*\)'), "WebSocketServer instantiated without maxPayload or client connection bounds")
621
+ ]
622
+ },
623
+
624
+ # ── TG-CSRF ──────────────────────────────────────────────────────────────
625
+ {
626
+ "rule_id": "TG-CSRF-001",
627
+ "title": "Missing CSRF Protection",
628
+ "severity": "Critical",
629
+ "category": "csrf",
630
+ "cluster": "cluster-auth-bypass",
631
+ "patterns": [
632
+ (re.compile(r'<form\s+[^>]*method=["\']POST["\'][^>]*>(?![\s\S]*name=["\'](?:csrf_token|csrfToken|_csrf)["\'])'), "HTML POST form without hidden CSRF token input field")
633
+ ]
634
+ },
635
+ {
636
+ "rule_id": "TG-CSRF-002",
637
+ "title": "Unsafe Credentialed Cross-Origin Request",
638
+ "severity": "Critical",
639
+ "category": "csrf",
640
+ "cluster": "cluster-auth-bypass",
641
+ "patterns": [
642
+ (re.compile(r'res\.setHeader\s*\(\s*["\']Access-Control-Allow-Origin["\']\s*,\s*["\']\*["\']\s*\);\s*res\.setHeader\s*\(\s*["\']Access-Control-Allow-Credentials["\']\s*,\s*["\']true["\']'), "Wildcard CORS origin paired with credentials flag")
643
+ ]
644
+ },
645
+
646
+ # ── TG-GQL ───────────────────────────────────────────────────────────────
647
+ {
648
+ "rule_id": "TG-GQL-001",
649
+ "title": "Missing Query Depth or Complexity Limits",
650
+ "severity": "High",
651
+ "category": "gql",
652
+ "cluster": "cluster-graphql",
653
+ "patterns": [
654
+ (re.compile(r'new\s+ApolloServer\s*\(\s*\{\s*typeDefs\s*,\s*resolvers\s*\}\s*\)'), "ApolloServer initialized without query depth or complexity limits")
655
+ ]
656
+ },
657
+ {
658
+ "rule_id": "TG-GQL-002",
659
+ "title": "Resolver Missing Authorization",
660
+ "severity": "Critical",
661
+ "category": "gql",
662
+ "cluster": "cluster-graphql",
663
+ "patterns": [
664
+ (re.compile(r'Mutation\s*:\s*\{[\s\S]*?(?:delete|update|mutate)[a-zA-Z0-9_]*\s*:\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{(?!.*(?:context\.user|context\.auth))'), "GraphQL mutation resolver executing without user authorization check")
665
+ ]
666
+ },
667
+ {
668
+ "rule_id": "TG-GQL-003",
669
+ "title": "Unbounded GraphQL Pagination or Batching",
670
+ "severity": "Critical",
671
+ "category": "gql",
672
+ "cluster": "cluster-graphql",
673
+ "patterns": [
674
+ (re.compile(r'resolve\s*\(\s*parent\s*,\s*args\s*\)\s*\{\s*return\s+db\.[a-zA-Z]+\.find\(\)'), "GraphQL field resolver returning unpaginated database collection")
675
+ ]
676
+ },
677
+ {
678
+ "rule_id": "TG-GQL-004",
679
+ "title": "Unnecessary Production Introspection",
680
+ "severity": "Critical",
681
+ "category": "gql",
682
+ "cluster": "cluster-graphql",
683
+ "patterns": [
684
+ (re.compile(r'\bintrospection\s*:\s*true\b', re.IGNORECASE), "GraphQL schema introspection enabled in configuration")
685
+ ]
686
+ },
687
+
688
+ # ── TG-SUPPLY ────────────────────────────────────────────────────────────
689
+ {
690
+ "rule_id": "TG-SUPPLY-001",
691
+ "title": "Missing or Ignored Dependency Lockfile",
692
+ "severity": "Critical",
693
+ "category": "supply",
694
+ "cluster": "cluster-supply-chain",
695
+ "patterns": [
696
+ (re.compile(r'^\s*package-lock\.json\s*$', re.MULTILINE), "Lockfile package-lock.json explicitly ignored in .gitignore")
697
+ ]
698
+ },
699
+ {
700
+ "rule_id": "TG-SUPPLY-002",
701
+ "title": "Vulnerable Dependency Review Missing",
702
+ "severity": "Critical",
703
+ "category": "supply",
704
+ "cluster": "cluster-supply-chain",
705
+ "patterns": [
706
+ (re.compile(r'(?:npm|yarn|pnpm)\s+(?:install|ci)\s+.*--(?:no-audit|audit=false)'), "Dependency installation command skipping security audit checks")
707
+ ]
708
+ },
709
+ {
710
+ "rule_id": "TG-SUPPLY-003",
711
+ "title": "Unsafe CI/CD Secret Exposure",
712
+ "severity": "Critical",
713
+ "category": "supply",
714
+ "cluster": "cluster-supply-chain",
715
+ "patterns": [
716
+ (re.compile(r'run\s*:\s*echo\s+["\']?\$\{\{\s*secrets\.[A-Z0-9_]+\s*\}\}'), "CI workflow step echoing secret into pipeline logs")
717
+ ]
718
+ },
719
+ {
720
+ "rule_id": "TG-SUPPLY-004",
721
+ "title": "Unpinned CI Action",
722
+ "severity": "Critical",
723
+ "category": "supply",
724
+ "cluster": "cluster-supply-chain",
725
+ "patterns": [
726
+ (re.compile(r'uses\s*:\s*actions\/[a-zA-Z0-9_\-]+@(?!main)[a-zA-Z0-9_\.]+(?<![a-f0-9]{40})$'), "GitHub Action referenced by mutable tag instead of immutable commit SHA")
727
+ ]
728
+ },
729
+ {
730
+ "rule_id": "TG-SUPPLY-005",
731
+ "title": "Unsafe Dependency Install Script",
732
+ "severity": "Critical",
733
+ "category": "supply",
734
+ "cluster": "cluster-supply-chain",
735
+ "patterns": [
736
+ (re.compile(r'curl\s+-[a-zA-Z]*s[a-zA-Z]*\s+https?:\/\/[^\s|]+\s*\|\s*(?:bash|sh)'), "Remote installation script piped directly to bash without checksum verification")
737
+ ]
738
+ },
739
+ {
740
+ "rule_id": "TG-SUPPLY-006",
741
+ "title": "Container Build Secret Persistence",
742
+ "severity": "High",
743
+ "category": "supply",
744
+ "cluster": "cluster-supply-chain",
745
+ "patterns": [
746
+ (re.compile(r'COPY\s+\.env\s+'), "Production Dockerfile copying local .env into container filesystem")
747
+ ]
748
+ },
749
+
750
+ # ── TG-BIZ ───────────────────────────────────────────────────────────────
751
+ {
752
+ "rule_id": "TG-BIZ-001",
753
+ "title": "Client-Controlled Sensitive Business Value",
754
+ "severity": "Critical",
755
+ "category": "biz",
756
+ "cluster": "cluster-biz-logic",
757
+ "patterns": [
758
+ (re.compile(r'(?:amount|price|discount|total)\s*:\s*(?:req\.body|request\.data)\.(?:amount|price|discount)'), "Sensitive pricing or discount amount accepted directly from client payload")
759
+ ]
760
+ },
761
+ {
762
+ "rule_id": "TG-BIZ-002",
763
+ "title": "Replayable One-Time Operation",
764
+ "severity": "Critical",
765
+ "category": "biz",
766
+ "cluster": "cluster-biz-logic",
767
+ "patterns": [
768
+ (re.compile(r'(?:chargeCard|processPayment|transferFunds)\s*\([^)]*\)(?!.*idempotency)'), "Financial transaction execution without idempotency key parameter")
769
+ ]
770
+ },
771
+ {
772
+ "rule_id": "TG-BIZ-003",
773
+ "title": "Missing Workflow-State Validation",
774
+ "severity": "Critical",
775
+ "category": "biz",
776
+ "cluster": "cluster-biz-logic",
777
+ "patterns": [
778
+ (re.compile(r'(?:order|invoice)\.status\s*=\s*(?:req\.body|request\.data)\.status'), "Direct client modification of order workflow status")
779
+ ]
780
+ },
781
+ {
782
+ "rule_id": "TG-BIZ-004",
783
+ "title": "Unrestricted Sensitive Business Flow",
784
+ "severity": "Critical",
785
+ "category": "biz",
786
+ "cluster": "cluster-biz-logic",
787
+ "patterns": [
788
+ (re.compile(r'(?:deleteMany|updateMany)\s*\(\s*\{\s*\}\s*\)'), "Bulk destructive mutation executing without filter criteria or threshold guard")
789
+ ]
790
+ },
791
+
792
+ # ── TG-CACHE ─────────────────────────────────────────────────────────────
793
+ {
794
+ "rule_id": "TG-CACHE-001",
795
+ "title": "Sensitive Response Publicly Cacheable",
796
+ "severity": "Critical",
797
+ "category": "cache",
798
+ "cluster": "cluster-cache",
799
+ "patterns": [
800
+ (re.compile(r'Cache-Control["\']?\s*,\s*["\']public,\s*max-age=\d+["\']', re.IGNORECASE), "Public caching header configured on potentially sensitive response")
801
+ ]
802
+ },
803
+ {
804
+ "rule_id": "TG-CACHE-002",
805
+ "title": "Missing User Cache Isolation",
806
+ "severity": "Critical",
807
+ "category": "cache",
808
+ "cluster": "cluster-cache",
809
+ "patterns": [
810
+ (re.compile(r'cache\.set\s*\(\s*["\']user_profile_["\']\s*\+\s*id\s*,\s*data\s*\)(?!.*tenant)'), "Cache key lacking tenant isolation prefix")
811
+ ]
812
+ },
813
+ {
814
+ "rule_id": "TG-CACHE-003",
815
+ "title": "Sensitive Data in URL",
816
+ "severity": "Critical",
817
+ "category": "cache",
818
+ "cluster": "cluster-cache",
819
+ "patterns": [
820
+ (re.compile(r'(?:router|app)\.get\s*\(\s*["\'][^"\']*(?:password|ssn|credit_card)[^"\']*["\']'), "Sensitive PII or password transmitted in GET URL query path")
821
+ ]
822
+ },
823
+
824
+ # ── TG-CLIENT ────────────────────────────────────────────────────────────
825
+ {
826
+ "rule_id": "TG-CLIENT-001",
827
+ "title": "Public Production Source Maps",
828
+ "severity": "Medium",
829
+ "category": "client",
830
+ "cluster": "cluster-client-leak",
831
+ "patterns": [
832
+ (re.compile(r'\bproductionSourceMap\s*:\s*true\b', re.IGNORECASE), "Webpack/Vite productionSourceMap enabled in build configuration"),
833
+ (re.compile(r'\bsourcemap\s*:\s*true\b', re.IGNORECASE), "Build config emitting unminified source maps in production output")
834
+ ]
835
+ },
836
+ {
837
+ "rule_id": "TG-CLIENT-002",
838
+ "title": "Sensitive Client Bundle Content",
839
+ "severity": "High",
840
+ "category": "client",
841
+ "cluster": "cluster-client-leak",
842
+ "patterns": [
843
+ (re.compile(r'["\']use client["\'];[\s\S]*?import\s+.*(?:nodemailer|bcrypt|jsonwebtoken|aws-sdk).*from'), "Server-only private dependency imported into React Client Component")
844
+ ]
845
+ },
846
+
847
+ # ── TG-PLATFORM ──────────────────────────────────────────────────────────
848
+ {
849
+ "rule_id": "TG-PLATFORM-001",
850
+ "title": "Wildcard CORS With Credentials",
851
+ "severity": "High",
852
+ "category": "platform",
853
+ "cluster": "cluster-auth-bypass",
854
+ "patterns": [
855
+ (re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled across entire application"),
856
+ (re.compile(r'@CrossOrigin\s*\(\s*["\']\*["\']\s*\)'), "Wildcard CORS origin annotation in Java/Spring"),
857
+ (re.compile(r'cors\(\s*\{\s*origin\s*:\s*["\']\*["\']'), "Wildcard CORS origin configured in Express middleware")
858
+ ]
859
+ },
860
+ {
861
+ "rule_id": "TG-PLATFORM-002",
862
+ "title": "Missing Security Headers",
863
+ "severity": "Medium",
864
+ "category": "platform",
865
+ "cluster": "cluster-platform",
866
+ "patterns": [
867
+ (re.compile(r'const\s+app\s*=\s*express\(\);(?![\s\S]*app\.use\s*\(\s*helmet)'), "Express application initialized without helmet security headers middleware")
868
+ ]
869
+ },
870
+ {
871
+ "rule_id": "TG-PLATFORM-003",
872
+ "title": "Production Stack Trace Exposure",
873
+ "severity": "Medium",
874
+ "category": "platform",
875
+ "cluster": "cluster-platform",
876
+ "patterns": [
877
+ (re.compile(r'res\.status\(500\)\.(?:send|json)\s*\(\s*(?:err\.stack|error\.stack)\s*\)'), "Uncaught internal error stack trace sent directly to client response"),
878
+ (re.compile(r'\bDEBUG\s*=\s*True\b'), "Django DEBUG mode enabled in configuration")
879
+ ]
880
+ },
881
+ {
882
+ "rule_id": "TG-PLATFORM-004",
883
+ "title": "Missing Request Size Limits",
884
+ "severity": "Medium",
885
+ "category": "platform",
886
+ "cluster": "cluster-platform",
887
+ "patterns": [
888
+ (re.compile(r'express\.(?:json|urlencoded)\s*\(\s*\{\s*limit\s*:\s*["\'](?:[5-9][0-9]|[1-9][0-9]{2,})mb["\']', re.IGNORECASE), "Excessively permissive request body size limit (>50MB) risking denial of service")
889
+ ]
890
+ },
891
+
892
+ # ── TG-DIFF ──────────────────────────────────────────────────────────────
893
+ {
894
+ "rule_id": "TG-DIFF-001",
895
+ "title": "Accidental Security Check Bypass in Patch Additions",
896
+ "severity": "Critical",
897
+ "category": "transport",
898
+ "cluster": "cluster-transport-security",
899
+ "patterns": [
900
+ (re.compile(r'\bverify\s*=\s*False\b'), "Disabled TLS certificate verification (verify=False)"),
901
+ (re.compile(r'\bInsecureSkipVerify\s*:\s*true\b'), "Disabled Go TLS certificate verification (InsecureSkipVerify: true)"),
902
+ (re.compile(r'\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*[\'"]?0[\'"]?'), "Disabled Node.js TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0)"),
903
+ (re.compile(r'\brejectUnauthorized\s*:\s*false\b'), "Disabled Node.js TLS rejectUnauthorized"),
904
+ (re.compile(r'\bCURLOPT_SSL_VERIFYPEER\s*(?:=>|,)\s*(?:false|0)\b', re.IGNORECASE), "Disabled PHP curl SSL verification")
905
+ ]
906
+ },
907
+ {
908
+ "rule_id": "TG-DIFF-002",
909
+ "title": "Secret and Token Ingestion in Patch Additions",
910
+ "severity": "Critical",
911
+ "category": "diff",
912
+ "cluster": "cluster-credentials-exposure",
913
+ "patterns": [
914
+ (re.compile(r'^\+[^+].*(?:eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}|ghp_[a-zA-Z0-9]{36})'), "Hardcoded secret or auth token introduced in patch addition")
915
+ ]
916
+ },
917
+ {
918
+ "rule_id": "TG-DIFF-003",
919
+ "title": "Unintentional Removal of Tenant Filter in Patch Deletions",
920
+ "severity": "High",
921
+ "category": "diff",
922
+ "cluster": "cluster-tenant-isolation",
923
+ "patterns": [
924
+ (re.compile(r'^\-[^-].*(?:tenantId|organization_id|account_id)'), "Removal of tenant isolation boundary in patch deletion")
925
+ ]
926
+ },
927
+
928
+ # ── TG-EDGE ──────────────────────────────────────────────────────────────
929
+ {
930
+ "rule_id": "TG-EDGE-001",
931
+ "title": "Serverless & Edge Global State Memory Leakage",
932
+ "severity": "High",
933
+ "category": "edge",
934
+ "cluster": "cluster-edge",
935
+ "patterns": [
936
+ (re.compile(r'^(?:let|var)\s+(?:userCache|sessionData|requestState)\s*=\s*\{', re.MULTILINE), "Mutable global state variable declared outside serverless handler function")
937
+ ]
938
+ },
939
+ {
940
+ "rule_id": "TG-EDGE-003",
941
+ "title": "AWS Lambda Ephemeral Execution & Cold Start Security",
942
+ "severity": "High",
943
+ "category": "edge",
944
+ "cluster": "cluster-edge",
945
+ "patterns": [
946
+ (re.compile(r'export\s+const\s+handler\s*=\s*async\s*\([^)]*\)\s*=>\s*\{[\s\S]*?secretsmanager\.getSecretValue'), "Fetching cloud secrets inside request handler loop instead of cold-start cache")
947
+ ]
948
+ }
949
+ ]
950
+
951
+ # ─── Rule Precision Map ───────────────────────────────────────────────────────
952
+ # Calibrates how precise each rule's regex is (0.0 = very broad, 1.0 = definitive)
953
+ # Used to adjust confidence scores: broad rules get lower base evidence quality.
954
+ RULE_PRECISION: Dict[str, float] = {
955
+ "TG-SEC-001": 0.90, "TG-SEC-002": 0.85, "TG-SEC-003": 0.70,
956
+ "TG-SEC-004": 0.55, "TG-SEC-005": 0.80, "TG-SEC-006": 0.75,
957
+ "TG-SEC-007": 0.95,
958
+ "TG-AUTH-001": 0.85, "TG-AUTH-002": 0.50, "TG-AUTH-003": 0.70,
959
+ "TG-AUTH-004": 0.80, "TG-AUTH-005": 0.75, "TG-AUTH-006": 0.65,
960
+ "TG-AUTH-007": 0.60, "TG-AUTH-008": 0.70,
961
+ "TG-DB-001": 0.75, "TG-DB-002": 0.90, "TG-DB-003": 0.80, "TG-DB-004": 0.55,
962
+ "TG-INPUT-001": 0.40, "TG-INPUT-002": 0.85, "TG-INPUT-003": 0.35,
963
+ "TG-INPUT-004": 0.80, "TG-INPUT-005": 0.75, "TG-INPUT-006": 0.70,
964
+ "TG-RATE-001": 0.45, "TG-RATE-002": 0.45, "TG-RATE-003": 0.50,
965
+ "TG-AGENT-001": 0.80, "TG-AGENT-002": 0.85, "TG-AGENT-003": 0.60,
966
+ "TG-AGENT-004": 0.55,
967
+ "TG-SSRF-001": 0.75, "TG-SSRF-002": 0.90, "TG-SSRF-003": 0.50,
968
+ "TG-SSRF-004": 0.65,
969
+ "TG-WEBHOOK-001": 0.55, "TG-WEBHOOK-002": 0.50, "TG-WEBHOOK-003": 0.45,
970
+ "TG-WEBHOOK-004": 0.60,
971
+ "TG-WS-001": 0.50, "TG-WS-002": 0.55, "TG-WS-003": 0.50, "TG-WS-004": 0.55,
972
+ "TG-CSRF-001": 0.60, "TG-CSRF-002": 0.80,
973
+ "TG-GQL-001": 0.55, "TG-GQL-002": 0.50, "TG-GQL-003": 0.50, "TG-GQL-004": 0.85,
974
+ "TG-SUPPLY-001": 0.90, "TG-SUPPLY-002": 0.80, "TG-SUPPLY-003": 0.85,
975
+ "TG-SUPPLY-004": 0.60, "TG-SUPPLY-005": 0.80, "TG-SUPPLY-006": 0.85,
976
+ "TG-BIZ-001": 0.60, "TG-BIZ-002": 0.55, "TG-BIZ-003": 0.65, "TG-BIZ-004": 0.70,
977
+ "TG-CACHE-001": 0.65, "TG-CACHE-002": 0.50, "TG-CACHE-003": 0.55,
978
+ "TG-CLIENT-001": 0.80, "TG-CLIENT-002": 0.75,
979
+ "TG-PLATFORM-001": 0.70, "TG-PLATFORM-002": 0.50, "TG-PLATFORM-003": 0.75,
980
+ "TG-PLATFORM-004": 0.60,
981
+ "TG-DIFF-001": 0.90, "TG-DIFF-002": 0.85, "TG-DIFF-003": 0.80,
982
+ "TG-EDGE-001": 0.55, "TG-EDGE-003": 0.60,
983
+ }
984
+
985
+ # ─── File-Type Discrimination ─────────────────────────────────────────────────
986
+ # Maps file path patterns to sets of rule families that should be SUPPRESSED
987
+ FILE_TYPE_SUPPRESSIONS: List[Tuple[re.Pattern, set]] = [
988
+ # Config & build tool files — suppress injection rules for eval/exec
989
+ (re.compile(r'(?:webpack|vite|rollup|esbuild|jest|vitest|babel|eslint|prettier|postcss|tailwind)\.config', re.IGNORECASE),
990
+ {"TG-INPUT-003"}),
991
+ # Type declaration files — no runtime code
992
+ (re.compile(r'\.d\.ts$'),
993
+ {"TG-INPUT-001", "TG-INPUT-002", "TG-INPUT-003", "TG-INPUT-004", "TG-INPUT-005",
994
+ "TG-RATE-001", "TG-RATE-002", "TG-RATE-003", "TG-DB-001", "TG-DB-004"}),
995
+ # .env.example / .env.sample — template files, not real credentials
996
+ (re.compile(r'\.env\.(?:example|sample|template|defaults)$', re.IGNORECASE),
997
+ {"TG-SEC-003", "TG-SEC-001"}),
998
+ # Lock files — suppress secret scanning
999
+ (re.compile(r'(?:package-lock|yarn\.lock|pnpm-lock|Pipfile\.lock|poetry\.lock)'),
1000
+ {"TG-SEC-001", "TG-SEC-005"}),
1001
+ # Migration files — suppress input validation rules (generated SQL)
1002
+ (re.compile(r'/migrations?/', re.IGNORECASE),
1003
+ {"TG-INPUT-001", "TG-INPUT-002", "TG-RATE-001"}),
1004
+ # Dockerfiles — suppress frontend-only rules, keep container-specific rules
1005
+ (re.compile(r'Dockerfile', re.IGNORECASE),
1006
+ {"TG-INPUT-003", "TG-RATE-001", "TG-RATE-002", "TG-DB-001", "TG-GQL-001"}),
1007
+ ]
1008
+
1009
+ def get_suppressed_rules_for_file(file_path: str) -> set:
1010
+ """Return the set of rule IDs that should be suppressed for this file type."""
1011
+ suppressed: set = set()
1012
+ for pattern, rules in FILE_TYPE_SUPPRESSIONS:
1013
+ if pattern.search(file_path):
1014
+ suppressed.update(rules)
1015
+ return suppressed
1016
+
1017
+
1018
+ # ─── Context-Aware Validation Engine ──────────────────────────────────────────
1019
+ def validate_finding_context(
1020
+ rule_id: str, line_idx: int, lines: List[str],
1021
+ file_path: str, full_content: str
1022
+ ) -> bool:
1023
+ """
1024
+ Post-match context validation. Returns False if the finding is a known
1025
+ false-positive pattern based on surrounding code context.
1026
+ """
1027
+ # Compute lookahead (next 5 lines) and lookbehind (prev 5 lines)
1028
+ lookahead = "\n".join(lines[line_idx:min(line_idx + 6, len(lines))])
1029
+ lookbehind = "\n".join(lines[max(0, line_idx - 5):line_idx])
1030
+
1031
+ # ── TG-INPUT-001: req.body destructuring followed by schema validation ────
1032
+ if rule_id == "TG-INPUT-001":
1033
+ if re.search(r'\.(parse|validate|safeParse|check|verify|sanitize)\s*\(', lookahead):
1034
+ return False
1035
+ # Zod/Joi/Yup schema declared before destructuring
1036
+ if re.search(r'(?:z\.|Joi\.|yup\.|schema\.).*\.(parse|validate)', lookbehind + lookahead):
1037
+ return False
1038
+
1039
+ # ── TG-INPUT-003: eval/exec in sandboxed or build-tool context ────────────
1040
+ if rule_id == "TG-INPUT-003":
1041
+ current_line = lines[line_idx].strip() if line_idx < len(lines) else ""
1042
+ # Suppress if it's a JSON.parse (not dangerous)
1043
+ if "JSON.parse" in current_line or "json.loads" in current_line:
1044
+ return False
1045
+ # Suppress eval in CSS-in-JS tagged templates
1046
+ if re.search(r'styled|css`|keyframes', lookbehind):
1047
+ return False
1048
+
1049
+ # ── TG-RATE-001/002: Rate limiter already applied earlier in file ─────────
1050
+ if rule_id in ("TG-RATE-001", "TG-RATE-002"):
1051
+ if re.search(r'(?:rateLimit|rateLimiter|slowDown|throttle|express-rate-limit)\s*\(', full_content):
1052
+ return False
1053
+ # Check if middleware is applied on the same router
1054
+ if re.search(r'app\.use\s*\([^)]*(?:rateLimit|limiter|throttle)', full_content):
1055
+ return False
1056
+
1057
+ # ── TG-PLATFORM-002: Helmet used anywhere in the file or app setup ────────
1058
+ if rule_id == "TG-PLATFORM-002":
1059
+ if "helmet" in full_content and re.search(r'(?:app|server)\.use\s*\(\s*helmet', full_content):
1060
+ return False
1061
+
1062
+ # ── TG-SSRF-004: requests.get with timeout in kwargs or session defaults ──
1063
+ if rule_id == "TG-SSRF-004":
1064
+ # Check if a session with default timeout is set earlier
1065
+ if re.search(r'session\.(?:request\.)?timeout|DEFAULT_TIMEOUT', full_content):
1066
+ return False
1067
+
1068
+ # ── TG-AUTH-003: JWT verify with algorithms in options object ──────────────
1069
+ if rule_id == "TG-AUTH-003":
1070
+ if re.search(r'algorithms\s*:', lookahead):
1071
+ return False
1072
+
1073
+ # ── TG-GQL-001: ApolloServer with plugins/validation rules elsewhere ──────
1074
+ if rule_id == "TG-GQL-001":
1075
+ if re.search(r'(?:depthLimit|costAnalysis|complexityLimit|validationRules)', full_content):
1076
+ return False
1077
+
1078
+ # ── TG-SEC-004: Logging that redacts sensitive fields ──────────────────────
1079
+ if rule_id == "TG-SEC-004":
1080
+ if re.search(r'(?:redact|mask|sanitize|\*{3,}|\[REDACTED\])', lookahead):
1081
+ return False
1082
+
1083
+ # ── TG-DB-004: findById with ownership check in next few lines ────────────
1084
+ if rule_id == "TG-DB-004":
1085
+ if re.search(r'(?:tenantId|tenant_id|organization_id|user\.id|userId|ownerId)', lookahead):
1086
+ return False
1087
+
1088
+ # ── TG-WEBHOOK-001: Signature verification in handler body ────────────────
1089
+ if rule_id == "TG-WEBHOOK-001":
1090
+ # Check if HMAC/signature verification happens in the function body
1091
+ func_body = "\n".join(lines[line_idx:min(line_idx + 20, len(lines))])
1092
+ if re.search(r'(?:verifySignature|hmac|createHmac|verify_webhook|constructEvent)', func_body):
1093
+ return False
1094
+
1095
+ return True # Keep the finding
1096
+
1097
+
1098
+ # File extensions to scan
1099
+ SOURCE_EXTENSIONS = {
1100
+ ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
1101
+ ".go", ".java", ".cs", ".php", ".rb", ".json", ".yaml", ".yml"
1102
+ }
1103
+
1104
+ # Directories to skip
1105
+ SKIP_DIRS = {
1106
+ ".git", "node_modules", ".venv", "venv", "env", "__pycache__",
1107
+ "dist", "build", ".next", ".nuxt", "coverage", ".torusguard",
1108
+ ".idea", ".vscode", "target", "bin", "obj", "payload"
1109
+ }
1110
+
1111
+ def is_test_file(path_str: str) -> bool:
1112
+ """Check if file belongs to tests, fixtures, or test harnesses across any language."""
1113
+ clean = "/" + path_str.replace("\\", "/").strip("/").lower() + "/"
1114
+ markers = [
1115
+ "/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
1116
+ "/harness/", "/qa-runs/", "/test_repos/", "/__tests__/"
1117
+ ]
1118
+ if any(m in clean for m in markers):
1119
+ return True
1120
+ filename = Path(path_str).name.lower()
1121
+ return filename.startswith("test_") or "_test." in filename or ".test." in filename or ".spec." in filename
1122
+
1123
+
1124
+ def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[Path]:
1125
+ """Recursively collect source files eligible for static security audit."""
1126
+ files = []
1127
+ for root, dirs, filenames in os.walk(target_root):
1128
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
1129
+ for f in filenames:
1130
+ ext = os.path.splitext(f)[1].lower()
1131
+ if ext in SOURCE_EXTENSIONS:
1132
+ if f in ("audit_runner.py", "diff_guard.py", "finding_scorer.py", "harden_runner.py", "apply_runner.py", "recheck_runner.py", "report_sync.py", "term_ui.py", "sarif_exporter.py", "safety_gate.py", "stack_detect.py", "recipes_runner.py", "html_reporter.py", "run_manager.py", "rules_sync.py", "bootstrap.py", "scope.json", "rules_catalog.json"):
1133
+ continue
1134
+ full_path = Path(root) / f
1135
+ if not include_tests and is_test_file(str(full_path)):
1136
+ continue
1137
+ files.append(full_path)
1138
+ return sorted(files)
1139
+
1140
+
1141
+ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
1142
+ """Scan a single source file against the TorusGuard rule families.
1143
+ Enhanced with context-aware validation, file-type discrimination,
1144
+ developer suppression comments, canary tags, and multi-finding support.
1145
+ """
1146
+ findings = []
1147
+ try:
1148
+ content = file_path.read_text(encoding="utf-8", errors="replace")
1149
+ except Exception:
1150
+ return findings
1151
+
1152
+ lines = content.splitlines()
1153
+ rel_path = str(file_path.relative_to(target_root)).replace("\\", "/")
1154
+ is_test = is_test_file(rel_path)
1155
+
1156
+ # Phase 1c: File-type discrimination — get suppressed rules for this file
1157
+ suppressed_rules = get_suppressed_rules_for_file(rel_path)
1158
+
1159
+ for rule in RULE_PATTERNS:
1160
+ rule_id = rule["rule_id"]
1161
+
1162
+ # Skip rules suppressed by file-type discrimination
1163
+ if rule_id in suppressed_rules:
1164
+ continue
1165
+
1166
+ patterns: List[Tuple[Pattern[str], str]] = rule["patterns"]
1167
+ found_lines: set = set() # Track matched lines to avoid duplicates
1168
+
1169
+ for regex, desc in patterns:
1170
+ for idx, line in enumerate(lines):
1171
+ if idx in found_lines:
1172
+ continue # Already reported a finding on this line for this rule
1173
+
1174
+ stripped = line.strip()
1175
+ if stripped.startswith(("#", "//", "/*", "*")):
1176
+ if "bypass" not in stripped.lower() and "todo" not in stripped.lower():
1177
+ continue
1178
+
1179
+ match = regex.search(line)
1180
+ if match:
1181
+ line_lower = line.lower()
1182
+ prev_line = lines[idx - 1].lower() if idx > 0 else ""
1183
+
1184
+ # Developer suppression annotations (e.g. # torusguard: ignore, // tg-ignore)
1185
+ if "torusguard: ignore" in line_lower or "torusguard: ignore" in prev_line or \
1186
+ "tg-ignore" in line_lower or "tg-ignore" in prev_line:
1187
+ if rule_id != "TG-DIFF-001":
1188
+ continue
1189
+
1190
+ # Developer test canary annotations (# torusguard: test-canary)
1191
+ is_canary = (
1192
+ "torusguard: test-canary" in line_lower or "torusguard: test-canary" in prev_line or
1193
+ "tg-canary" in line_lower or "tg-canary" in prev_line or
1194
+ "test-canary" in line_lower or "test-canary" in prev_line or
1195
+ is_test
1196
+ )
1197
+
1198
+ # Phase 1a: Context-aware false positive suppression
1199
+ if not validate_finding_context(rule_id, idx, lines, rel_path, content):
1200
+ continue # Suppressed by context validation
1201
+
1202
+ line_num = idx + 1
1203
+ seed = f"{rule_id}:{rel_path}:{line_num}:{stripped}"
1204
+ fp = f"{rule_id}-{hashlib.sha256(seed.encode('utf-8')).hexdigest()[:8]}"
1205
+
1206
+ start_ctx = max(0, idx - 2)
1207
+ end_ctx = min(len(lines), idx + 3)
1208
+ snippet = "\n".join(lines[start_ctx:end_ctx])
1209
+
1210
+ findings.append({
1211
+ "finding_id": fp,
1212
+ "rule_id": rule_id,
1213
+ "title": rule["title"],
1214
+ "description": desc,
1215
+ "severity": "Low" if (is_test or is_canary) else rule["severity"],
1216
+ "category": rule["category"],
1217
+ "cluster": rule["cluster"],
1218
+ "file_path": rel_path,
1219
+ "line_number": line_num,
1220
+ "matched_text": match.group(0)[:80],
1221
+ "snippet": snippet,
1222
+ "is_test": is_test,
1223
+ "is_canary": is_canary,
1224
+ "rule_precision": RULE_PRECISION.get(rule_id, 0.60),
1225
+ "target": {
1226
+ "file_path": rel_path,
1227
+ "start_line": line_num,
1228
+ "end_line": line_num
1229
+ }
1230
+ })
1231
+ found_lines.add(idx)
1232
+ return findings
1233
+
1234
+
1235
+ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
1236
+ """
1237
+ Score each finding using finding_scorer.py and persistent memory patterns.
1238
+ Returns (scored_findings, clusters_map).
1239
+ """
1240
+ try:
1241
+ import finding_scorer
1242
+ except Exception:
1243
+ finding_scorer = None
1244
+
1245
+ scored = []
1246
+ clusters: Dict[str, List[Dict[str, Any]]] = {}
1247
+
1248
+ # Phase 1d: Pre-compute cross-file corroboration bonus
1249
+ # If multiple rule families flag the same file, each finding gets +5 confidence
1250
+ file_rule_families: Dict[str, set] = {}
1251
+ for f in findings:
1252
+ fp = f["file_path"]
1253
+ if fp not in file_rule_families:
1254
+ file_rule_families[fp] = set()
1255
+ file_rule_families[fp].add(f["category"])
1256
+
1257
+ for f in findings:
1258
+ score = 70
1259
+ band = "High Confidence"
1260
+ factors = {}
1261
+
1262
+ if finding_scorer:
1263
+ try:
1264
+ # Phase 1d: Dynamic evidence quality based on rule precision
1265
+ precision = f.get("rule_precision", RULE_PRECISION.get(f["rule_id"], 0.60))
1266
+ eq = max(10, int(35 * precision)) # Scale evidence quality by precision
1267
+ rs = 0
1268
+
1269
+ # Corroboration bonus: multiple rule families in same file
1270
+ families_in_file = len(file_rule_families.get(f["file_path"], set()))
1271
+ ic = min(15, 5 + (families_in_file - 1) * 3) if families_in_file > 1 else 5
1272
+
1273
+ # Environmental clarity: production files score higher
1274
+ is_prod = not f.get("is_test", False)
1275
+ ec = 15 if is_prod else 5
1276
+
1277
+ mr = 0
1278
+ s, b, facts = finding_scorer.compute_confidence_score(
1279
+ evidence_quality=eq,
1280
+ reproduction_success=rs,
1281
+ independent_confirmations=ic,
1282
+ environmental_clarity=ec,
1283
+ manual_review_status=mr,
1284
+ rule_id=f["rule_id"],
1285
+ file_path=f["file_path"],
1286
+ root_dir=target_root
1287
+ )
1288
+ score = s
1289
+ band = b
1290
+ factors = facts
1291
+ except Exception:
1292
+ pass
1293
+
1294
+ f["confidence_score"] = score
1295
+ f["confidence_band"] = band
1296
+ f["confidence_factors"] = factors
1297
+ scored.append(f)
1298
+
1299
+ c_id = f["cluster"]
1300
+ if c_id not in clusters:
1301
+ clusters[c_id] = []
1302
+ clusters[c_id].append(f)
1303
+
1304
+ # Sort descending by severity (Critical -> High -> Medium -> Low), then score
1305
+ sev_rank = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1}
1306
+ scored.sort(key=lambda x: (sev_rank.get(x["severity"], 0), x["confidence_score"]), reverse=True)
1307
+ return scored, clusters
1308
+
1309
+
1310
+ def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], target_root: Path) -> None:
1311
+ """Generate findings.json, findings.md, and summary.md into run folder."""
1312
+ now_ist = get_ist_now()
1313
+
1314
+ # 1. findings.json
1315
+ (run_folder / "findings.json").write_text(json.dumps(scored_findings, indent=2), encoding="utf-8")
1316
+
1317
+ # 2. findings.md
1318
+ md_lines = [
1319
+ f"# TorusGuard Security Audit Findings: {run_folder.name}",
1320
+ f"- Target Root: `{target_root}`",
1321
+ f"- Total Scored Findings: `{len(scored_findings)}`",
1322
+ f"- Identified Clusters: `{len(clusters)}`",
1323
+ f"- Generated At: `{now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}`\n",
1324
+ "---",
1325
+ "## Summary of Findings\n"
1326
+ ]
1327
+
1328
+ for f in scored_findings:
1329
+ md_lines.append(f"### [{f['severity']}] {f['rule_id']} - {f['title']}")
1330
+ md_lines.append(f"- **Finding ID:** `{f['finding_id']}`")
1331
+ md_lines.append(f"- **Location:** `{f['file_path']}:{f['line_number']}`")
1332
+ md_lines.append(f"- **Confidence Score:** `{f['confidence_score']}/100` ({f['confidence_band']})")
1333
+ md_lines.append(f"- **Cluster:** `{f['cluster']}`")
1334
+ md_lines.append(f"- **Description:** {f['description']}")
1335
+ md_lines.append("```")
1336
+ md_lines.append(f['snippet'])
1337
+ md_lines.append("```\n")
1338
+
1339
+ (run_folder / "findings.md").write_text("\n".join(md_lines), encoding="utf-8")
1340
+
1341
+ # 3. summary.md
1342
+ summary_lines = [
1343
+ f"# TorusGuard Audit Summary: {run_folder.name}",
1344
+ f"- Scanned Scope: {target_root}",
1345
+ f"- Total Findings: {len(scored_findings)}",
1346
+ f"- Root-Cause Clusters: {len(clusters)}",
1347
+ f"- Generated: {now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}"
1348
+ ]
1349
+ (run_folder / "summary.md").write_text("\n".join(summary_lines), encoding="utf-8")
1350
+
1351
+ # 4. Update manifest.json status
1352
+ manifest_file = run_folder / "manifest.json"
1353
+ if manifest_file.exists():
1354
+ try:
1355
+ with open(manifest_file, "r", encoding="utf-8") as mf:
1356
+ mdata = json.load(mf)
1357
+ mdata["status"] = "completed"
1358
+ mdata["findings_count"] = len(scored_findings)
1359
+ with open(manifest_file, "w", encoding="utf-8") as mf:
1360
+ json.dump(mdata, mf, indent=2)
1361
+ except Exception:
1362
+ pass
1363
+
1364
+
1365
+ 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:
1366
+ """Print the unified 75-column TorusGuard Static Audit results card."""
1367
+ crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
1368
+ high_count = sum(1 for f in scored_findings if f["severity"] == "High")
1369
+ med_count = sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low"))
1370
+
1371
+ stack_str = "Auto-detected"
1372
+ if detected_stack and detected_stack.get("framework") and detected_stack.get("framework") != "None":
1373
+ stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
1374
+
1375
+ print()
1376
+ print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.3.6"))
1377
+ print()
1378
+
1379
+ print(card_border_top("Audit Execution Scope"))
1380
+ print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root)}{RESET}"))
1381
+ print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str}{RESET}"))
1382
+ print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 18 rule families (74 rules){RESET}"))
1383
+ print(card_border_bottom())
1384
+ print()
1385
+
1386
+ # Findings Box
1387
+ 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}")
1388
+ print(card_border_top("Findings & Confidence Scores"))
1389
+ print(format_box_line(f"Status: {status_icon}"))
1390
+ 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)"))
1391
+ print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
1392
+ print(card_divider())
1393
+
1394
+ if not scored_findings:
1395
+ print(format_box_line(f"{GREEN}✔ Zero security invariant violations detected.{RESET}"))
1396
+ else:
1397
+ for idx, f in enumerate(scored_findings[:5]):
1398
+ sev_color = RED if f["severity"] == "Critical" else (YELLOW if f["severity"] == "High" else CYAN)
1399
+ line_str = f"[{sev_color}{f['severity']}{RESET}] {BOLD}{f['rule_id']}{RESET} at {f['file_path']}:{f['line_number']}"
1400
+ print(format_box_line(line_str))
1401
+ desc_str = f" └─ {DIM}{f['description'][:58]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
1402
+ print(format_box_line(desc_str))
1403
+ if len(scored_findings) > 5:
1404
+ print(format_box_line(f"{DIM}... and {len(scored_findings) - 5} more findings in report file{RESET}"))
1405
+
1406
+ print(card_border_bottom())
1407
+ print()
1408
+
1409
+ # Run Artifacts & Next Steps Box
1410
+ rel_run = str(run_folder.relative_to(target_root)).replace("\\", "/") if str(run_folder).startswith(str(target_root)) else str(run_folder).replace("\\", "/")
1411
+ findings_artifact = f"{rel_run}/findings.md"
1412
+ print(card_border_top("Artifacts & Next Actions", border_color=GREEN, double=True))
1413
+ print(format_box_line(f"{BOLD}Run Artifact:{RESET} {WHITE}{findings_artifact}{RESET}", border="║", border_color=GREEN))
1414
+ print(format_box_line(f"{BOLD}Living Ledger:{RESET} {CYAN}security_report.md (updated){RESET}", border="║", border_color=GREEN))
1415
+ print(format_box_line(f"{BOLD}Visual Dash:{RESET} {CYAN}npx torusguard report --html{RESET}", border="║", border_color=GREEN))
1416
+ print(format_box_line(f"{BOLD}Surgical Fix:{RESET} In AI Chat, run {CYAN}/torusguard-harden{RESET}", border="║", border_color=GREEN))
1417
+ print(card_border_bottom(border_color=GREEN, double=True))
1418
+ print()
1419
+
1420
+
1421
+ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
1422
+ """Execute the full TorusGuard static security audit."""
1423
+ target_root = target_root.resolve()
1424
+ s_dir = Path(__file__).resolve().parent
1425
+
1426
+ # 1. Stack detection
1427
+ detected_stack = None
1428
+ stack_script = s_dir / "stack_detect.py"
1429
+ if stack_script.is_file():
1430
+ try:
1431
+ import importlib.util
1432
+ spec = importlib.util.spec_from_file_location("stack_detect", str(stack_script))
1433
+ if spec and spec.loader:
1434
+ sd = importlib.util.module_from_spec(spec)
1435
+ spec.loader.exec_module(sd)
1436
+ if hasattr(sd, "detect_stack"):
1437
+ detected_stack = sd.detect_stack(target_root)
1438
+ except Exception:
1439
+ pass
1440
+
1441
+ # 2. Collect files & scan
1442
+ files = find_files_to_scan(target_root, include_tests=include_tests)
1443
+ all_findings = []
1444
+ for f in files:
1445
+ all_findings.extend(scan_file(f, target_root))
1446
+
1447
+ # 3. Score & Cluster
1448
+ scored_findings, clusters = score_and_cluster_findings(all_findings, target_root)
1449
+
1450
+ # 4. Allocate run folder
1451
+ runs_dir = target_root / ".torusguard" / "runs"
1452
+ runs_dir.mkdir(parents=True, exist_ok=True)
1453
+
1454
+ if str(s_dir) not in sys.path:
1455
+ sys.path.insert(0, str(s_dir))
1456
+ try:
1457
+ import run_manager
1458
+ run_folder = run_manager.create_run(runs_dir, "audit", target_root.name)
1459
+ except Exception:
1460
+ timestamp = get_ist_now().strftime("%Y%m%d-%H%M%S")
1461
+ run_folder = runs_dir / f"run-{timestamp}-audit"
1462
+ run_folder.mkdir(parents=True, exist_ok=True)
1463
+
1464
+ # 5. Emit artifacts & sync memory
1465
+ emit_run_artifacts(run_folder, scored_findings, clusters, target_root)
1466
+ try:
1467
+ import report_sync
1468
+ report_sync.record_audit_findings(target_root, scored_findings, run_folder.name)
1469
+ except Exception:
1470
+ pass
1471
+ try:
1472
+ if "run_manager" in sys.modules:
1473
+ sys.modules["run_manager"].sync_run_to_memory(run_folder, scored_findings)
1474
+ except Exception:
1475
+ pass
1476
+
1477
+ result = {
1478
+ "status": "completed",
1479
+ "target_root": str(target_root),
1480
+ "files_scanned": len(files),
1481
+ "total_findings": len(scored_findings),
1482
+ "critical_count": sum(1 for f in scored_findings if f["severity"] == "Critical"),
1483
+ "high_count": sum(1 for f in scored_findings if f["severity"] == "High"),
1484
+ "medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
1485
+ "clusters_count": len(clusters),
1486
+ "run_folder": str(run_folder),
1487
+ "findings": scored_findings
1488
+ }
1489
+
1490
+ if json_output:
1491
+ print(json.dumps(result, indent=2))
1492
+ else:
1493
+ print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
1494
+
1495
+ return result
1496
+
1497
+
1498
+ def main():
1499
+ parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
1500
+ parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
1501
+ parser.add_argument("--scope", "-s", help="Alternative path to target project")
1502
+ parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
1503
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
1504
+ parser.add_argument("--include-tests", action="store_true", help="Include test files and harness fixtures in scan")
1505
+ args = parser.parse_args()
1506
+
1507
+ target = Path(args.scope or args.path).resolve()
1508
+ res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
1509
+ sys.exit(0 if res["critical_count"] == 0 else 1)
1510
+
1511
+
1512
+ if __name__ == "__main__":
1513
+ main()