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,375 +1,628 @@
1
- #!/usr/bin/env python3
2
- """
3
- TorusGuard Autonomous Remediation Engine (v1.3.0)
4
- Evaluates audit findings against canonical patch templates, enforces Ponytail bounds
5
- (<= 35 additions, <= 25 deletions), packages structured candidate remediation bundles,
6
- and renders unified patch diffs.
7
-
8
- Pure Python 3.10+ standard library (zero external dependencies).
9
- """
10
-
11
- import sys
12
- import os
13
- import re
14
- import json
15
- import difflib
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
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
-
30
- # ─── Timezone Configuration: Indian Standard Time (IST, UTC+05:30) ───────────
31
- IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
32
-
33
- def get_ist_now() -> datetime.datetime:
34
- return datetime.datetime.now(IST)
35
-
36
- # ─── ANSI Colors & Formatter ───────────────────────────────────────────────────
37
- BOLD = "\033[1m"
38
- DIM = "\033[2m"
39
- RESET = "\033[0m"
40
- CYAN = "\033[36m"
41
- GREEN = "\033[32m"
42
- YELLOW = "\033[33m"
43
- WHITE = "\033[97m"
44
- GRAY = "\033[90m"
45
- RED = "\033[31m"
46
-
47
- ANSI_REGEX = re.compile(r'\033\[[0-9;]*m')
48
-
49
- def get_visual_width(text: str) -> int:
50
- clean = ANSI_REGEX.sub('', text)
51
- width = 0
52
- for ch in clean:
53
- ea = unicodedata.east_asian_width(ch)
54
- if ea in ('W', 'F'):
55
- width += 2
56
- elif ord(ch) >= 0x1F300:
57
- width += 2
58
- else:
59
- width += 1
60
- return width
61
-
62
- def format_box_line(content: str, width: int = 71, border: str = "│", border_color: str = CYAN) -> str:
63
- vis = get_visual_width(content)
64
- pad = " " * max(0, width - vis)
65
- return f" {border_color}{border}{RESET} {content}{pad}{border_color}{border}{RESET}"
66
-
67
-
68
- def find_latest_audit_run(runs_dir: Path) -> Optional[Path]:
69
- """Locate the most recent audit run folder containing findings.json."""
70
- if not runs_dir.is_dir():
71
- return None
72
- runs = sorted(runs_dir.glob("run-*-audit"), key=lambda p: p.stat().st_mtime, reverse=True)
73
- for r in runs:
74
- if (r / "findings.json").is_file():
75
- return r
76
- return None
77
-
78
-
79
- def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Optional[Dict[str, Any]]:
80
- """
81
- Formulate a minimal, surgical fix for a finding adhering to Ponytail bounds
82
- (<= 35 additions, <= 25 deletions).
83
- """
84
- rule_id = finding.get("rule_id", "")
85
- file_path = finding.get("file_path", "")
86
- line_number = finding.get("line_number", 1)
87
- description = finding.get("description", "")
88
-
89
- full_target = target_root / file_path
90
- if not full_target.is_file():
91
- return None
92
-
93
- try:
94
- content = full_target.read_text(encoding="utf-8", errors="replace")
95
- except Exception:
96
- return None
97
-
98
- lines = content.splitlines()
99
- if not (1 <= line_number <= len(lines)):
100
- return None
101
-
102
- target_line = lines[line_number - 1]
103
- indent = re.match(r'^\s*', target_line).group(0) # type: ignore
104
- new_lines = list(lines)
105
- applied_rule = False
106
- fix_explanation = ""
107
-
108
- # Strategy 1: Hardcoded Secrets (TG-SEC-001 / TG-SEC-002)
109
- if rule_id in ("TG-SEC-001", "TG-SEC-002"):
110
- if file_path.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs")):
111
- # const secret = "..." -> const secret = process.env.JWT_SECRET || "...";
112
- if re.search(r'(jwt_secret|jwtSecret|JWT_SECRET)', target_line, re.IGNORECASE):
113
- replacement = f'{indent}const secret = process.env.JWT_SECRET || "";'
114
- new_lines[line_number - 1] = replacement
115
- applied_rule = True
116
- fix_explanation = "Migrated hardcoded JWT secret to environment variable process.env.JWT_SECRET"
117
- elif re.search(r'(:=|=)\s*["\'][A-Za-z0-9_\-]{8,}["\']', target_line):
118
- var_match = re.search(r'(const|let|var)?\s*([A-Za-z0-9_]+)\s*[:=]', target_line)
119
- var_name = var_match.group(2) if var_match else "API_KEY"
120
- env_key = re.sub(r'(?<!^)(?=[A-Z])', '_', var_name).upper()
121
- replacement = f'{indent}const {var_name} = process.env.{env_key} || "";'
122
- new_lines[line_number - 1] = replacement
123
- applied_rule = True
124
- fix_explanation = f"Replaced hardcoded credential with process.env.{env_key}"
125
- elif file_path.endswith(".py"):
126
- if re.search(r'(:=|=)\s*["\'][A-Za-z0-9_\-]{8,}["\']', target_line):
127
- var_match = re.search(r'([A-Za-z0-9_]+)\s*=', target_line)
128
- var_name = var_match.group(1) if var_match else "SECRET_KEY"
129
- env_key = var_name.upper()
130
- replacement = f'{indent}{var_name} = os.environ.get("{env_key}", "")'
131
- new_lines[line_number - 1] = replacement
132
- applied_rule = True
133
- fix_explanation = f"Replaced hardcoded credential with os.environ.get('{env_key}')"
134
-
135
- # Strategy 2: Dangerous React HTML rendering & DOM innerHTML (TG-INPUT-003)
136
- elif rule_id == "TG-INPUT-003":
137
- if "dangerouslySetInnerHTML" in target_line:
138
- # <div dangerouslySetInnerHTML={{ __html: bio }} /> -> <div>{bio}</div>
139
- var_match = re.search(r'__html\s*:\s*([A-Za-z0-9_]+)', target_line)
140
- var_name = var_match.group(1) if var_match else "content"
141
- replacement = f'{indent}<div>{{{var_name}}}</div>'
142
- new_lines[line_number - 1] = replacement
143
- applied_rule = True
144
- fix_explanation = "Replaced raw dangerouslySetInnerHTML injection with safe React text interpolation"
145
- elif ".innerHTML" in target_line:
146
- # Pattern A: el.innerHTML = '' or "" -> el.textContent = ""
147
- if re.search(r'\.innerHTML\s*=\s*["\']["\']', target_line):
148
- fixed_line = re.sub(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*["\']["\']', r'\1.textContent = ""', target_line)
149
- if fixed_line != target_line:
150
- new_lines[line_number - 1] = fixed_line
151
- applied_rule = True
152
- fix_explanation = "Replaced unsafe innerHTML DOM reset with safe textContent assignment"
153
- # Pattern B: el.innerHTML = someVar -> el.textContent = someVar
154
- elif re.search(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*([a-zA-Z0-9_$.]+)\s*;?$', target_line):
155
- fixed_line = re.sub(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*([a-zA-Z0-9_$.]+)', r'\1.textContent = \2', target_line)
156
- if fixed_line != target_line:
157
- new_lines[line_number - 1] = fixed_line
158
- applied_rule = True
159
- fix_explanation = "Replaced raw DOM innerHTML assignment with XSS-safe textContent"
160
-
161
- # Strategy 3: Raw SQL Concatenation (TG-INPUT-002)
162
- elif rule_id == "TG-INPUT-002":
163
- if file_path.endswith(".py") and ("f\"SELECT" in target_line or "f'SELECT" in target_line):
164
- # Replace f-string injection with parameterized query
165
- fixed_line = re.sub(r'f(["\']SELECT.*?WHERE.*?)(\{[^}]+\})', r'\1%s', target_line)
166
- if fixed_line != target_line:
167
- new_lines[line_number - 1] = fixed_line
168
- applied_rule = True
169
- fix_explanation = "Replaced unsafe Python SQL f-string interpolation with parameterized SQL query placeholder"
170
-
171
- # Strategy 4: Multi-Tenant Query Scoping (TG-DB-004)
172
- elif rule_id == "TG-DB-004":
173
- if ".objects.get(" in target_line and "tenant" not in target_line.lower() and "org" not in target_line.lower():
174
- # .objects.get(id=...) -> .objects.get(id=..., organization_id=request.user.organization_id)
175
- fixed_line = target_line.replace(".objects.get(", ".objects.get(organization_id=request.user.organization_id, ")
176
- if fixed_line != target_line:
177
- new_lines[line_number - 1] = fixed_line
178
- applied_rule = True
179
- fix_explanation = "Added explicit organization_id tenant boundary filter to ORM query"
180
-
181
- # Strategy 5: Permissive CORS with Credentials (TG-PLATFORM-001)
182
- elif rule_id == "TG-PLATFORM-001":
183
- if "CORS_ALLOW_ALL_ORIGINS = True" in target_line:
184
- replacement = f'{indent}CORS_ALLOWED_ORIGINS = [os.environ.get("ALLOWED_ORIGIN", "https://app.example.com")]'
185
- new_lines[line_number - 1] = replacement
186
- applied_rule = True
187
- fix_explanation = "Replaced wildcard CORS configuration with strict origin whitelist environment variable"
188
-
189
- if not applied_rule:
190
- return None
191
-
192
- # Calculate unified diff
193
- orig_lines = [l + "\n" for l in lines]
194
- patched_lines = [l + "\n" for l in new_lines]
195
- diff = "".join(difflib.unified_diff(
196
- orig_lines, patched_lines,
197
- fromfile=f"a/{file_path}", tofile=f"b/{file_path}",
198
- n=2
199
- ))
200
-
201
- additions = sum(1 for l in diff.splitlines() if l.startswith("+") and not l.startswith("+++"))
202
- deletions = sum(1 for l in diff.splitlines() if l.startswith("-") and not l.startswith("---"))
203
-
204
- # Ponytail bounds validation
205
- if additions > 35 or deletions > 25:
206
- return None
207
-
208
- bundle_id = f"bnd-{rule_id.lower()}-{line_number}-{hashlib.sha256(diff.encode('utf-8')).hexdigest()[:6]}"
209
-
210
- return {
211
- "bundle_id": bundle_id,
212
- "finding_id": finding.get("finding_id", f"{rule_id}-{line_number}"),
213
- "rule_id": rule_id,
214
- "title": finding.get("title", rule_id),
215
- "target_file": file_path,
216
- "line_number": line_number,
217
- "what_is_wrong": description,
218
- "why_it_matters": "Security vulnerability violating TorusGuard strict production safety invariant.",
219
- "what_should_change": fix_explanation,
220
- "proposed_diff": diff,
221
- "additions": additions,
222
- "deletions": deletions,
223
- "patched_content": "\n".join(new_lines)
224
- }
225
-
226
-
227
- def execute_harden(target_root: Path, run_id_arg: Optional[str] = None) -> Dict[str, Any]:
228
- """Execute autonomous remediation formulation and package candidate bundles."""
229
- runs_dir = target_root / ".torusguard" / "runs"
230
- if run_id_arg:
231
- run_folder = runs_dir / run_id_arg
232
- else:
233
- run_folder = find_latest_audit_run(runs_dir)
234
-
235
- if not run_folder or not run_folder.is_dir():
236
- print(f"\n {RED}✖ No audit runs found to harden.{RESET}")
237
- print(f" Run {CYAN}npx torusguard audit{RESET} first to discover actionable findings.\n")
238
- sys.exit(1)
239
-
240
- findings_file = run_folder / "findings.json"
241
- if not findings_file.is_file():
242
- print(f"\n {RED}✖ Missing findings.json in run folder:{RESET} {run_folder}\n")
243
- sys.exit(1)
244
-
245
- try:
246
- findings = json.loads(findings_file.read_text(encoding="utf-8"))
247
- except Exception as e:
248
- print(f"\n {RED}✖ Failed to parse findings.json:{RESET} {e}\n")
249
- sys.exit(1)
250
-
251
- bundles_dir = run_folder / "bundles"
252
- bundles_dir.mkdir(parents=True, exist_ok=True)
253
-
254
- bundles = []
255
- for f in findings:
256
- candidate = generate_patch_for_finding(f, target_root)
257
- if candidate:
258
- bundles.append(candidate)
259
- b_dir = bundles_dir / candidate["bundle_id"]
260
- b_dir.mkdir(parents=True, exist_ok=True)
261
-
262
- # 1. patch.diff
263
- (b_dir / "patch.diff").write_text(candidate["proposed_diff"], encoding="utf-8")
264
-
265
- # 2. minimal_patch_plan.md
266
- plan_content = [
267
- f"# Remediation Plan: {candidate['bundle_id']}",
268
- f"- **Rule ID:** `{candidate['rule_id']}`",
269
- f"- **Target File:** `{candidate['target_file']}:{candidate['line_number']}`",
270
- f"- **Ponytail Churn:** `+{candidate['additions']} / -{candidate['deletions']}` (Compliant <=35/<=25)",
271
- f"\n## Proposed Change\n{candidate['what_should_change']}\n",
272
- "## Unified Diff Preview\n```diff",
273
- candidate["proposed_diff"],
274
- "```"
275
- ]
276
- (b_dir / "minimal_patch_plan.md").write_text("\n".join(plan_content), encoding="utf-8")
277
-
278
- # 3. metadata.json
279
- with open(b_dir / "metadata.json", "w", encoding="utf-8") as mf:
280
- json.dump(candidate, mf, indent=2)
281
-
282
- # Render run-level remediation.md
283
- remediation_lines = [
284
- f"# TorusGuard Governed Remediation Catalog",
285
- f"**Run ID:** `{run_folder.name}` ",
286
- f"**Generated:** `{get_ist_now().strftime('%Y-%m-%d %H:%M:%S IST')}` ",
287
- f"**Candidate Bundles Formulated:** `{len(bundles)}` \n",
288
- "---",
289
- "## Formulated Candidate Patches\n"
290
- ]
291
-
292
- for b in bundles:
293
- remediation_lines.append(f"### [{b['rule_id']}] {b['title']}")
294
- remediation_lines.append(f"- **Target:** `{b['target_file']}:{b['line_number']}` | **Bundle ID:** `{b['bundle_id']}`")
295
- remediation_lines.append(f"- **Strategy:** {b['what_should_change']}")
296
- remediation_lines.append(f"- **Ponytail Churn:** `+{b['additions']} / -{b['deletions']}`")
297
- remediation_lines.append("```diff")
298
- remediation_lines.append(b["proposed_diff"].strip())
299
- remediation_lines.append("```\n")
300
-
301
- (run_folder / "remediation.md").write_text("\n".join(remediation_lines), encoding="utf-8")
302
-
303
- # Update manifest
304
- manifest_file = run_folder / "manifest.json"
305
- if manifest_file.is_file():
306
- try:
307
- mdata = json.loads(manifest_file.read_text(encoding="utf-8"))
308
- mdata["remediation_bundles_count"] = len(bundles)
309
- manifest_file.write_text(json.dumps(mdata, indent=2), encoding="utf-8")
310
- except Exception:
311
- pass
312
-
313
- # Print 75-column Terminal Card
314
- print(f"\n {CYAN}╭─────────────────────────────────────────────────────────────────────────╮{RESET}")
315
- print(f" {CYAN}│ │{RESET}")
316
- print(format_box_line(f"{BOLD}🛡️ TORUSGUARD GOVERNED REMEDIATION ENGINE v1.3.1{RESET}", border_color=CYAN))
317
- print(format_box_line(f"{DIM}Autonomous Minimal Patch Formulation & Ponytail Packaging{RESET}", border_color=CYAN))
318
- print(f" {CYAN}│ │{RESET}")
319
- print(f" {CYAN}╰─────────────────────────────────────────────────────────────────────────╯{RESET}\n")
320
-
321
- print(f" {CYAN}┌─ Remediation Scope ─────────────────────────────────────────────────────┐{RESET}")
322
- print(format_box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
323
- print(format_box_line(f"Target Scope: {WHITE}{target_root}{RESET}"))
324
- print(format_box_line(f"Total Findings: {len(findings)} evaluated | {GREEN}{len(bundles)} surgical patches formulated{RESET}"))
325
- print(format_box_line(f"Ponytail Guard: {GREEN}All patches strictly <= 35 additions, <= 25 deletions{RESET}"))
326
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
327
-
328
- if bundles:
329
- print(f" {CYAN}┌─ Formulated Candidate Patches ({len(bundles)}) ───────────────────────────────────┐{RESET}")
330
- for b in bundles[:5]:
331
- print(format_box_line(f"{YELLOW}[{b['rule_id']}]{RESET} {WHITE}{b['target_file']}:{b['line_number']}{RESET} (+{b['additions']}/-{b['deletions']})"))
332
- print(format_box_line(f" └─ {DIM}{b['what_should_change']}{RESET}"))
333
- if len(bundles) > 5:
334
- print(format_box_line(f" ... and {len(bundles) - 5} more patches cataloged in remediation.md"))
335
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
336
-
337
- print(f" {GREEN}╔═ Next Governed Action ═════════════════════════════════════════════════════╗{RESET}")
338
- print(format_box_line(f"Review & Apply: {BOLD}{WHITE}npx torusguard apply{RESET} (CLI Human Gate)", border="║", border_color=GREEN))
339
- print(format_box_line(f"Auto-Apply Flag: {CYAN}npx torusguard apply --yes{RESET} (Automated mode)", border="║", border_color=GREEN))
340
- print(format_box_line(f"AI IDE Chat: Run {CYAN}/torusguard-apply{RESET} in your AI chat", border="║", border_color=GREEN))
341
- print(f" {GREEN}╚═════════════════════════════════════════════════════════════════════════════╝{RESET}\n")
342
- else:
343
- print(f" {YELLOW}ℹ No automatic patch templates matched current findings.{RESET}")
344
- print(f" {GRAY}Findings require architectural refactoring or AI-assisted guidance.{RESET}\n")
345
- print(f" {CYAN}┌─ Recommended Next Steps ────────────────────────────────────────────────┐{RESET}")
346
- print(format_box_line("1. In AI Chat (Antigravity/Cursor/Claude): Run /torusguard-harden"))
347
- print(format_box_line(" to synthesize custom architectural fixes with your AI agent."))
348
- print(format_box_line("2. View visual posture report: npx torusguard report --html"))
349
- print(format_box_line(f"3. Inspect finding details: .torusguard/runs/{run_folder.name}/findings.md"))
350
- print(f" {CYAN}└─────────────────────────────────────────────────────────────────────────┘{RESET}\n")
351
-
352
- return {
353
- "status": "success",
354
- "run_folder": str(run_folder),
355
- "bundles_count": len(bundles),
356
- "bundles": bundles
357
- }
358
-
359
-
360
- def main():
361
- parser = argparse.ArgumentParser(description="TorusGuard Autonomous Governed Remediation Engine")
362
- parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
363
- parser.add_argument("--run", "-r", help="Explicit run ID to harden (default: latest audit run)")
364
- parser.add_argument("--json", action="store_true", help="Output raw JSON")
365
- args = parser.parse_args()
366
-
367
- target = Path(args.path).resolve()
368
- result = execute_harden(target, run_id_arg=args.run)
369
-
370
- if args.json:
371
- print(json.dumps(result, indent=2))
372
-
373
-
374
- if __name__ == "__main__":
375
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Autonomous Remediation Engine (v1.3.5)
4
+ Evaluates audit findings against canonical patch templates, enforces Ponytail bounds
5
+ (<= 35 additions, <= 25 deletions), packages structured candidate remediation bundles,
6
+ and renders unified patch diffs with standardized 75-column terminal UI.
7
+
8
+ Pure Python 3.10+ standard library (zero external dependencies).
9
+ """
10
+
11
+ import sys
12
+ import os
13
+ import re
14
+ import json
15
+ import difflib
16
+ import hashlib
17
+ import argparse
18
+ import datetime
19
+ from pathlib import Path
20
+ from typing import Dict, List, Any, Optional
21
+
22
+ # Ensure scripts dir is in sys.path for term_ui import
23
+ scripts_dir = Path(__file__).resolve().parent
24
+ if str(scripts_dir) not in sys.path:
25
+ sys.path.insert(0, str(scripts_dir))
26
+
27
+ try:
28
+ import term_ui
29
+ except ImportError:
30
+ term_ui = None
31
+
32
+ # Windows console UTF-8 support
33
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
34
+ try:
35
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
36
+ except Exception:
37
+ pass
38
+
39
+ # ─── Timezone Configuration: Indian Standard Time (IST, UTC+05:30) ───────────
40
+ IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
41
+
42
+ def get_ist_now() -> datetime.datetime:
43
+ return datetime.datetime.now(IST)
44
+
45
+ # ─── Fallback Formatters (if term_ui missing) ──────────────────────────────────
46
+ BOLD = "\033[1m"
47
+ DIM = "\033[2m"
48
+ RESET = "\033[0m"
49
+ CYAN = "\033[36m"
50
+ GREEN = "\033[32m"
51
+ YELLOW = "\033[33m"
52
+ WHITE = "\033[97m"
53
+ GRAY = "\033[90m"
54
+ RED = "\033[31m"
55
+
56
+ def box_line(content: str, width: int = 67, border: str = "│", border_color: str = CYAN) -> str:
57
+ if term_ui:
58
+ return term_ui.format_box_line(content, width=width, border=border, border_color=border_color)
59
+ return f" {border_color}{border}{RESET} {content}"
60
+
61
+ def box_header(title: str, subtitle: str = "", version: str = "v1.3.5", border_color: str = CYAN) -> str:
62
+ if term_ui:
63
+ return term_ui.card_header(title, subtitle, version, border_color)
64
+ return f"=== {title} ({version}) ==="
65
+
66
+ def border_top(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
67
+ if term_ui:
68
+ return term_ui.card_border_top(title, border_color=border_color, double=double)
69
+ return "┌" + "─" * 71 + "┐"
70
+
71
+ def border_bottom(border_color: str = CYAN, double: bool = False) -> str:
72
+ if term_ui:
73
+ return term_ui.card_border_bottom(border_color=border_color, double=double)
74
+ return "└" + "─" * 71 + "┘"
75
+
76
+
77
+ def find_latest_audit_run(runs_dir: Path) -> Optional[Path]:
78
+ """Locate the most recent audit run folder containing findings.json."""
79
+ if not runs_dir.is_dir():
80
+ return None
81
+ runs = sorted(runs_dir.glob("run-*-audit"), key=lambda p: p.stat().st_mtime, reverse=True)
82
+ for r in runs:
83
+ if (r / "findings.json").is_file():
84
+ return r
85
+ return None
86
+
87
+
88
+ def generate_patch_for_finding(finding: Dict[str, Any], target_root: Path) -> Optional[Dict[str, Any]]:
89
+ """
90
+ Formulate a minimal, surgical fix for a finding adhering to Ponytail bounds
91
+ (<= 35 additions, <= 25 deletions).
92
+ """
93
+ rule_id = finding.get("rule_id", "")
94
+ file_path = finding.get("file_path", "")
95
+ line_number = finding.get("line_number", 1)
96
+ description = finding.get("description", "")
97
+
98
+ full_target = target_root / file_path
99
+ if not full_target.is_file():
100
+ return None
101
+
102
+ try:
103
+ content = full_target.read_text(encoding="utf-8", errors="replace")
104
+ except Exception:
105
+ return None
106
+
107
+ lines = content.splitlines()
108
+ if not (1 <= line_number <= len(lines)):
109
+ return None
110
+
111
+ target_line = lines[line_number - 1]
112
+ indent = re.match(r'^\s*', target_line).group(0) # type: ignore
113
+ new_lines = list(lines)
114
+ applied_rule = False
115
+ fix_explanation = ""
116
+
117
+ # Strategy 1: Hardcoded Secrets (TG-SEC-001 / TG-SEC-002)
118
+ if rule_id in ("TG-SEC-001", "TG-SEC-002"):
119
+ if file_path.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs")):
120
+ if re.search(r'(jwt_secret|jwtSecret|JWT_SECRET)', target_line, re.IGNORECASE):
121
+ var_match = re.search(r'(const|let|var)\s+([A-Za-z0-9_]+)\s*=', target_line)
122
+ var_name = var_match.group(2) if var_match else "JWT_SECRET"
123
+ replacement = f'{indent}const {var_name} = process.env.JWT_SECRET || "";'
124
+ new_lines[line_number - 1] = replacement
125
+ applied_rule = True
126
+ fix_explanation = f"Migrated hardcoded JWT secret to environment variable process.env.JWT_SECRET"
127
+ elif re.search(r'(:=|=)\s*["\'][A-Za-z0-9_\-]{8,}["\']', target_line):
128
+ var_match = re.search(r'(const|let|var)?\s*([A-Za-z0-9_]+)\s*[:=]', target_line)
129
+ var_name = var_match.group(2) if var_match else "API_KEY"
130
+ env_key = re.sub(r'(?<!^)(?=[A-Z])', '_', var_name).upper()
131
+ replacement = f'{indent}const {var_name} = process.env.{env_key} || "";'
132
+ new_lines[line_number - 1] = replacement
133
+ applied_rule = True
134
+ fix_explanation = f"Replaced hardcoded credential with process.env.{env_key}"
135
+ elif "password:" in target_line or "password :" in target_line:
136
+ fixed_line = re.sub(r'password:\s*["\'][^"\']+["\']', 'password: process.env.DEMO_USER_PASSWORD || "REDACTED_PASSWORD"', target_line)
137
+ if fixed_line != target_line:
138
+ new_lines[line_number - 1] = fixed_line
139
+ applied_rule = True
140
+ fix_explanation = "Replaced hardcoded password with environment variable process.env.DEMO_USER_PASSWORD"
141
+ elif file_path.endswith(".py"):
142
+ if re.search(r'(:=|=)\s*["\'][A-Za-z0-9_\-]{8,}["\']', target_line):
143
+ var_match = re.search(r'([A-Za-z0-9_]+)\s*=', target_line)
144
+ var_name = var_match.group(1) if var_match else "SECRET_KEY"
145
+ env_key = var_name.upper()
146
+ replacement = f'{indent}{var_name} = os.environ.get("{env_key}", "")'
147
+ new_lines[line_number - 1] = replacement
148
+ applied_rule = True
149
+ fix_explanation = f"Replaced hardcoded credential with os.environ.get('{env_key}')"
150
+
151
+ # Strategy 2: Dangerous React HTML rendering & DOM innerHTML (TG-INPUT-003)
152
+ elif rule_id == "TG-INPUT-003":
153
+ if "dangerouslySetInnerHTML" in target_line:
154
+ var_match = re.search(r'__html\s*:\s*([A-Za-z0-9_$.]+)', target_line)
155
+ var_name = var_match.group(1) if var_match else "content"
156
+ replacement = f'{indent}<div>{{{var_name}}}</div>'
157
+ new_lines[line_number - 1] = replacement
158
+ applied_rule = True
159
+ fix_explanation = "Replaced raw dangerouslySetInnerHTML injection with safe React text interpolation"
160
+ elif ".innerHTML" in target_line:
161
+ if re.search(r'\.innerHTML\s*=\s*["\']["\']', target_line):
162
+ fixed_line = re.sub(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*["\']["\']', r'\1.textContent = ""', target_line)
163
+ if fixed_line != target_line:
164
+ new_lines[line_number - 1] = fixed_line
165
+ applied_rule = True
166
+ fix_explanation = "Replaced unsafe innerHTML DOM reset with safe textContent assignment"
167
+ elif re.search(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*([a-zA-Z0-9_$.]+)\s*;?$', target_line):
168
+ fixed_line = re.sub(r'([a-zA-Z0-9_$]+)\.innerHTML\s*=\s*([a-zA-Z0-9_$.]+)', r'\1.textContent = \2', target_line)
169
+ if fixed_line != target_line:
170
+ new_lines[line_number - 1] = fixed_line
171
+ applied_rule = True
172
+ fix_explanation = "Replaced raw DOM innerHTML assignment with XSS-safe textContent"
173
+
174
+ # Strategy 3: Raw SQL Concatenation (TG-INPUT-002)
175
+ elif rule_id == "TG-INPUT-002":
176
+ if file_path.endswith(".py") and ("f\"SELECT" in target_line or "f'SELECT" in target_line):
177
+ fixed_line = re.sub(r'f(["\']SELECT.*?WHERE.*?)(\{[^}]+\})', r'\1%s', target_line)
178
+ if fixed_line != target_line:
179
+ new_lines[line_number - 1] = fixed_line
180
+ applied_rule = True
181
+ fix_explanation = "Replaced unsafe Python SQL f-string interpolation with parameterized SQL query placeholder"
182
+ elif file_path.endswith((".js", ".jsx", ".ts", ".tsx", ".mjs")):
183
+ # Template string SQL injection: `SELECT ... ${var}` -> parameterized placeholder
184
+ if "`SELECT" in target_line or "`select" in target_line:
185
+ fixed_line = re.sub(r'\$\{[^}]+\}', '?', target_line)
186
+ if fixed_line != target_line:
187
+ new_lines[line_number - 1] = fixed_line
188
+ applied_rule = True
189
+ fix_explanation = "Replaced template literal SQL string interpolation with parameterized SQL placeholder (?)"
190
+
191
+ # Strategy 4: Multi-Tenant Query Scoping (TG-DB-004)
192
+ elif rule_id == "TG-DB-004":
193
+ if ".objects.get(" in target_line and "tenant" not in target_line.lower() and "org" not in target_line.lower():
194
+ fixed_line = target_line.replace(".objects.get(", ".objects.get(organization_id=request.user.organization_id, ")
195
+ if fixed_line != target_line:
196
+ new_lines[line_number - 1] = fixed_line
197
+ applied_rule = True
198
+ fix_explanation = "Added explicit organization_id tenant boundary filter to ORM query"
199
+ elif "findUnique(" in target_line and "tenant" not in target_line.lower():
200
+ fixed_line = target_line.replace("findUnique({ where: {", "findFirst({ where: { tenantId: req.tenantId,")
201
+ if fixed_line != target_line:
202
+ new_lines[line_number - 1] = fixed_line
203
+ applied_rule = True
204
+ fix_explanation = "Enforced tenantId multi-tenant isolation in Prisma lookup"
205
+
206
+ # Strategy 5: Permissive CORS with Credentials (TG-PLATFORM-001)
207
+ elif rule_id == "TG-PLATFORM-001":
208
+ if "CORS_ALLOW_ALL_ORIGINS = True" in target_line:
209
+ replacement = f'{indent}CORS_ALLOWED_ORIGINS = [os.environ.get("ALLOWED_ORIGIN", "https://app.example.com")]'
210
+ new_lines[line_number - 1] = replacement
211
+ applied_rule = True
212
+ fix_explanation = "Replaced wildcard CORS configuration with strict origin whitelist environment variable"
213
+ elif "cors(" in target_line and ("origin: '*'" in target_line or 'origin: "*"' in target_line):
214
+ fixed_line = target_line.replace("origin: '*'", "origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173'")
215
+ fixed_line = fixed_line.replace('origin: "*"', "origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173'")
216
+ if fixed_line != target_line:
217
+ new_lines[line_number - 1] = fixed_line
218
+ applied_rule = True
219
+ fix_explanation = "Constrained wildcard CORS origin to process.env.ALLOWED_ORIGIN with secure fallback"
220
+
221
+ # Strategy 6: Cookie Missing Security Flags (TG-AUTH-004)
222
+ elif rule_id == "TG-AUTH-004":
223
+ if "res.cookie(" in target_line and "httpOnly" not in target_line:
224
+ # res.cookie('token', token) -> res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' })
225
+ fixed_line = re.sub(
226
+ r'res\.cookie\(\s*([a-zA-Z0-9_\'\"]+)\s*,\s*([a-zA-Z0-9_]+)\s*\)',
227
+ r"res.cookie(\1, \2, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' })",
228
+ target_line
229
+ )
230
+ if fixed_line != target_line:
231
+ new_lines[line_number - 1] = fixed_line
232
+ applied_rule = True
233
+ fix_explanation = "Injected mandatory security flags (httpOnly, secure, sameSite) to cookie definition"
234
+
235
+ # Strategy 7: Path Traversal (TG-INPUT-006)
236
+ elif rule_id == "TG-INPUT-006":
237
+ if "path.join(" in target_line and "path.basename" not in target_line:
238
+ fixed_line = re.sub(r'path\.join\((.*?),\s*([a-zA-Z0-9_$.]+)\)', r'path.join(\1, path.basename(\2))', target_line)
239
+ if fixed_line != target_line:
240
+ new_lines[line_number - 1] = fixed_line
241
+ applied_rule = True
242
+ fix_explanation = "Wrapped dynamic path segment in path.basename to neutralize path traversal sequences"
243
+ elif "os.path.join(" in target_line and "os.path.basename" not in target_line:
244
+ fixed_line = re.sub(r'os\.path\.join\((.*?),\s*([a-zA-Z0-9_$.]+)\)', r'os.path.join(\1, os.path.basename(\2))', target_line)
245
+ if fixed_line != target_line:
246
+ new_lines[line_number - 1] = fixed_line
247
+ applied_rule = True
248
+ fix_explanation = "Wrapped dynamic path segment in os.path.basename to neutralize path traversal sequences"
249
+
250
+ # Strategy 8: TLS Verification Bypass (TG-DIFF-001)
251
+ elif rule_id == "TG-DIFF-001":
252
+ if "verify=False" in target_line:
253
+ fixed_line = target_line.replace("verify=False", "verify=True")
254
+ new_lines[line_number - 1] = fixed_line
255
+ applied_rule = True
256
+ fix_explanation = "Restored mandatory TLS certificate verification (verify=True)"
257
+ elif "rejectUnauthorized: false" in target_line:
258
+ fixed_line = target_line.replace("rejectUnauthorized: false", "rejectUnauthorized: true")
259
+ new_lines[line_number - 1] = fixed_line
260
+ applied_rule = True
261
+ fix_explanation = "Restored TLS certificate authority validation (rejectUnauthorized: true)"
262
+ elif "InsecureSkipVerify: true" in target_line:
263
+ fixed_line = target_line.replace("InsecureSkipVerify: true", "InsecureSkipVerify: false")
264
+ new_lines[line_number - 1] = fixed_line
265
+ applied_rule = True
266
+ fix_explanation = "Restored mandatory TLS certificate validation (InsecureSkipVerify: false)"
267
+
268
+
269
+ # Strategy 9: Hardcoded Database / Service Credential in Config (TG-SEC-003)
270
+ elif rule_id == "TG-SEC-003":
271
+ if "=" in target_line:
272
+ var_match = re.match(r'^\s*([A-Za-z0-9_]+)\s*=', target_line)
273
+ if var_match:
274
+ vname = var_match.group(1)
275
+ fixed_line = f'{indent}{vname} = os.environ.get("{vname}", "")' if file_path.endswith('.py') else f'{indent}const {vname} = process.env.{vname} || "";'
276
+ new_lines[line_number - 1] = fixed_line
277
+ applied_rule = True
278
+ fix_explanation = f"Extracted sensitive config variable {vname} to environment variable"
279
+
280
+ # Strategy 10: Weak Password Hashing (TG-AUTH-001)
281
+ elif rule_id == "TG-AUTH-001":
282
+ if "hashlib.md5" in target_line:
283
+ fixed_line = target_line.replace("hashlib.md5(", "hashlib.sha256(")
284
+ new_lines[line_number - 1] = fixed_line
285
+ applied_rule = True
286
+ fix_explanation = "Replaced broken MD5 hashing with SHA-256 (or bcrypt/argon2)"
287
+ elif "createHash('md5')" in target_line:
288
+ fixed_line = target_line.replace("createHash('md5')", "createHash('sha256')")
289
+ new_lines[line_number - 1] = fixed_line
290
+ applied_rule = True
291
+ fix_explanation = "Replaced broken MD5 hashing with SHA-256"
292
+
293
+ # Strategy 11: JWT Missing Algorithm Restriction (TG-AUTH-003)
294
+ elif rule_id == "TG-AUTH-003":
295
+ if "jwt.verify(" in target_line and "algorithms" not in target_line:
296
+ fixed_line = re.sub(r'jwt\.verify\(\s*([^,\)]+)\s*,\s*([^,\)]+)\s*\)', r"jwt.verify(\1, \2, { algorithms: ['HS256'] })", target_line)
297
+ if fixed_line != target_line:
298
+ new_lines[line_number - 1] = fixed_line
299
+ applied_rule = True
300
+ fix_explanation = "Restricted JWT verification to explicit algorithm list ['HS256']"
301
+
302
+ # Strategy 12: Untrusted Role/Tenant Header Injection (TG-AUTH-008)
303
+ elif rule_id == "TG-AUTH-008":
304
+ if "req.headers[" in target_line or "request.headers.get(" in target_line:
305
+ if file_path.endswith((".js", ".ts")):
306
+ fixed_line = f'{indent}const role = req.user ? req.user.role : "user"; // Derived securely from authenticated session'
307
+ new_lines[line_number - 1] = fixed_line
308
+ applied_rule = True
309
+ fix_explanation = "Replaced unverified client HTTP header with verified session-derived identity"
310
+ elif file_path.endswith(".py"):
311
+ fixed_line = f'{indent}role = getattr(request.user, "role", "user") # Derived securely from authenticated session'
312
+ new_lines[line_number - 1] = fixed_line
313
+ applied_rule = True
314
+ fix_explanation = "Replaced unverified client HTTP header with verified session-derived identity"
315
+
316
+ # Strategy 13: Rate Limiting Injection (TG-RATE-001)
317
+ elif rule_id == "TG-RATE-001":
318
+ if re.search(r'\.(?:post|all)\s*\(\s*[\'"][^\'"]+[\'"]\s*,\s*(?:async\s*)?\(', target_line):
319
+ fixed_line = re.sub(
320
+ r'(\.(?:post|all)\s*\(\s*[\'"][^\'"]+[\'"]\s*),\s*((?:async\s*)?\()',
321
+ r'\1, authLimiter, \2',
322
+ target_line
323
+ )
324
+ if fixed_line != target_line:
325
+ new_lines[line_number - 1] = fixed_line
326
+ applied_rule = True
327
+ fix_explanation = "Injected authLimiter rate-limiting middleware to protect authentication endpoint from brute force"
328
+
329
+ # Strategy 14: Prompt Injection System Context Isolation (TG-AGENT-001)
330
+ elif rule_id == "TG-AGENT-001":
331
+ if 'role": "system"' in target_line or "role': 'system'" in target_line:
332
+ fixed_line = target_line.replace('"role": "system"', '"role": "user"')
333
+ if fixed_line != target_line:
334
+ new_lines[line_number - 1] = fixed_line
335
+ applied_rule = True
336
+ fix_explanation = "Demoted untrusted user input from system prompt into role: 'user' message container"
337
+
338
+ # Strategy 15: Outbound URL Fetch Validation (TG-SSRF-001)
339
+ elif rule_id == "TG-SSRF-001":
340
+ if "fetch(" in target_line and "validateUrl(" not in target_line:
341
+ fixed_line = re.sub(r'fetch\(\s*([^,\)]+)', r'fetch(validateSafeUrl(\1)', target_line)
342
+ if fixed_line != target_line:
343
+ new_lines[line_number - 1] = fixed_line
344
+ applied_rule = True
345
+ fix_explanation = "Wrapped outbound URL in validateSafeUrl domain whitelist guard"
346
+ elif "requests.get(" in target_line and "validate_url(" not in target_line:
347
+ fixed_line = re.sub(r'requests\.get\(\s*([^,\)]+)', r'requests.get(validate_safe_url(\1)', target_line)
348
+ if fixed_line != target_line:
349
+ new_lines[line_number - 1] = fixed_line
350
+ applied_rule = True
351
+ fix_explanation = "Wrapped outbound URL in validate_safe_url domain whitelist guard"
352
+
353
+ # Strategy 16: Outbound Request Timeout (TG-SSRF-004)
354
+ elif rule_id == "TG-SSRF-004":
355
+ if "requests.get(" in target_line and "timeout=" not in target_line:
356
+ fixed_line = target_line.replace(")", ", timeout=10)")
357
+ new_lines[line_number - 1] = fixed_line
358
+ applied_rule = True
359
+ fix_explanation = "Injected explicit 10-second request timeout to prevent resource exhaustion"
360
+
361
+ # Strategy 17: Production Debug Mode Disablement (TG-PLATFORM-003)
362
+ elif rule_id == "TG-PLATFORM-003":
363
+ if "DEBUG = True" in target_line:
364
+ fixed_line = f'{indent}DEBUG = os.environ.get("DJANGO_DEBUG", "False").lower() in ("true", "1")'
365
+ new_lines[line_number - 1] = fixed_line
366
+ applied_rule = True
367
+ fix_explanation = "Guard DEBUG mode behind DJANGO_DEBUG environment variable"
368
+
369
+ # Strategy 18: Production Introspection Disablement (TG-GQL-004)
370
+ elif rule_id == "TG-GQL-004":
371
+ if "introspection: true" in target_line or "introspection: True" in target_line:
372
+ fixed_line = re.sub(r'introspection\s*:\s*(?:true|True)', 'introspection: process.env.NODE_ENV !== "production"', target_line)
373
+ new_lines[line_number - 1] = fixed_line
374
+ # Strategy 19: Privileged Database Credential in Frontend (TG-DB-002)
375
+ elif rule_id == "TG-DB-002":
376
+ if "SUPABASE_SERVICE_ROLE_KEY" in target_line:
377
+ var_match = re.search(r'(export\s+const|const)\s+([A-Za-z0-9_]+)\s*=', target_line)
378
+ var_name = var_match.group(2) if var_match else "SUPABASE_SERVICE_ROLE_KEY"
379
+ replacement = f'{indent}export const {var_name} = process.env.{var_name} || "";'
380
+ new_lines[line_number - 1] = replacement
381
+ applied_rule = True
382
+ fix_explanation = f"Replaced hardcoded privileged database credential with process.env.{var_name}"
383
+
384
+ # Strategy 20: Public Source Maps in Production (TG-CLIENT-001)
385
+ elif rule_id == "TG-CLIENT-001":
386
+ if "sourcemap: true" in target_line:
387
+ fixed_line = target_line.replace("sourcemap: true", "sourcemap: false")
388
+ new_lines[line_number - 1] = fixed_line
389
+ applied_rule = True
390
+ fix_explanation = "Disabled public production source map emission"
391
+
392
+ # Strategy 21: Sensitive Information in Logs (TG-SEC-004)
393
+ elif rule_id == "TG-SEC-004":
394
+ if "console.log" in target_line:
395
+ fixed_line = f"{indent}// Security: Sensitive credential log removed per TG-SEC-004"
396
+ new_lines[line_number - 1] = fixed_line
397
+ applied_rule = True
398
+ fix_explanation = "Redacted console logging of sensitive token or credential"
399
+
400
+ # Strategy 22: Unlimited Public Write Endpoint (TG-RATE-002)
401
+ elif rule_id == "TG-RATE-002":
402
+ if re.search(r'\.(?:post|put|patch)\s*\(\s*[\'"][^\'"]+[\'"]\s*,\s*(?:async\s*)?\(', target_line):
403
+ fixed_line = re.sub(
404
+ r'(\.(?:post|put|patch)\s*\(\s*[\'"][^\'"]+[\'"]\s*),\s*((?:async\s*)?\()',
405
+ r'\1, writeLimiter, \2',
406
+ target_line
407
+ )
408
+ if fixed_line != target_line:
409
+ new_lines[line_number - 1] = fixed_line
410
+ applied_rule = True
411
+ fix_explanation = "Injected writeLimiter rate-limiting middleware to public write endpoint"
412
+
413
+ # Strategy 23: Missing Security Headers (TG-PLATFORM-002)
414
+ elif rule_id == "TG-PLATFORM-002":
415
+ if "const app = express();" in target_line:
416
+ replacement = f'{indent}const helmet = require("helmet");\n{indent}const app = express();\n{indent}app.use(helmet());'
417
+ new_lines[line_number - 1] = replacement
418
+ applied_rule = True
419
+ fix_explanation = "Injected helmet security headers middleware into Express application"
420
+
421
+ # Strategy 24: Missing Request Body Validation (TG-INPUT-001)
422
+ elif rule_id == "TG-INPUT-001":
423
+ if "req.body" in target_line and ("const {" in target_line or "let {" in target_line):
424
+ replacement = f'{indent}if (!req.body || typeof req.body !== "object") return res.status(400).json({{ error: "Invalid payload" }});\n{target_line}'
425
+ new_lines[line_number - 1] = replacement
426
+ applied_rule = True
427
+ fix_explanation = "Added defensive request body payload validation before destructuring"
428
+
429
+ if not applied_rule:
430
+ return None
431
+
432
+ # Calculate unified diff
433
+ orig_lines = [l + "\n" for l in lines]
434
+ patched_lines = [l + "\n" for l in new_lines]
435
+ diff = "".join(difflib.unified_diff(
436
+ orig_lines, patched_lines,
437
+ fromfile=f"a/{file_path}", tofile=f"b/{file_path}",
438
+ n=2
439
+ ))
440
+
441
+ additions = sum(1 for l in diff.splitlines() if l.startswith("+") and not l.startswith("+++"))
442
+ deletions = sum(1 for l in diff.splitlines() if l.startswith("-") and not l.startswith("---"))
443
+
444
+ # Ponytail bounds validation: <= 35 additions, <= 25 deletions
445
+ if additions > 35 or deletions > 25:
446
+ return None
447
+
448
+ bundle_id = f"bnd-{rule_id.lower()}-{line_number}-{hashlib.sha256(diff.encode('utf-8')).hexdigest()[:6]}"
449
+
450
+ return {
451
+ "bundle_id": bundle_id,
452
+ "finding_id": finding.get("finding_id", f"{rule_id}-{line_number}"),
453
+ "rule_id": rule_id,
454
+ "title": finding.get("title", rule_id),
455
+ "target_file": file_path,
456
+ "line_number": line_number,
457
+ "target_line": target_line,
458
+ "replacement_line": new_lines[line_number - 1],
459
+ "what_is_wrong": description,
460
+ "why_it_matters": "Security vulnerability violating TorusGuard strict production safety invariant.",
461
+ "what_should_change": fix_explanation,
462
+ "proposed_diff": diff,
463
+ "additions": additions,
464
+ "deletions": deletions,
465
+ "patched_content": "\n".join(new_lines)
466
+ }
467
+
468
+
469
+ def execute_harden(target_root: Path, run_id_arg: Optional[str] = None) -> Dict[str, Any]:
470
+ """Execute autonomous remediation formulation and package candidate bundles."""
471
+ runs_dir = target_root / ".torusguard" / "runs"
472
+ if run_id_arg:
473
+ run_folder = runs_dir / run_id_arg
474
+ else:
475
+ run_folder = find_latest_audit_run(runs_dir)
476
+
477
+ if not run_folder or not run_folder.is_dir():
478
+ print(f"\n {RED}✖ No audit runs found to harden.{RESET}")
479
+ print(f" Run {CYAN}npx torusguard audit{RESET} first to discover actionable findings.\n")
480
+ sys.exit(1)
481
+
482
+ findings_file = run_folder / "findings.json"
483
+ if not findings_file.is_file():
484
+ print(f"\n {RED}✖ Missing findings.json in run folder:{RESET} {run_folder}\n")
485
+ sys.exit(1)
486
+
487
+ try:
488
+ findings = json.loads(findings_file.read_text(encoding="utf-8"))
489
+ except Exception as e:
490
+ print(f"\n {RED}✖ Failed to parse findings.json:{RESET} {e}\n")
491
+ sys.exit(1)
492
+
493
+ bundles_dir = run_folder / "bundles"
494
+ bundles_dir.mkdir(parents=True, exist_ok=True)
495
+
496
+ bundles = []
497
+ seen_file_lines = set()
498
+ for f in findings:
499
+ target_key = (f.get("file_path"), f.get("line_number"))
500
+ if target_key in seen_file_lines:
501
+ continue
502
+ candidate = generate_patch_for_finding(f, target_root)
503
+ if candidate:
504
+ seen_file_lines.add(target_key)
505
+ bundles.append(candidate)
506
+ b_dir = bundles_dir / candidate["bundle_id"]
507
+ b_dir.mkdir(parents=True, exist_ok=True)
508
+
509
+ # 1. patch.diff
510
+ (b_dir / "patch.diff").write_text(candidate["proposed_diff"], encoding="utf-8")
511
+
512
+ # 2. minimal_patch_plan.md
513
+ plan_content = [
514
+ f"# Remediation Plan: {candidate['bundle_id']}",
515
+ f"- **Rule ID:** `{candidate['rule_id']}`",
516
+ f"- **Target File:** `{candidate['target_file']}:{candidate['line_number']}`",
517
+ f"- **Ponytail Churn:** `+{candidate['additions']} / -{candidate['deletions']}` (Compliant <=35/<=25)",
518
+ f"\n## Proposed Change\n{candidate['what_should_change']}\n",
519
+ "## Unified Diff Preview\n```diff",
520
+ candidate["proposed_diff"],
521
+ "```"
522
+ ]
523
+ (b_dir / "minimal_patch_plan.md").write_text("\n".join(plan_content), encoding="utf-8")
524
+
525
+ # 3. metadata.json
526
+ with open(b_dir / "metadata.json", "w", encoding="utf-8") as mf:
527
+ json.dump(candidate, mf, indent=2)
528
+
529
+ # Render run-level remediation.md
530
+ remediation_lines = [
531
+ f"# TorusGuard Governed Remediation Catalog",
532
+ f"**Run ID:** `{run_folder.name}` ",
533
+ f"**Generated:** `{get_ist_now().strftime('%Y-%m-%d %H:%M:%S IST')}` ",
534
+ f"**Candidate Bundles Formulated:** `{len(bundles)}` \n",
535
+ "---",
536
+ "## Formulated Candidate Patches\n"
537
+ ]
538
+
539
+ for b in bundles:
540
+ remediation_lines.append(f"### [{b['rule_id']}] {b['title']}")
541
+ remediation_lines.append(f"- **Target:** `{b['target_file']}:{b['line_number']}` | **Bundle ID:** `{b['bundle_id']}`")
542
+ remediation_lines.append(f"- **Strategy:** {b['what_should_change']}")
543
+ remediation_lines.append(f"- **Ponytail Churn:** `+{b['additions']} / -{b['deletions']}`")
544
+ remediation_lines.append("```diff")
545
+ remediation_lines.append(b["proposed_diff"].strip())
546
+ remediation_lines.append("```\n")
547
+
548
+ (run_folder / "remediation.md").write_text("\n".join(remediation_lines), encoding="utf-8")
549
+ try:
550
+ import report_sync
551
+ report_sync.record_harden_bundles(target_root, bundles, run_folder.name)
552
+ except Exception:
553
+ pass
554
+
555
+ # Update manifest
556
+ manifest_file = run_folder / "manifest.json"
557
+ if manifest_file.is_file():
558
+ try:
559
+ mdata = json.loads(manifest_file.read_text(encoding="utf-8"))
560
+ mdata["remediation_bundles_count"] = len(bundles)
561
+ manifest_file.write_text(json.dumps(mdata, indent=2), encoding="utf-8")
562
+ except Exception:
563
+ pass
564
+
565
+ # Print 75-column Terminal Cards
566
+ print()
567
+ print(box_header("🛡️ TORUSGUARD GOVERNED REMEDIATION ENGINE", "Autonomous Minimal Patch Formulation & Ponytail Packaging", "v1.3.5"))
568
+ print()
569
+
570
+ print(border_top("Remediation Scope"))
571
+ print(box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
572
+ print(box_line(f"Target Scope: {WHITE}{target_root}{RESET}"))
573
+ print(box_line(f"Total Findings: {len(findings)} evaluated | {GREEN}{len(bundles)} surgical patches formulated{RESET}"))
574
+ print(box_line(f"Ponytail Guard: {GREEN}All patches strictly <= 35 additions, <= 25 deletions{RESET}"))
575
+ print(border_bottom())
576
+ print()
577
+
578
+ if bundles:
579
+ print(border_top(f"Formulated Candidate Patches ({len(bundles)})"))
580
+ for b in bundles[:5]:
581
+ print(box_line(f"{YELLOW}[{b['rule_id']}]{RESET} {WHITE}{b['target_file']}:{b['line_number']}{RESET} (+{b['additions']}/-{b['deletions']})"))
582
+ print(box_line(f" └─ {DIM}{b['what_should_change']}{RESET}"))
583
+ if len(bundles) > 5:
584
+ print(box_line(f" ... and {len(bundles) - 5} more patches cataloged in remediation.md"))
585
+ print(border_bottom())
586
+ print()
587
+
588
+ print(border_top("Next Governed Action", border_color=GREEN, double=True))
589
+ print(box_line(f"Living Report: {CYAN}security_report.md (patch previews attached){RESET}", border="║", border_color=GREEN))
590
+ print(box_line(f"Review & Apply: {BOLD}{WHITE}npx torusguard apply{RESET} (CLI Human Gate)", border="║", border_color=GREEN))
591
+ print(box_line(f"Auto-Apply Flag: {CYAN}npx torusguard apply --yes{RESET} (Automated mode)", border="║", border_color=GREEN))
592
+ print(box_line(f"AI IDE Chat: Run {CYAN}/torusguard-apply{RESET} in your AI chat", border="║", border_color=GREEN))
593
+ print(border_bottom(border_color=GREEN, double=True))
594
+ print()
595
+ else:
596
+ print(f" {YELLOW}ℹ No automatic patch templates matched current findings.{RESET}")
597
+ print(f" {GRAY}Findings require architectural refactoring or AI-assisted guidance.{RESET}\n")
598
+ print(border_top("Recommended Next Steps"))
599
+ print(box_line("1. In AI Chat: Run /torusguard-harden to synthesize custom fixes"))
600
+ print(box_line("2. View visual posture report: npx torusguard report --html"))
601
+ print(box_line(f"3. Inspect findings: .torusguard/runs/{run_folder.name}/findings.md"))
602
+ print(border_bottom())
603
+ print()
604
+
605
+ return {
606
+ "status": "success",
607
+ "run_folder": str(run_folder),
608
+ "bundles_count": len(bundles),
609
+ "bundles": bundles
610
+ }
611
+
612
+
613
+ def main():
614
+ parser = argparse.ArgumentParser(description="TorusGuard Autonomous Governed Remediation Engine")
615
+ parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
616
+ parser.add_argument("--run", "-r", help="Explicit run ID to harden (default: latest audit run)")
617
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
618
+ args = parser.parse_args()
619
+
620
+ target = Path(args.path).resolve()
621
+ result = execute_harden(target, run_id_arg=args.run)
622
+
623
+ if args.json:
624
+ print(json.dumps(result, indent=2))
625
+
626
+
627
+ if __name__ == "__main__":
628
+ main()