torusguard 1.3.3 → 1.3.5

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 (322) 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 +93 -7
  33. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/metadata.json +15 -0
  34. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/minimal_patch_plan.md +21 -0
  35. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/patch.diff +9 -0
  36. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/metadata.json +15 -0
  37. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/minimal_patch_plan.md +21 -0
  38. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/patch.diff +9 -0
  39. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/metadata.json +15 -0
  40. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/minimal_patch_plan.md +21 -0
  41. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/patch.diff +9 -0
  42. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/metadata.json +15 -0
  43. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/minimal_patch_plan.md +21 -0
  44. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/patch.diff +9 -0
  45. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/metadata.json +15 -0
  46. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/minimal_patch_plan.md +21 -0
  47. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/patch.diff +9 -0
  48. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/metadata.json +15 -0
  49. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/minimal_patch_plan.md +21 -0
  50. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/patch.diff +9 -0
  51. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/metadata.json +15 -0
  52. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/minimal_patch_plan.md +21 -0
  53. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/patch.diff +9 -0
  54. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/metadata.json +15 -0
  55. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/minimal_patch_plan.md +21 -0
  56. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/patch.diff +9 -0
  57. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +15 -0
  58. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  59. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  60. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/metadata.json +15 -0
  61. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/minimal_patch_plan.md +21 -0
  62. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/patch.diff +9 -0
  63. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/metadata.json +15 -0
  64. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/minimal_patch_plan.md +21 -0
  65. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/patch.diff +9 -0
  66. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/metadata.json +15 -0
  67. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/minimal_patch_plan.md +21 -0
  68. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/patch.diff +9 -0
  69. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/metadata.json +15 -0
  70. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/minimal_patch_plan.md +21 -0
  71. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/patch.diff +9 -0
  72. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/metadata.json +15 -0
  73. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/minimal_patch_plan.md +21 -0
  74. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/patch.diff +9 -0
  75. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/metadata.json +15 -0
  76. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/minimal_patch_plan.md +21 -0
  77. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/patch.diff +9 -0
  78. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/metadata.json +15 -0
  79. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/minimal_patch_plan.md +21 -0
  80. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/patch.diff +9 -0
  81. package/.torusguard/runs/run-20260910-120238-audit/findings.json +1520 -0
  82. package/.torusguard/runs/run-20260910-120238-audit/findings.md +560 -0
  83. package/.torusguard/runs/run-20260910-120238-audit/manifest.json +13 -0
  84. package/.torusguard/runs/run-20260910-120238-audit/recheck.md +86 -0
  85. package/.torusguard/runs/run-20260910-120238-audit/remediation.md +262 -0
  86. package/.torusguard/runs/run-20260910-120238-audit/summary.md +5 -0
  87. package/.torusguard/runs/run-20260910-121238-audit/findings.json +794 -0
  88. package/.torusguard/runs/run-20260910-121238-audit/findings.md +344 -0
  89. package/.torusguard/runs/run-20260910-121238-audit/manifest.json +12 -0
  90. package/.torusguard/runs/run-20260910-121238-audit/summary.md +5 -0
  91. package/.torusguard/runs/run-20260910-130454-audit/findings.json +893 -0
  92. package/.torusguard/runs/run-20260910-130454-audit/findings.md +386 -0
  93. package/.torusguard/runs/run-20260910-130454-audit/manifest.json +12 -0
  94. package/.torusguard/runs/run-20260910-130454-audit/summary.md +5 -0
  95. package/.torusguard/runs/run-20260910-130519-audit/findings.json +563 -0
  96. package/.torusguard/runs/run-20260910-130519-audit/findings.md +246 -0
  97. package/.torusguard/runs/run-20260910-130519-audit/manifest.json +12 -0
  98. package/.torusguard/runs/run-20260910-130519-audit/summary.md +5 -0
  99. package/.torusguard/runs/run-20260912-214110-audit/findings.json +1256 -0
  100. package/.torusguard/runs/run-20260912-214110-audit/findings.md +534 -0
  101. package/.torusguard/runs/run-20260912-214110-audit/manifest.json +12 -0
  102. package/.torusguard/runs/run-20260912-214110-audit/summary.md +5 -0
  103. package/.torusguard/runs/run-20260912-214813-audit/findings.json +992 -0
  104. package/.torusguard/runs/run-20260912-214813-audit/findings.md +422 -0
  105. package/.torusguard/runs/run-20260912-214813-audit/manifest.json +12 -0
  106. package/.torusguard/runs/run-20260912-214813-audit/summary.md +5 -0
  107. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/metadata.json +17 -0
  108. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/minimal_patch_plan.md +21 -0
  109. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/patch.diff +9 -0
  110. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
  111. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  112. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  113. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
  114. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
  115. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
  116. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
  117. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
  118. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
  119. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
  120. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
  121. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
  122. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
  123. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
  124. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
  125. package/.torusguard/runs/run-20260912-215555-audit/findings.json +530 -0
  126. package/.torusguard/runs/run-20260912-215555-audit/findings.md +232 -0
  127. package/.torusguard/runs/run-20260912-215555-audit/manifest.json +13 -0
  128. package/.torusguard/runs/run-20260912-215555-audit/remediation.md +102 -0
  129. package/.torusguard/runs/run-20260912-215555-audit/summary.md +5 -0
  130. package/.torusguard/runs/run-20260912-215731-audit/apply_plan.md +6 -0
  131. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/metadata.json +17 -0
  132. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/minimal_patch_plan.md +21 -0
  133. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/patch.diff +9 -0
  134. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/metadata.json +17 -0
  135. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/minimal_patch_plan.md +21 -0
  136. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/patch.diff +9 -0
  137. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/metadata.json +17 -0
  138. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/minimal_patch_plan.md +22 -0
  139. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/patch.diff +10 -0
  140. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
  141. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  142. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  143. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
  144. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
  145. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
  146. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/metadata.json +17 -0
  147. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/minimal_patch_plan.md +23 -0
  148. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/patch.diff +11 -0
  149. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/metadata.json +17 -0
  150. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/minimal_patch_plan.md +23 -0
  151. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/patch.diff +11 -0
  152. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
  153. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
  154. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
  155. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/metadata.json +17 -0
  156. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/minimal_patch_plan.md +21 -0
  157. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/patch.diff +9 -0
  158. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/metadata.json +17 -0
  159. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/minimal_patch_plan.md +21 -0
  160. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/patch.diff +9 -0
  161. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
  162. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
  163. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
  164. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/metadata.json +17 -0
  165. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/minimal_patch_plan.md +21 -0
  166. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/patch.diff +9 -0
  167. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/metadata.json +17 -0
  168. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/minimal_patch_plan.md +21 -0
  169. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/patch.diff +9 -0
  170. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
  171. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
  172. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
  173. package/.torusguard/runs/run-20260912-215731-audit/diff_summary.md +186 -0
  174. package/.torusguard/runs/run-20260912-215731-audit/findings.json +497 -0
  175. package/.torusguard/runs/run-20260912-215731-audit/findings.md +218 -0
  176. package/.torusguard/runs/run-20260912-215731-audit/manifest.json +14 -0
  177. package/.torusguard/runs/run-20260912-215731-audit/recheck.md +76 -0
  178. package/.torusguard/runs/run-20260912-215731-audit/remediation.md +234 -0
  179. package/.torusguard/runs/run-20260912-215731-audit/summary.md +5 -0
  180. package/.torusguard/runs/run-20260912-220134-audit/findings.json +68 -0
  181. package/.torusguard/runs/run-20260912-220134-audit/findings.md +36 -0
  182. package/.torusguard/runs/run-20260912-220134-audit/manifest.json +12 -0
  183. package/.torusguard/runs/run-20260912-220134-audit/summary.md +5 -0
  184. package/.torusguard/runs/run-20260912-220200-audit/findings.json +1 -0
  185. package/.torusguard/runs/run-20260912-220200-audit/findings.md +8 -0
  186. package/.torusguard/runs/run-20260912-220200-audit/manifest.json +12 -0
  187. package/.torusguard/runs/run-20260912-220200-audit/recheck.md +6 -0
  188. package/.torusguard/runs/run-20260912-220200-audit/summary.md +5 -0
  189. package/.torusguard/runs/run-20260912-220227-audit/findings.json +1 -0
  190. package/.torusguard/runs/run-20260912-220227-audit/findings.md +8 -0
  191. package/.torusguard/runs/run-20260912-220227-audit/manifest.json +12 -0
  192. package/.torusguard/runs/run-20260912-220227-audit/recheck.md +6 -0
  193. package/.torusguard/runs/run-20260912-220227-audit/summary.md +5 -0
  194. package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  195. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  196. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  197. package/.torusguard/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  198. package/.torusguard/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  199. package/.torusguard/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  200. package/.torusguard/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  201. package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  202. package/.torusguard/scripts/apply_runner.py +383 -339
  203. package/.torusguard/scripts/audit_runner.py +1306 -550
  204. package/.torusguard/scripts/finding_scorer.py +14 -2
  205. package/.torusguard/scripts/harden_runner.py +628 -375
  206. package/.torusguard/scripts/recheck_runner.py +248 -221
  207. package/.torusguard/scripts/recipes_runner.py +137 -130
  208. package/.torusguard/scripts/report_sync.py +423 -0
  209. package/.torusguard/scripts/sarif_exporter.py +116 -116
  210. package/.torusguard/scripts/term_ui.py +162 -0
  211. package/.torusguard/skills/torusguard/SKILL.md +61 -31
  212. package/.torusguard/skills/torusguard-apply/SKILL.md +80 -46
  213. package/.torusguard/skills/torusguard-audit/SKILL.md +83 -70
  214. package/.torusguard/skills/torusguard-authorize/SKILL.md +5 -4
  215. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +6 -5
  216. package/.torusguard/skills/torusguard-full/SKILL.md +1 -1
  217. package/.torusguard/skills/torusguard-harden/SKILL.md +94 -63
  218. package/.torusguard/skills/torusguard-init/SKILL.md +64 -52
  219. package/.torusguard/skills/torusguard-recheck/SKILL.md +77 -53
  220. package/.torusguard/skills/torusguard-report/SKILL.md +72 -54
  221. package/.torusguard/skills/torusguard-status/SKILL.md +58 -42
  222. package/.torusguard/skills/torusguard-verify/SKILL.md +8 -7
  223. package/.torusguard/skills/torusguard-web-validate/SKILL.md +8 -7
  224. package/.torusguard/snapshots/run-20260912-215731-audit/demo/playground/vulnerable_nextjs/actions.ts.bak +24 -0
  225. package/.torusguard/snapshots/run-20260912-215731-audit/examples/hardened-react-express/server/index.js.bak +121 -0
  226. package/.torusguard/snapshots/run-20260912-215731-audit/examples/python/fastapi-vuln/main.py.bak +20 -0
  227. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/src/App.jsx.bak +63 -0
  228. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/vite.config.js.bak +12 -0
  229. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/server/index.js.bak +82 -0
  230. package/.torusguard/workflows/apply.md +11 -4
  231. package/.torusguard/workflows/audit.md +7 -1
  232. package/.torusguard/workflows/authorize.md +2 -1
  233. package/.torusguard/workflows/exploit-check.md +2 -1
  234. package/.torusguard/workflows/harden.md +7 -1
  235. package/.torusguard/workflows/init.md +6 -1
  236. package/.torusguard/workflows/memory.md +2 -1
  237. package/.torusguard/workflows/recheck.md +7 -1
  238. package/.torusguard/workflows/report.md +7 -1
  239. package/.torusguard/workflows/status.md +6 -1
  240. package/.torusguard/workflows/verify.md +6 -1
  241. package/.torusguard/workflows/web-validate.md +2 -1
  242. package/README.md +470 -344
  243. package/bin/torusguard.js +209 -97
  244. package/package.json +1 -1
  245. package/skills/torusguard/SKILL.md +73 -42
  246. package/skills/torusguard/payload/.manifest.json +66 -63
  247. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
  248. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
  249. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
  250. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
  251. package/skills/torusguard/payload/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
  252. package/skills/torusguard/payload/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
  253. package/skills/torusguard/payload/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
  254. package/skills/torusguard/payload/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
  255. package/skills/torusguard/payload/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
  256. package/skills/torusguard/payload/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
  257. package/skills/torusguard/payload/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
  258. package/skills/torusguard/payload/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
  259. package/skills/torusguard/payload/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
  260. package/skills/torusguard/payload/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
  261. package/skills/torusguard/payload/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
  262. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
  263. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
  264. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
  265. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
  266. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
  267. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
  268. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
  269. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
  270. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
  271. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
  272. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
  273. package/skills/torusguard/payload/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
  274. package/skills/torusguard/payload/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
  275. package/skills/torusguard/payload/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
  276. package/skills/torusguard/payload/rules_catalog.json +520 -0
  277. package/skills/torusguard/payload/scripts/apply_runner.py +383 -339
  278. package/skills/torusguard/payload/scripts/audit_runner.py +1306 -550
  279. package/skills/torusguard/payload/scripts/finding_scorer.py +14 -2
  280. package/skills/torusguard/payload/scripts/harden_runner.py +628 -375
  281. package/skills/torusguard/payload/scripts/recheck_runner.py +248 -221
  282. package/skills/torusguard/payload/scripts/recipes_runner.py +137 -130
  283. package/skills/torusguard/payload/scripts/report_sync.py +423 -0
  284. package/skills/torusguard/payload/scripts/sarif_exporter.py +116 -116
  285. package/skills/torusguard/payload/scripts/term_ui.py +162 -0
  286. package/skills/torusguard/payload/skills/torusguard/SKILL.md +75 -42
  287. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +63 -25
  288. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +62 -45
  289. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +11 -5
  290. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +12 -6
  291. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +7 -2
  292. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +71 -36
  293. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +45 -29
  294. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +59 -31
  295. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +52 -30
  296. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +40 -20
  297. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +14 -8
  298. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +14 -8
  299. package/skills/torusguard/payload/workflows/apply.md +11 -4
  300. package/skills/torusguard/payload/workflows/audit.md +7 -1
  301. package/skills/torusguard/payload/workflows/authorize.md +2 -1
  302. package/skills/torusguard/payload/workflows/exploit-check.md +2 -1
  303. package/skills/torusguard/payload/workflows/harden.md +7 -1
  304. package/skills/torusguard/payload/workflows/init.md +6 -1
  305. package/skills/torusguard/payload/workflows/memory.md +2 -1
  306. package/skills/torusguard/payload/workflows/recheck.md +7 -1
  307. package/skills/torusguard/payload/workflows/report.md +7 -1
  308. package/skills/torusguard/payload/workflows/status.md +6 -1
  309. package/skills/torusguard/payload/workflows/verify.md +6 -1
  310. package/skills/torusguard/payload/workflows/web-validate.md +2 -1
  311. package/skills/torusguard-apply/SKILL.md +63 -27
  312. package/skills/torusguard-audit/SKILL.md +62 -50
  313. package/skills/torusguard-authorize/SKILL.md +7 -2
  314. package/skills/torusguard-exploit-check/SKILL.md +7 -2
  315. package/skills/torusguard-full/SKILL.md +7 -2
  316. package/skills/torusguard-harden/SKILL.md +71 -40
  317. package/skills/torusguard-init/SKILL.md +45 -30
  318. package/skills/torusguard-recheck/SKILL.md +59 -32
  319. package/skills/torusguard-report/SKILL.md +52 -31
  320. package/skills/torusguard-status/SKILL.md +40 -21
  321. package/skills/torusguard-verify/SKILL.md +7 -2
  322. package/skills/torusguard-web-validate/SKILL.md +7 -2
@@ -1,550 +1,1306 @@
1
- #!/usr/bin/env python3
2
- """
3
- TorusGuard Autonomous Static Security Audit Engine (v1.3.0)
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
- # ─── ANSI Colors & Formatter ───────────────────────────────────────────────────
43
- BOLD = "\033[1m"
44
- DIM = "\033[2m"
45
- RESET = "\033[0m"
46
- CYAN = "\033[36m"
47
- GREEN = "\033[32m"
48
- YELLOW = "\033[33m"
49
- WHITE = "\033[97m"
50
- GRAY = "\033[90m"
51
- RED = "\033[31m"
52
- MAGENTA = "\033[35m"
53
-
54
- ANSI_REGEX = re.compile(r'\033\[[0-9;]*m')
55
-
56
- def get_visual_width(text: str) -> int:
57
- """Calculate the printable display width of a string (ANSI and emoji aware)."""
58
- clean = ANSI_REGEX.sub('', text)
59
- width = 0
60
- for ch in clean:
61
- ea = unicodedata.east_asian_width(ch)
62
- if ea in ('W', 'F'):
63
- width += 2
64
- elif ord(ch) >= 0x1F300:
65
- width += 2
66
- else:
67
- width += 1
68
- return width
69
-
70
- def format_box_line(content: str, width: int = 71, border: str = "│", border_color: str = CYAN) -> str:
71
- """Pad content so the right border is consistently placed at visual column 75."""
72
- vis = get_visual_width(content)
73
- pad = " " * max(0, width - vis)
74
- return f" {border_color}{border}{RESET} {content}{pad}{border_color}{border}{RESET}"
75
-
76
- # ─── Rule Definitions & Sinks ─────────────────────────────────────────────────
77
- class RulePattern(TypedDict):
78
- rule_id: str
79
- title: str
80
- severity: str
81
- category: str
82
- cluster: str
83
- patterns: List[Tuple[Pattern[str], str]]
84
-
85
-
86
- RULE_PATTERNS: List[RulePattern] = [
87
- # ── Secrets & Credentials (TG-SEC) ──────────────────────────────────────────
88
- {
89
- "rule_id": "TG-SEC-001",
90
- "title": "Hardcoded Secret or API Key in Tracked Source",
91
- "severity": "Critical",
92
- "category": "secrets",
93
- "cluster": "cluster-credentials-exposure",
94
- "patterns": [
95
- (re.compile(r'\b(?:sk_live|ak_live)_[0-9a-zA-Z]{20,}\b'), "Live payment or cloud API secret key"),
96
- (re.compile(r'\bgh[pousr]_[0-9a-zA-Z]{36}\b'), "GitHub personal access token"),
97
- (re.compile(r'-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----'), "Hardcoded private key block"),
98
- (re.compile(r'\b(?:jwt_secret|jwtSecret|JWT_SECRET)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{16,}["\']'), "Hardcoded JWT signing secret"),
99
- (re.compile(r'(?:password|passwd|api_key|apiKey|secret_key)\s*[:=]\s*["\'][A-Za-z0-9@#$%^&+=_\-]{10,}["\']', re.IGNORECASE), "Hardcoded credential or API secret string")
100
- ]
101
- },
102
- {
103
- "rule_id": "TG-SEC-002",
104
- "title": "Public Client Environment Variable Secret Exposure",
105
- "severity": "High",
106
- "category": "secrets",
107
- "cluster": "cluster-credentials-exposure",
108
- "patterns": [
109
- (re.compile(r'\b(?:NEXT_PUBLIC_|VITE_|REACT_APP_)[A-Z0-9_]*(?:SECRET|PRIVATE|KEY|PASSWORD|TOKEN)\b'), "Client-exposed environment variable containing secret naming pattern")
110
- ]
111
- },
112
- # ── Database Safety & Multi-Tenant Query Isolation (TG-DB) ────────────────
113
- {
114
- "rule_id": "TG-DB-004",
115
- "title": "Missing Multi-Tenant Isolation in Scoped Query",
116
- "severity": "High",
117
- "category": "database",
118
- "cluster": "cluster-tenant-isolation",
119
- "patterns": [
120
- (re.compile(r'\.objects\.get\s*\(\s*id\s*=\s*[^,\)]+\)'), "Django ORM .get(id=...) query lacking explicit tenant scope filter"),
121
- (re.compile(r'\.findById\s*\(\s*(?:req\.params|id)[^\)]*\)'), "Direct findById query lacking organization/tenant boundaries")
122
- ]
123
- },
124
- {
125
- "rule_id": "TG-DB-001",
126
- "title": "Frontend / Client-Side Direct Database Query",
127
- "severity": "Critical",
128
- "category": "database",
129
- "cluster": "cluster-client-leak",
130
- "patterns": [
131
- (re.compile(r'\b(?:createClient|supabase)\.from\([^)]+\)\.delete\(\)'), "Dangerous direct database mutation in client component"),
132
- (re.compile(r'\bimport\s+.*(?:pg|mysql2|prisma|typeorm).*\s+from'), "Server database driver imported in client file")
133
- ]
134
- },
135
- # ── Input Validation & Code / SQL Injection (TG-INPUT) ─────────────────────
136
- {
137
- "rule_id": "TG-INPUT-002",
138
- "title": "Unescaped Raw SQL Query Concatenation",
139
- "severity": "Critical",
140
- "category": "injection",
141
- "cluster": "cluster-injection",
142
- "patterns": [
143
- (re.compile(r'(?:query|execute|raw)\s*\(\s*f["\'].*SELECT.*\{', re.IGNORECASE), "Python f-string interpolation in raw SQL query"),
144
- (re.compile(r'(?:query|execute|raw)\s*\(\s*[`"\'].*SELECT.*\$\{', re.IGNORECASE), "JavaScript template literal interpolation in SQL query"),
145
- (re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query")
146
- ]
147
- },
148
- {
149
- "rule_id": "TG-INPUT-003",
150
- "title": "Unsafe Dynamic Code or HTML Execution",
151
- "severity": "High",
152
- "category": "injection",
153
- "cluster": "cluster-injection",
154
- "patterns": [
155
- (re.compile(r'\beval\s*\('), "Direct eval() execution on untrusted input"),
156
- (re.compile(r'\bexec\s*\('), "Direct exec() execution on untrusted input"),
157
- (re.compile(r'\bdangerouslySetInnerHTML\s*='), "React dangerouslySetInnerHTML rendering raw unescaped HTML"),
158
- (re.compile(r'\.innerHTML\s*='), "Direct DOM innerHTML assignment vulnerable to XSS")
159
- ]
160
- },
161
- {
162
- "rule_id": "TG-INPUT-006",
163
- "title": "Unvalidated User Input in File Path Resolution",
164
- "severity": "High",
165
- "category": "filesystem",
166
- "cluster": "cluster-injection",
167
- "patterns": [
168
- (re.compile(r'(?:open|readFile|readFileSync)\s*\([^)]*\+\s*(?:req|params|query)'), "File read operation using unsanitized user request path")
169
- ]
170
- },
171
- # ── Authentication & Session Safety (TG-AUTH) ──────────────────────────────
172
- {
173
- "rule_id": "TG-AUTH-004",
174
- "title": "Insecure Session Cookie or Disabled CSRF Protection",
175
- "severity": "High",
176
- "category": "auth",
177
- "cluster": "cluster-auth-bypass",
178
- "patterns": [
179
- (re.compile(r'\bSESSION_COOKIE_SECURE\s*=\s*False\b'), "Session cookie secure flag disabled in Django settings"),
180
- (re.compile(r'\bsecure\s*:\s*false\b', re.IGNORECASE), "Cookie secure attribute set to false"),
181
- (re.compile(r'\bhttpOnly\s*:\s*false\b', re.IGNORECASE), "Cookie httpOnly attribute set to false (XSS readable)"),
182
- (re.compile(r'@(?:csrf_exempt|allow_anonymous)\b'), "Decorator disabling route-level authentication or CSRF protection"),
183
- (re.compile(r'\[(?:AllowAnonymous)\]'), "Attribute disabling ASP.NET authentication"),
184
- (re.compile(r'\.csrf\(\)\.disable\(\)'), "Spring Security CSRF protection explicitly disabled")
185
- ]
186
- },
187
- # ── Security Bypasses & Transport Invariants (TG-DIFF / TG-PLATFORM) ───────
188
- {
189
- "rule_id": "TG-DIFF-001",
190
- "title": "Disabled TLS / SSL Certificate Validation",
191
- "severity": "Critical",
192
- "category": "transport",
193
- "cluster": "cluster-transport-security",
194
- "patterns": [
195
- (re.compile(r'\bverify\s*=\s*False\b'), "Disabled TLS certificate verification (verify=False)"),
196
- (re.compile(r'\bInsecureSkipVerify\s*:\s*true\b'), "Disabled Go TLS certificate verification (InsecureSkipVerify: true)"),
197
- (re.compile(r'\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*[\'"]?0[\'"]?'), "Disabled Node.js TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0)"),
198
- (re.compile(r'\brejectUnauthorized\s*:\s*false\b'), "Disabled Node.js TLS rejectUnauthorized"),
199
- (re.compile(r'\bCURLOPT_SSL_VERIFYPEER\s*(?:=>|,)\s*(?:false|0)\b', re.IGNORECASE), "Disabled PHP curl SSL verification")
200
- ]
201
- },
202
- {
203
- "rule_id": "TG-PLATFORM-001",
204
- "title": "Wildcard Permissive CORS Configuration with Credentials",
205
- "severity": "High",
206
- "category": "platform",
207
- "cluster": "cluster-auth-bypass",
208
- "patterns": [
209
- (re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled across entire application"),
210
- (re.compile(r'@CrossOrigin\s*\(\s*["\']\*["\']\s*\)'), "Wildcard CORS origin annotation in Java/Spring")
211
- ]
212
- }
213
- ]
214
-
215
- # File extensions to scan
216
- SOURCE_EXTENSIONS = {
217
- ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
218
- ".go", ".java", ".cs", ".php", ".rb", ".json", ".yaml", ".yml"
219
- }
220
-
221
- # Directories to skip
222
- SKIP_DIRS = {
223
- ".git", "node_modules", ".venv", "venv", "env", "__pycache__",
224
- "dist", "build", ".next", ".nuxt", "coverage", ".torusguard",
225
- ".idea", ".vscode", "target", "bin", "obj"
226
- }
227
-
228
- def is_test_file(path_str: str) -> bool:
229
- """Check if file belongs to tests, fixtures, or mocks."""
230
- clean = path_str.replace("\\", "/").lower()
231
- markers = ["/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
232
- "__tests__", "test_", "_test.", ".test.", ".spec."]
233
- return any(m in clean for m in markers)
234
-
235
-
236
- def find_files_to_scan(target_root: Path) -> List[Path]:
237
- """Recursively collect source files eligible for static security audit."""
238
- files = []
239
- for root, dirs, filenames in os.walk(target_root):
240
- # Mutate dirs in-place to skip ignored folders
241
- dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
242
- for f in filenames:
243
- ext = os.path.splitext(f)[1].lower()
244
- if ext in SOURCE_EXTENSIONS:
245
- files.append(Path(root) / f)
246
- return sorted(files)
247
-
248
-
249
- def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
250
- """Scan a single source file against the TorusGuard rule families."""
251
- findings = []
252
- try:
253
- content = file_path.read_text(encoding="utf-8", errors="replace")
254
- except Exception:
255
- return findings
256
-
257
- lines = content.splitlines()
258
- rel_path = str(file_path.relative_to(target_root)).replace("\\", "/")
259
- is_test = is_test_file(rel_path)
260
-
261
- for rule in RULE_PATTERNS:
262
- rule_id = rule["rule_id"]
263
- patterns: List[Tuple[Pattern[str], str]] = rule["patterns"]
264
- for regex, desc in patterns:
265
- for idx, line in enumerate(lines):
266
- stripped = line.strip()
267
- if stripped.startswith(("#", "//", "/*", "*")):
268
- if "bypass" not in stripped.lower() and "todo" not in stripped.lower():
269
- continue
270
-
271
- match = regex.search(line)
272
- if match:
273
- line_num = idx + 1
274
- seed = f"{rule_id}:{rel_path}:{line_num}:{stripped}"
275
- fp = f"{rule_id}-{hashlib.sha256(seed.encode('utf-8')).hexdigest()[:8]}"
276
-
277
- start_ctx = max(0, idx - 2)
278
- end_ctx = min(len(lines), idx + 3)
279
- snippet = "\n".join(lines[start_ctx:end_ctx])
280
-
281
- findings.append({
282
- "finding_id": fp,
283
- "rule_id": rule_id,
284
- "title": rule["title"],
285
- "description": desc,
286
- "severity": "Low" if is_test else rule["severity"],
287
- "category": rule["category"],
288
- "cluster": rule["cluster"],
289
- "file_path": rel_path,
290
- "line_number": line_num,
291
- "matched_text": match.group(0)[:80],
292
- "snippet": snippet,
293
- "is_test": is_test,
294
- "target": {
295
- "file_path": rel_path,
296
- "start_line": line_num,
297
- "end_line": line_num
298
- }
299
- })
300
- break
301
- return findings
302
-
303
-
304
- def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
305
- """
306
- Score each finding using finding_scorer.py and persistent memory patterns.
307
- Returns (scored_findings, clusters_map).
308
- """
309
- scripts_dir = Path(__file__).resolve().parent
310
- if str(scripts_dir) not in sys.path:
311
- sys.path.insert(0, str(scripts_dir))
312
- try:
313
- import finding_scorer
314
- except Exception:
315
- finding_scorer = None
316
-
317
- scored = []
318
- clusters: Dict[str, List[Dict[str, Any]]] = {}
319
-
320
- for f in findings:
321
- rule_id = f["rule_id"]
322
- file_path = f["file_path"]
323
-
324
- if finding_scorer and hasattr(finding_scorer, "compute_confidence_score"):
325
- total, band, factors = finding_scorer.compute_confidence_score(
326
- evidence_quality=35,
327
- reproduction_success=0,
328
- independent_confirmations=5,
329
- environmental_clarity=15,
330
- manual_review_status=0,
331
- rule_id=rule_id,
332
- file_path=file_path,
333
- root_dir=target_root
334
- )
335
- else:
336
- total = 70
337
- band = "High Confidence"
338
- factors = {"total_score": total, "classification_band": band}
339
-
340
- f["confidence_score"] = total
341
- f["confidence_band"] = band
342
- f["confidence_factors"] = factors
343
-
344
- scored.append(f)
345
- cluster_id = f.get("cluster", "cluster-general")
346
- clusters.setdefault(cluster_id, []).append(f)
347
-
348
- return scored, clusters
349
-
350
-
351
- def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], target_root: Path) -> None:
352
- """Write findings.json, findings.md, and summary.md into isolated run directory."""
353
- run_folder.mkdir(parents=True, exist_ok=True)
354
-
355
- # 1. findings.json
356
- findings_file = run_folder / "findings.json"
357
- with open(findings_file, "w", encoding="utf-8") as f:
358
- json.dump(scored_findings, f, indent=2)
359
-
360
- # 2. findings.md
361
- now_ist = get_ist_now()
362
- md_lines = [
363
- f"# TorusGuard Static Security Audit Report",
364
- f"**Run ID:** `{run_folder.name}` ",
365
- f"**Timestamp:** `{now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}` ",
366
- f"**Target:** `{target_root}` ",
367
- f"**Total Findings:** `{len(scored_findings)}` \n",
368
- "---",
369
- "## Findings Catalog\n"
370
- ]
371
-
372
- for item in scored_findings:
373
- md_lines.append(f"### [{item['rule_id']}] {item['title']}")
374
- md_lines.append(f"- **Severity:** `{item['severity']}` | **Confidence:** `{item['confidence_score']}/100 ({item['confidence_band']})`")
375
- md_lines.append(f"- **File:** `{item['file_path']}:{item['line_number']}` | **Cluster:** `{item['cluster']}`")
376
- md_lines.append(f"- **Description:** {item['description']}")
377
- md_lines.append("```")
378
- code_snippet = str(item.get("snippet") or item.get("matched_text") or "")
379
- md_lines.append(code_snippet)
380
- md_lines.append("```\n")
381
-
382
- (run_folder / "findings.md").write_text("\n".join(md_lines), encoding="utf-8")
383
-
384
- # 3. summary.md
385
- summary_lines = [
386
- f"# TorusGuard Audit Summary: {run_folder.name}",
387
- f"- Scanned Scope: {target_root}",
388
- f"- Total Findings: {len(scored_findings)}",
389
- f"- Root-Cause Clusters: {len(clusters)}",
390
- f"- Generated: {now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}"
391
- ]
392
- (run_folder / "summary.md").write_text("\n".join(summary_lines), encoding="utf-8")
393
-
394
- # 4. Update manifest.json status
395
- manifest_file = run_folder / "manifest.json"
396
- if manifest_file.exists():
397
- try:
398
- with open(manifest_file, "r", encoding="utf-8") as mf:
399
- mdata = json.load(mf)
400
- mdata["status"] = "completed"
401
- mdata["findings_count"] = len(scored_findings)
402
- with open(manifest_file, "w", encoding="utf-8") as mf:
403
- json.dump(mdata, mf, indent=2)
404
- except Exception:
405
- pass
406
-
407
-
408
- 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:
409
- """Print the unified 75-column TorusGuard Static Audit results card."""
410
- crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
411
- high_count = sum(1 for f in scored_findings if f["severity"] == "High")
412
- med_count = sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low"))
413
-
414
- stack_str = "Auto-detected"
415
- if detected_stack and detected_stack.get("framework") and detected_stack.get("framework") != "None":
416
- stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
417
-
418
- print(f"""
419
- {CYAN}╭─────────────────────────────────────────────────────────────────────────╮{RESET}
420
- {CYAN}│{RESET} {CYAN}│{RESET}
421
- {CYAN}│{RESET} {BOLD}{WHITE}🛡️ TORUSGUARD STATIC SECURITY AUDIT{RESET} {GRAY}v1.3.0{RESET} {CYAN}│{RESET}
422
- {CYAN}│{RESET} {DIM}Autonomous AST & Invariant Security Scanner{RESET} {CYAN}│{RESET}
423
- {CYAN}│{RESET} {CYAN}│{RESET}
424
- {CYAN}╰─────────────────────────────────────────────────────────────────────────╯{RESET}
425
- """)
426
-
427
- print(f" {CYAN}┌─ {BOLD}Audit Execution Scope{RESET} {CYAN}─────────────────────────────────────────────────┐{RESET}")
428
- print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root):<56}{RESET}"))
429
- print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str:<56}{RESET}"))
430
- print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 11 canonical families{RESET}"))
431
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
432
-
433
- # Findings Box
434
- status_icon = f"{RED}✖ CRITICAL FINDINGS DETECTED{RESET}" if crit_count > 0 else (f"{YELLOW}⚠ HIGH VULNERABILITIES DETECTED{RESET}" if high_count > 0 else f"{GREEN}✔ SECURITY POSTURE CLEAN{RESET}")
435
- print(f" {CYAN}┌─ {BOLD}Findings & Confidence Scores{RESET} {CYAN}─────────────────────────────────────────┐{RESET}")
436
- print(format_box_line(f"Status: {status_icon}"))
437
- print(format_box_line(f"Findings: {RED}{crit_count} Critical{RESET} {YELLOW}{high_count} High{RESET} {CYAN}{med_count} Medium/Low{RESET} ({len(scored_findings)} total)"))
438
- print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
439
- print(f" {CYAN}├─────────────────────────────────────────────────────────────────────────┤{RESET}")
440
-
441
- if not scored_findings:
442
- print(format_box_line(f"{GREEN}✔ Zero security invariant violations detected.{RESET}"))
443
- else:
444
- for idx, f in enumerate(scored_findings[:5]):
445
- sev_color = RED if f["severity"] == "Critical" else (YELLOW if f["severity"] == "High" else CYAN)
446
- line_str = f"[{sev_color}{f['severity']}{RESET}] {BOLD}{f['rule_id']}{RESET} at {f['file_path']}:{f['line_number']}"
447
- print(format_box_line(line_str))
448
- desc_str = f" └─ {DIM}{f['description'][:60]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
449
- print(format_box_line(desc_str))
450
- if len(scored_findings) > 5:
451
- print(format_box_line(f"{DIM}... and {len(scored_findings) - 5} more findings in report file{RESET}"))
452
-
453
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
454
-
455
- # Run Artifacts & Next Steps Box
456
- rel_run = str(run_folder.relative_to(target_root)) if str(run_folder).startswith(str(target_root)) else str(run_folder)
457
- print(f" {GREEN}╔═ {BOLD}Artifacts & Next Actions{RESET} {GREEN}══════════════════════════════════════════════════╗{RESET}")
458
- print(format_box_line(f"{BOLD}Run Artifact:{RESET} {WHITE}{rel_run}/findings.md{RESET}", border="║", border_color=GREEN))
459
- print(format_box_line(f"{BOLD}Visual Dash:{RESET} {CYAN}npx torusguard report --html{RESET}", border="║", border_color=GREEN))
460
- print(format_box_line(f"{BOLD}Surgical Fix:{RESET} In AI Chat, run {CYAN}/torusguard-harden{RESET}", border="║", border_color=GREEN))
461
- print(f" {GREEN}╚═════════════════════════════════════════════════════════════════════════╝{RESET}\n")
462
-
463
-
464
- def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False) -> Dict[str, Any]:
465
- """Execute the full TorusGuard static security audit."""
466
- target_root = target_root.resolve()
467
- scripts_dir = Path(__file__).resolve().parent
468
-
469
- # 1. Stack detection
470
- detected_stack = None
471
- stack_script = scripts_dir / "stack_detect.py"
472
- if stack_script.is_file():
473
- try:
474
- import importlib.util
475
- spec = importlib.util.spec_from_file_location("stack_detect", str(stack_script))
476
- if spec and spec.loader:
477
- sd = importlib.util.module_from_spec(spec)
478
- spec.loader.exec_module(sd)
479
- if hasattr(sd, "detect_stack"):
480
- detected_stack = sd.detect_stack(target_root)
481
- except Exception:
482
- pass
483
-
484
- # 2. Collect files & scan
485
- files = find_files_to_scan(target_root)
486
- all_findings = []
487
- for f in files:
488
- all_findings.extend(scan_file(f, target_root))
489
-
490
- # 3. Score & Cluster
491
- scored_findings, clusters = score_and_cluster_findings(all_findings, target_root)
492
-
493
- # 4. Allocate run folder
494
- runs_dir = target_root / ".torusguard" / "runs"
495
- runs_dir.mkdir(parents=True, exist_ok=True)
496
-
497
- if str(scripts_dir) not in sys.path:
498
- sys.path.insert(0, str(scripts_dir))
499
- try:
500
- import run_manager
501
- run_folder = run_manager.create_run(runs_dir, "audit", target_root.name)
502
- except Exception:
503
- timestamp = get_ist_now().strftime("%Y%m%d-%H%M%S")
504
- run_folder = runs_dir / f"run-{timestamp}-audit"
505
- run_folder.mkdir(parents=True, exist_ok=True)
506
-
507
- # 5. Emit artifacts & sync memory
508
- emit_run_artifacts(run_folder, scored_findings, clusters, target_root)
509
- try:
510
- if "run_manager" in sys.modules:
511
- sys.modules["run_manager"].sync_run_to_memory(run_folder, scored_findings)
512
- except Exception:
513
- pass
514
-
515
- result = {
516
- "status": "completed",
517
- "target_root": str(target_root),
518
- "files_scanned": len(files),
519
- "total_findings": len(scored_findings),
520
- "critical_count": sum(1 for f in scored_findings if f["severity"] == "Critical"),
521
- "high_count": sum(1 for f in scored_findings if f["severity"] == "High"),
522
- "medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
523
- "clusters_count": len(clusters),
524
- "run_folder": str(run_folder),
525
- "findings": scored_findings
526
- }
527
-
528
- if json_output:
529
- print(json.dumps(result, indent=2))
530
- else:
531
- print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
532
-
533
- return result
534
-
535
-
536
- def main():
537
- parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
538
- parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
539
- parser.add_argument("--scope", "-s", help="Alternative path to target project")
540
- parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
541
- parser.add_argument("--json", action="store_true", help="Output raw JSON")
542
- args = parser.parse_args()
543
-
544
- target = Path(args.scope or args.path).resolve()
545
- res = execute_audit(target, severity_floor=args.severity, json_output=args.json)
546
- sys.exit(0 if res["critical_count"] == 0 else 1)
547
-
548
-
549
- if __name__ == "__main__":
550
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Autonomous Static Security Audit Engine (v1.3.5)
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.5", 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
+ # File extensions to scan
952
+ SOURCE_EXTENSIONS = {
953
+ ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
954
+ ".go", ".java", ".cs", ".php", ".rb", ".json", ".yaml", ".yml"
955
+ }
956
+
957
+ # Directories to skip
958
+ SKIP_DIRS = {
959
+ ".git", "node_modules", ".venv", "venv", "env", "__pycache__",
960
+ "dist", "build", ".next", ".nuxt", "coverage", ".torusguard",
961
+ ".idea", ".vscode", "target", "bin", "obj", "payload"
962
+ }
963
+
964
+ def is_test_file(path_str: str) -> bool:
965
+ """Check if file belongs to tests, fixtures, or test harnesses."""
966
+ clean = path_str.replace("\\", "/").lower()
967
+ markers = ["/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
968
+ "/harness/", "__tests__", "test_", "_test.", ".test.", ".spec."]
969
+ return any(m in clean for m in markers)
970
+
971
+
972
+ def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[Path]:
973
+ """Recursively collect source files eligible for static security audit."""
974
+ files = []
975
+ for root, dirs, filenames in os.walk(target_root):
976
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
977
+ for f in filenames:
978
+ ext = os.path.splitext(f)[1].lower()
979
+ if ext in SOURCE_EXTENSIONS:
980
+ 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"):
981
+ continue
982
+ full_path = Path(root) / f
983
+ if not include_tests and is_test_file(str(full_path)):
984
+ continue
985
+ files.append(full_path)
986
+ return sorted(files)
987
+
988
+
989
+ def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
990
+ """Scan a single source file against the TorusGuard rule families."""
991
+ findings = []
992
+ try:
993
+ content = file_path.read_text(encoding="utf-8", errors="replace")
994
+ except Exception:
995
+ return findings
996
+
997
+ lines = content.splitlines()
998
+ rel_path = str(file_path.relative_to(target_root)).replace("\\", "/")
999
+ is_test = is_test_file(rel_path)
1000
+
1001
+ for rule in RULE_PATTERNS:
1002
+ rule_id = rule["rule_id"]
1003
+ if rule_id == "TG-PLATFORM-002" and "helmet" in content and "app.use(helmet" in content:
1004
+ continue
1005
+ patterns: List[Tuple[Pattern[str], str]] = rule["patterns"]
1006
+ for regex, desc in patterns:
1007
+ for idx, line in enumerate(lines):
1008
+ stripped = line.strip()
1009
+ if stripped.startswith(("#", "//", "/*", "*")):
1010
+ if "bypass" not in stripped.lower() and "todo" not in stripped.lower():
1011
+ continue
1012
+
1013
+ match = regex.search(line)
1014
+ if match:
1015
+ line_num = idx + 1
1016
+ seed = f"{rule_id}:{rel_path}:{line_num}:{stripped}"
1017
+ fp = f"{rule_id}-{hashlib.sha256(seed.encode('utf-8')).hexdigest()[:8]}"
1018
+
1019
+ start_ctx = max(0, idx - 2)
1020
+ end_ctx = min(len(lines), idx + 3)
1021
+ snippet = "\n".join(lines[start_ctx:end_ctx])
1022
+
1023
+ findings.append({
1024
+ "finding_id": fp,
1025
+ "rule_id": rule_id,
1026
+ "title": rule["title"],
1027
+ "description": desc,
1028
+ "severity": "Low" if is_test else rule["severity"],
1029
+ "category": rule["category"],
1030
+ "cluster": rule["cluster"],
1031
+ "file_path": rel_path,
1032
+ "line_number": line_num,
1033
+ "matched_text": match.group(0)[:80],
1034
+ "snippet": snippet,
1035
+ "is_test": is_test,
1036
+ "target": {
1037
+ "file_path": rel_path,
1038
+ "start_line": line_num,
1039
+ "end_line": line_num
1040
+ }
1041
+ })
1042
+ break
1043
+ return findings
1044
+
1045
+
1046
+ def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
1047
+ """
1048
+ Score each finding using finding_scorer.py and persistent memory patterns.
1049
+ Returns (scored_findings, clusters_map).
1050
+ """
1051
+ try:
1052
+ import finding_scorer
1053
+ except Exception:
1054
+ finding_scorer = None
1055
+
1056
+ scored = []
1057
+ clusters: Dict[str, List[Dict[str, Any]]] = {}
1058
+
1059
+ for f in findings:
1060
+ score = 70
1061
+ band = "High Confidence"
1062
+ factors = {}
1063
+
1064
+ if finding_scorer:
1065
+ try:
1066
+ eq = 35
1067
+ rs = 0
1068
+ ic = 5
1069
+ ec = 15
1070
+ mr = 0
1071
+ s, b, facts = finding_scorer.compute_confidence_score(
1072
+ evidence_quality=eq,
1073
+ reproduction_success=rs,
1074
+ independent_confirmations=ic,
1075
+ environmental_clarity=ec,
1076
+ manual_review_status=mr,
1077
+ rule_id=f["rule_id"],
1078
+ file_path=f["file_path"],
1079
+ root_dir=target_root
1080
+ )
1081
+ score = s
1082
+ band = b
1083
+ factors = facts
1084
+ except Exception:
1085
+ pass
1086
+
1087
+ f["confidence_score"] = score
1088
+ f["confidence_band"] = band
1089
+ f["confidence_factors"] = factors
1090
+ scored.append(f)
1091
+
1092
+ c_id = f["cluster"]
1093
+ if c_id not in clusters:
1094
+ clusters[c_id] = []
1095
+ clusters[c_id].append(f)
1096
+
1097
+ # Sort descending by severity (Critical -> High -> Medium -> Low), then score
1098
+ sev_rank = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1}
1099
+ scored.sort(key=lambda x: (sev_rank.get(x["severity"], 0), x["confidence_score"]), reverse=True)
1100
+ return scored, clusters
1101
+
1102
+
1103
+ def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], target_root: Path) -> None:
1104
+ """Generate findings.json, findings.md, and summary.md into run folder."""
1105
+ now_ist = get_ist_now()
1106
+
1107
+ # 1. findings.json
1108
+ (run_folder / "findings.json").write_text(json.dumps(scored_findings, indent=2), encoding="utf-8")
1109
+
1110
+ # 2. findings.md
1111
+ md_lines = [
1112
+ f"# TorusGuard Security Audit Findings: {run_folder.name}",
1113
+ f"- Target Root: `{target_root}`",
1114
+ f"- Total Scored Findings: `{len(scored_findings)}`",
1115
+ f"- Identified Clusters: `{len(clusters)}`",
1116
+ f"- Generated At: `{now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}`\n",
1117
+ "---",
1118
+ "## Summary of Findings\n"
1119
+ ]
1120
+
1121
+ for f in scored_findings:
1122
+ md_lines.append(f"### [{f['severity']}] {f['rule_id']} - {f['title']}")
1123
+ md_lines.append(f"- **Finding ID:** `{f['finding_id']}`")
1124
+ md_lines.append(f"- **Location:** `{f['file_path']}:{f['line_number']}`")
1125
+ md_lines.append(f"- **Confidence Score:** `{f['confidence_score']}/100` ({f['confidence_band']})")
1126
+ md_lines.append(f"- **Cluster:** `{f['cluster']}`")
1127
+ md_lines.append(f"- **Description:** {f['description']}")
1128
+ md_lines.append("```")
1129
+ md_lines.append(f['snippet'])
1130
+ md_lines.append("```\n")
1131
+
1132
+ (run_folder / "findings.md").write_text("\n".join(md_lines), encoding="utf-8")
1133
+
1134
+ # 3. summary.md
1135
+ summary_lines = [
1136
+ f"# TorusGuard Audit Summary: {run_folder.name}",
1137
+ f"- Scanned Scope: {target_root}",
1138
+ f"- Total Findings: {len(scored_findings)}",
1139
+ f"- Root-Cause Clusters: {len(clusters)}",
1140
+ f"- Generated: {now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}"
1141
+ ]
1142
+ (run_folder / "summary.md").write_text("\n".join(summary_lines), encoding="utf-8")
1143
+
1144
+ # 4. Update manifest.json status
1145
+ manifest_file = run_folder / "manifest.json"
1146
+ if manifest_file.exists():
1147
+ try:
1148
+ with open(manifest_file, "r", encoding="utf-8") as mf:
1149
+ mdata = json.load(mf)
1150
+ mdata["status"] = "completed"
1151
+ mdata["findings_count"] = len(scored_findings)
1152
+ with open(manifest_file, "w", encoding="utf-8") as mf:
1153
+ json.dump(mdata, mf, indent=2)
1154
+ except Exception:
1155
+ pass
1156
+
1157
+
1158
+ def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], run_folder: Path, detected_stack: Optional[Dict[str, Any]] = None) -> None:
1159
+ """Print the unified 75-column TorusGuard Static Audit results card."""
1160
+ crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
1161
+ high_count = sum(1 for f in scored_findings if f["severity"] == "High")
1162
+ med_count = sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low"))
1163
+
1164
+ stack_str = "Auto-detected"
1165
+ if detected_stack and detected_stack.get("framework") and detected_stack.get("framework") != "None":
1166
+ stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
1167
+
1168
+ print()
1169
+ print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.3.5"))
1170
+ print()
1171
+
1172
+ print(card_border_top("Audit Execution Scope"))
1173
+ print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root)}{RESET}"))
1174
+ print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str}{RESET}"))
1175
+ print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 18 rule families (74 rules){RESET}"))
1176
+ print(card_border_bottom())
1177
+ print()
1178
+
1179
+ # Findings Box
1180
+ 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}")
1181
+ print(card_border_top("Findings & Confidence Scores"))
1182
+ print(format_box_line(f"Status: {status_icon}"))
1183
+ print(format_box_line(f"Findings: {RED}{crit_count} Critical{RESET} {YELLOW}{high_count} High{RESET} {CYAN}{med_count} Medium/Low{RESET} ({len(scored_findings)} total)"))
1184
+ print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
1185
+ print(card_divider())
1186
+
1187
+ if not scored_findings:
1188
+ print(format_box_line(f"{GREEN}✔ Zero security invariant violations detected.{RESET}"))
1189
+ else:
1190
+ for idx, f in enumerate(scored_findings[:5]):
1191
+ sev_color = RED if f["severity"] == "Critical" else (YELLOW if f["severity"] == "High" else CYAN)
1192
+ line_str = f"[{sev_color}{f['severity']}{RESET}] {BOLD}{f['rule_id']}{RESET} at {f['file_path']}:{f['line_number']}"
1193
+ print(format_box_line(line_str))
1194
+ desc_str = f" └─ {DIM}{f['description'][:58]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
1195
+ print(format_box_line(desc_str))
1196
+ if len(scored_findings) > 5:
1197
+ print(format_box_line(f"{DIM}... and {len(scored_findings) - 5} more findings in report file{RESET}"))
1198
+
1199
+ print(card_border_bottom())
1200
+ print()
1201
+
1202
+ # Run Artifacts & Next Steps Box
1203
+ rel_run = str(run_folder.relative_to(target_root)).replace("\\", "/") if str(run_folder).startswith(str(target_root)) else str(run_folder).replace("\\", "/")
1204
+ findings_artifact = f"{rel_run}/findings.md"
1205
+ print(card_border_top("Artifacts & Next Actions", border_color=GREEN, double=True))
1206
+ print(format_box_line(f"{BOLD}Run Artifact:{RESET} {WHITE}{findings_artifact}{RESET}", border="║", border_color=GREEN))
1207
+ print(format_box_line(f"{BOLD}Living Ledger:{RESET} {CYAN}security_report.md (updated){RESET}", border="║", border_color=GREEN))
1208
+ print(format_box_line(f"{BOLD}Visual Dash:{RESET} {CYAN}npx torusguard report --html{RESET}", border="║", border_color=GREEN))
1209
+ print(format_box_line(f"{BOLD}Surgical Fix:{RESET} In AI Chat, run {CYAN}/torusguard-harden{RESET}", border="║", border_color=GREEN))
1210
+ print(card_border_bottom(border_color=GREEN, double=True))
1211
+ print()
1212
+
1213
+
1214
+ def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
1215
+ """Execute the full TorusGuard static security audit."""
1216
+ target_root = target_root.resolve()
1217
+ s_dir = Path(__file__).resolve().parent
1218
+
1219
+ # 1. Stack detection
1220
+ detected_stack = None
1221
+ stack_script = s_dir / "stack_detect.py"
1222
+ if stack_script.is_file():
1223
+ try:
1224
+ import importlib.util
1225
+ spec = importlib.util.spec_from_file_location("stack_detect", str(stack_script))
1226
+ if spec and spec.loader:
1227
+ sd = importlib.util.module_from_spec(spec)
1228
+ spec.loader.exec_module(sd)
1229
+ if hasattr(sd, "detect_stack"):
1230
+ detected_stack = sd.detect_stack(target_root)
1231
+ except Exception:
1232
+ pass
1233
+
1234
+ # 2. Collect files & scan
1235
+ files = find_files_to_scan(target_root, include_tests=include_tests)
1236
+ all_findings = []
1237
+ for f in files:
1238
+ all_findings.extend(scan_file(f, target_root))
1239
+
1240
+ # 3. Score & Cluster
1241
+ scored_findings, clusters = score_and_cluster_findings(all_findings, target_root)
1242
+
1243
+ # 4. Allocate run folder
1244
+ runs_dir = target_root / ".torusguard" / "runs"
1245
+ runs_dir.mkdir(parents=True, exist_ok=True)
1246
+
1247
+ if str(s_dir) not in sys.path:
1248
+ sys.path.insert(0, str(s_dir))
1249
+ try:
1250
+ import run_manager
1251
+ run_folder = run_manager.create_run(runs_dir, "audit", target_root.name)
1252
+ except Exception:
1253
+ timestamp = get_ist_now().strftime("%Y%m%d-%H%M%S")
1254
+ run_folder = runs_dir / f"run-{timestamp}-audit"
1255
+ run_folder.mkdir(parents=True, exist_ok=True)
1256
+
1257
+ # 5. Emit artifacts & sync memory
1258
+ emit_run_artifacts(run_folder, scored_findings, clusters, target_root)
1259
+ try:
1260
+ import report_sync
1261
+ report_sync.record_audit_findings(target_root, scored_findings, run_folder.name)
1262
+ except Exception:
1263
+ pass
1264
+ try:
1265
+ if "run_manager" in sys.modules:
1266
+ sys.modules["run_manager"].sync_run_to_memory(run_folder, scored_findings)
1267
+ except Exception:
1268
+ pass
1269
+
1270
+ result = {
1271
+ "status": "completed",
1272
+ "target_root": str(target_root),
1273
+ "files_scanned": len(files),
1274
+ "total_findings": len(scored_findings),
1275
+ "critical_count": sum(1 for f in scored_findings if f["severity"] == "Critical"),
1276
+ "high_count": sum(1 for f in scored_findings if f["severity"] == "High"),
1277
+ "medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
1278
+ "clusters_count": len(clusters),
1279
+ "run_folder": str(run_folder),
1280
+ "findings": scored_findings
1281
+ }
1282
+
1283
+ if json_output:
1284
+ print(json.dumps(result, indent=2))
1285
+ else:
1286
+ print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
1287
+
1288
+ return result
1289
+
1290
+
1291
+ def main():
1292
+ parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
1293
+ parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
1294
+ parser.add_argument("--scope", "-s", help="Alternative path to target project")
1295
+ parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
1296
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
1297
+ parser.add_argument("--include-tests", action="store_true", help="Include test files and harness fixtures in scan")
1298
+ args = parser.parse_args()
1299
+
1300
+ target = Path(args.scope or args.path).resolve()
1301
+ res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
1302
+ sys.exit(0 if res["critical_count"] == 0 else 1)
1303
+
1304
+
1305
+ if __name__ == "__main__":
1306
+ main()