torusguard 1.3.4 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/.torusguard/.manifest.json +66 -63
  2. package/.torusguard/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
  3. package/.torusguard/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
  4. package/.torusguard/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
  5. package/.torusguard/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
  6. package/.torusguard/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
  7. package/.torusguard/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
  8. package/.torusguard/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
  9. package/.torusguard/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
  10. package/.torusguard/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
  11. package/.torusguard/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
  12. package/.torusguard/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
  13. package/.torusguard/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
  14. package/.torusguard/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
  15. package/.torusguard/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
  16. package/.torusguard/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
  17. package/.torusguard/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
  18. package/.torusguard/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
  19. package/.torusguard/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
  20. package/.torusguard/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
  21. package/.torusguard/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
  22. package/.torusguard/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
  23. package/.torusguard/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
  24. package/.torusguard/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
  25. package/.torusguard/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
  26. package/.torusguard/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
  27. package/.torusguard/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
  28. package/.torusguard/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
  29. package/.torusguard/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
  30. package/.torusguard/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
  31. package/.torusguard/rules_catalog.json +520 -0
  32. package/.torusguard/runs/report-latest.html +2700 -232
  33. package/.torusguard/runs/results-latest.sarif +20 -0
  34. package/.torusguard/runs/run-20260912-214110-audit/findings.json +1256 -0
  35. package/.torusguard/runs/run-20260912-214110-audit/findings.md +534 -0
  36. package/.torusguard/runs/run-20260912-214110-audit/manifest.json +12 -0
  37. package/.torusguard/runs/run-20260912-214110-audit/summary.md +5 -0
  38. package/.torusguard/runs/run-20260912-214813-audit/findings.json +992 -0
  39. package/.torusguard/runs/run-20260912-214813-audit/findings.md +422 -0
  40. package/.torusguard/runs/run-20260912-214813-audit/manifest.json +12 -0
  41. package/.torusguard/runs/run-20260912-214813-audit/summary.md +5 -0
  42. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/metadata.json +17 -0
  43. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/minimal_patch_plan.md +21 -0
  44. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/patch.diff +9 -0
  45. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
  46. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  47. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  48. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
  49. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
  50. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
  51. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
  52. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
  53. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
  54. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
  55. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
  56. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
  57. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
  58. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
  59. package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
  60. package/.torusguard/runs/run-20260912-215555-audit/findings.json +530 -0
  61. package/.torusguard/runs/run-20260912-215555-audit/findings.md +232 -0
  62. package/.torusguard/runs/run-20260912-215555-audit/manifest.json +13 -0
  63. package/.torusguard/runs/run-20260912-215555-audit/remediation.md +102 -0
  64. package/.torusguard/runs/run-20260912-215555-audit/summary.md +5 -0
  65. package/.torusguard/runs/run-20260912-215731-audit/apply_plan.md +6 -0
  66. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/metadata.json +17 -0
  67. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/minimal_patch_plan.md +21 -0
  68. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/patch.diff +9 -0
  69. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/metadata.json +17 -0
  70. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/minimal_patch_plan.md +21 -0
  71. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/patch.diff +9 -0
  72. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/metadata.json +17 -0
  73. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/minimal_patch_plan.md +22 -0
  74. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/patch.diff +10 -0
  75. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
  76. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  77. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  78. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
  79. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
  80. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
  81. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/metadata.json +17 -0
  82. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/minimal_patch_plan.md +23 -0
  83. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/patch.diff +11 -0
  84. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/metadata.json +17 -0
  85. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/minimal_patch_plan.md +23 -0
  86. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/patch.diff +11 -0
  87. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
  88. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
  89. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
  90. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/metadata.json +17 -0
  91. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/minimal_patch_plan.md +21 -0
  92. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/patch.diff +9 -0
  93. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/metadata.json +17 -0
  94. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/minimal_patch_plan.md +21 -0
  95. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/patch.diff +9 -0
  96. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
  97. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
  98. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
  99. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/metadata.json +17 -0
  100. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/minimal_patch_plan.md +21 -0
  101. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/patch.diff +9 -0
  102. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/metadata.json +17 -0
  103. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/minimal_patch_plan.md +21 -0
  104. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/patch.diff +9 -0
  105. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
  106. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
  107. package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
  108. package/.torusguard/runs/run-20260912-215731-audit/diff_summary.md +186 -0
  109. package/.torusguard/runs/run-20260912-215731-audit/findings.json +497 -0
  110. package/.torusguard/runs/run-20260912-215731-audit/findings.md +218 -0
  111. package/.torusguard/runs/run-20260912-215731-audit/manifest.json +14 -0
  112. package/.torusguard/runs/run-20260912-215731-audit/recheck.md +76 -0
  113. package/.torusguard/runs/run-20260912-215731-audit/remediation.md +234 -0
  114. package/.torusguard/runs/run-20260912-215731-audit/summary.md +5 -0
  115. package/.torusguard/runs/run-20260912-220134-audit/findings.json +68 -0
  116. package/.torusguard/runs/run-20260912-220134-audit/findings.md +36 -0
  117. package/.torusguard/runs/run-20260912-220134-audit/manifest.json +12 -0
  118. package/.torusguard/runs/run-20260912-220134-audit/summary.md +5 -0
  119. package/.torusguard/runs/run-20260912-220200-audit/findings.json +1 -0
  120. package/.torusguard/runs/run-20260912-220200-audit/findings.md +8 -0
  121. package/.torusguard/runs/run-20260912-220200-audit/manifest.json +12 -0
  122. package/.torusguard/runs/run-20260912-220200-audit/recheck.md +6 -0
  123. package/.torusguard/runs/run-20260912-220200-audit/summary.md +5 -0
  124. package/.torusguard/runs/run-20260912-220227-audit/findings.json +1 -0
  125. package/.torusguard/runs/run-20260912-220227-audit/findings.md +8 -0
  126. package/.torusguard/runs/run-20260912-220227-audit/manifest.json +12 -0
  127. package/.torusguard/runs/run-20260912-220227-audit/recheck.md +6 -0
  128. package/.torusguard/runs/run-20260912-220227-audit/summary.md +5 -0
  129. package/.torusguard/runs/run-20260918-104221-audit/findings.json +1 -0
  130. package/.torusguard/runs/run-20260918-104221-audit/findings.md +8 -0
  131. package/.torusguard/runs/run-20260918-104221-audit/manifest.json +12 -0
  132. package/.torusguard/runs/run-20260918-104221-audit/summary.md +5 -0
  133. package/.torusguard/runs/run-20260918-104236-audit/findings.json +2314 -0
  134. package/.torusguard/runs/run-20260918-104236-audit/findings.md +960 -0
  135. package/.torusguard/runs/run-20260918-104236-audit/manifest.json +12 -0
  136. package/.torusguard/runs/run-20260918-104236-audit/summary.md +5 -0
  137. package/.torusguard/runs/run-20260918-105723-audit/findings.json +2382 -0
  138. package/.torusguard/runs/run-20260918-105723-audit/findings.md +960 -0
  139. package/.torusguard/runs/run-20260918-105723-audit/manifest.json +12 -0
  140. package/.torusguard/runs/run-20260918-105723-audit/summary.md +5 -0
  141. package/.torusguard/runs/run-20260918-105731-audit/findings.json +1 -0
  142. package/.torusguard/runs/run-20260918-105731-audit/findings.md +8 -0
  143. package/.torusguard/runs/run-20260918-105731-audit/manifest.json +12 -0
  144. package/.torusguard/runs/run-20260918-105731-audit/summary.md +5 -0
  145. package/.torusguard/runs/run-20260918-105940-audit/findings.json +1 -0
  146. package/.torusguard/runs/run-20260918-105940-audit/findings.md +8 -0
  147. package/.torusguard/runs/run-20260918-105940-audit/manifest.json +12 -0
  148. package/.torusguard/runs/run-20260918-105940-audit/summary.md +5 -0
  149. package/.torusguard/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  150. package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  151. package/.torusguard/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  152. package/.torusguard/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  153. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  154. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  155. package/.torusguard/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  156. package/.torusguard/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  157. package/.torusguard/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  158. package/.torusguard/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  159. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  160. package/.torusguard/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  161. package/.torusguard/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  162. package/.torusguard/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  163. package/.torusguard/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  164. package/.torusguard/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  165. package/.torusguard/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  166. package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  167. package/.torusguard/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  168. package/.torusguard/scripts/apply_runner.py +389 -377
  169. package/.torusguard/scripts/audit_runner.py +1513 -653
  170. package/.torusguard/scripts/finding_scorer.py +14 -2
  171. package/.torusguard/scripts/harden_runner.py +628 -455
  172. package/.torusguard/scripts/html_reporter.py +2668 -648
  173. package/.torusguard/scripts/recheck_runner.py +248 -238
  174. package/.torusguard/scripts/recipes_runner.py +137 -137
  175. package/.torusguard/scripts/report_sync.py +719 -0
  176. package/.torusguard/scripts/sarif_exporter.py +156 -116
  177. package/.torusguard/scripts/stack_detect.py +44 -2
  178. package/.torusguard/scripts/term_ui.py +162 -162
  179. package/.torusguard/skills/torusguard/SKILL.md +75 -72
  180. package/.torusguard/skills/torusguard-apply/SKILL.md +84 -80
  181. package/.torusguard/skills/torusguard-audit/SKILL.md +87 -83
  182. package/.torusguard/skills/torusguard-authorize/SKILL.md +7 -2
  183. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +7 -2
  184. package/.torusguard/skills/torusguard-full/SKILL.md +7 -2
  185. package/.torusguard/skills/torusguard-harden/SKILL.md +98 -94
  186. package/.torusguard/skills/torusguard-init/SKILL.md +68 -64
  187. package/.torusguard/skills/torusguard-recheck/SKILL.md +81 -77
  188. package/.torusguard/skills/torusguard-report/SKILL.md +93 -72
  189. package/.torusguard/skills/torusguard-status/SKILL.md +62 -58
  190. package/.torusguard/skills/torusguard-verify/SKILL.md +7 -2
  191. package/.torusguard/skills/torusguard-web-validate/SKILL.md +7 -2
  192. package/.torusguard/snapshots/run-20260912-215731-audit/demo/playground/vulnerable_nextjs/actions.ts.bak +24 -0
  193. package/.torusguard/snapshots/run-20260912-215731-audit/examples/hardened-react-express/server/index.js.bak +121 -0
  194. package/.torusguard/snapshots/run-20260912-215731-audit/examples/python/fastapi-vuln/main.py.bak +20 -0
  195. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/src/App.jsx.bak +63 -0
  196. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/vite.config.js.bak +12 -0
  197. package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/server/index.js.bak +82 -0
  198. package/.torusguard/workflows/apply.md +11 -4
  199. package/.torusguard/workflows/audit.md +7 -1
  200. package/.torusguard/workflows/authorize.md +2 -1
  201. package/.torusguard/workflows/exploit-check.md +2 -1
  202. package/.torusguard/workflows/harden.md +7 -1
  203. package/.torusguard/workflows/init.md +6 -1
  204. package/.torusguard/workflows/memory.md +2 -1
  205. package/.torusguard/workflows/recheck.md +7 -1
  206. package/.torusguard/workflows/report.md +7 -1
  207. package/.torusguard/workflows/status.md +6 -1
  208. package/.torusguard/workflows/verify.md +6 -1
  209. package/.torusguard/workflows/web-validate.md +2 -1
  210. package/README.md +476 -345
  211. package/bin/torusguard.js +586 -545
  212. package/package.json +1 -1
  213. package/skills/torusguard/SKILL.md +73 -72
  214. package/skills/torusguard/bootstrap.py +7 -2
  215. package/skills/torusguard/payload/.manifest.json +66 -63
  216. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
  217. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
  218. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
  219. package/skills/torusguard/payload/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
  220. package/skills/torusguard/payload/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
  221. package/skills/torusguard/payload/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
  222. package/skills/torusguard/payload/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
  223. package/skills/torusguard/payload/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
  224. package/skills/torusguard/payload/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
  225. package/skills/torusguard/payload/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
  226. package/skills/torusguard/payload/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
  227. package/skills/torusguard/payload/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
  228. package/skills/torusguard/payload/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
  229. package/skills/torusguard/payload/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
  230. package/skills/torusguard/payload/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
  231. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
  232. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
  233. package/skills/torusguard/payload/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
  234. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
  235. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
  236. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
  237. package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
  238. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
  239. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
  240. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
  241. package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
  242. package/skills/torusguard/payload/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
  243. package/skills/torusguard/payload/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
  244. package/skills/torusguard/payload/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
  245. package/skills/torusguard/payload/rules_catalog.json +520 -0
  246. package/skills/torusguard/payload/scripts/__pycache__/apply_runner.cpython-314.pyc +0 -0
  247. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  248. package/skills/torusguard/payload/scripts/__pycache__/audit_runner.cpython-314.pyc +0 -0
  249. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  250. package/skills/torusguard/payload/scripts/__pycache__/diff_guard.cpython-314.pyc +0 -0
  251. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  252. package/skills/torusguard/payload/scripts/__pycache__/finding_scorer.cpython-314.pyc +0 -0
  253. package/skills/torusguard/payload/scripts/__pycache__/harden_runner.cpython-314.pyc +0 -0
  254. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  255. package/skills/torusguard/payload/scripts/__pycache__/html_reporter.cpython-314.pyc +0 -0
  256. package/skills/torusguard/payload/scripts/__pycache__/manifest_builder.cpython-314.pyc +0 -0
  257. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  258. package/skills/torusguard/payload/scripts/__pycache__/memory_engine.cpython-314.pyc +0 -0
  259. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  260. package/skills/torusguard/payload/scripts/__pycache__/monorepo_detector.cpython-314.pyc +0 -0
  261. package/skills/torusguard/payload/scripts/__pycache__/recheck_runner.cpython-314.pyc +0 -0
  262. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
  263. package/skills/torusguard/payload/scripts/__pycache__/report_sync.cpython-314.pyc +0 -0
  264. package/skills/torusguard/payload/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  265. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-311.pyc +0 -0
  266. package/skills/torusguard/payload/scripts/__pycache__/run_manager.cpython-314.pyc +0 -0
  267. package/skills/torusguard/payload/scripts/__pycache__/sarif_exporter.cpython-314.pyc +0 -0
  268. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  269. package/skills/torusguard/payload/scripts/__pycache__/stack_detect.cpython-314.pyc +0 -0
  270. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  271. package/skills/torusguard/payload/scripts/__pycache__/term_ui.cpython-314.pyc +0 -0
  272. package/skills/torusguard/payload/scripts/apply_runner.py +389 -377
  273. package/skills/torusguard/payload/scripts/audit_runner.py +1513 -653
  274. package/skills/torusguard/payload/scripts/finding_scorer.py +14 -2
  275. package/skills/torusguard/payload/scripts/harden_runner.py +628 -455
  276. package/skills/torusguard/payload/scripts/html_reporter.py +2668 -648
  277. package/skills/torusguard/payload/scripts/recheck_runner.py +248 -238
  278. package/skills/torusguard/payload/scripts/recipes_runner.py +137 -137
  279. package/skills/torusguard/payload/scripts/report_sync.py +719 -0
  280. package/skills/torusguard/payload/scripts/sarif_exporter.py +156 -116
  281. package/skills/torusguard/payload/scripts/stack_detect.py +44 -2
  282. package/skills/torusguard/payload/scripts/term_ui.py +162 -162
  283. package/skills/torusguard/payload/skills/torusguard/SKILL.md +75 -72
  284. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +84 -80
  285. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +87 -83
  286. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +7 -2
  287. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +7 -2
  288. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +7 -2
  289. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +98 -94
  290. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +68 -64
  291. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +81 -77
  292. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +93 -72
  293. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +62 -58
  294. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +7 -2
  295. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +7 -2
  296. package/skills/torusguard/payload/workflows/apply.md +11 -4
  297. package/skills/torusguard/payload/workflows/audit.md +7 -1
  298. package/skills/torusguard/payload/workflows/authorize.md +2 -1
  299. package/skills/torusguard/payload/workflows/exploit-check.md +2 -1
  300. package/skills/torusguard/payload/workflows/harden.md +7 -1
  301. package/skills/torusguard/payload/workflows/init.md +6 -1
  302. package/skills/torusguard/payload/workflows/memory.md +2 -1
  303. package/skills/torusguard/payload/workflows/recheck.md +7 -1
  304. package/skills/torusguard/payload/workflows/report.md +7 -1
  305. package/skills/torusguard/payload/workflows/status.md +6 -1
  306. package/skills/torusguard/payload/workflows/verify.md +6 -1
  307. package/skills/torusguard/payload/workflows/web-validate.md +2 -1
  308. package/skills/torusguard-apply/SKILL.md +84 -80
  309. package/skills/torusguard-audit/SKILL.md +87 -83
  310. package/skills/torusguard-authorize/SKILL.md +7 -2
  311. package/skills/torusguard-exploit-check/SKILL.md +7 -2
  312. package/skills/torusguard-full/SKILL.md +7 -2
  313. package/skills/torusguard-harden/SKILL.md +98 -94
  314. package/skills/torusguard-init/SKILL.md +68 -64
  315. package/skills/torusguard-recheck/SKILL.md +81 -77
  316. package/skills/torusguard-report/SKILL.md +93 -72
  317. package/skills/torusguard-status/SKILL.md +62 -58
  318. package/skills/torusguard-verify/SKILL.md +7 -2
  319. package/skills/torusguard-web-validate/SKILL.md +7 -2
@@ -0,0 +1,719 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Living Security Report Sync Engine (v1.3.6)
4
+ Maintains a living, ground-truth `security_report.md` ledger at the root of the workspace.
5
+ Tracks finding lifecycles from Discovery -> Candidate -> Applied -> Resolved -> Regressed.
6
+ Pure Python 3.10+ standard library (zero external dependencies).
7
+ """
8
+
9
+ import sys
10
+ import os
11
+ import re
12
+ import json
13
+ import time
14
+ import datetime
15
+ from pathlib import Path
16
+ from typing import Dict, List, Any, Optional, Tuple
17
+
18
+ # Windows console UTF-8 support
19
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
20
+ try:
21
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
22
+ except Exception:
23
+ pass
24
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
25
+ try:
26
+ getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
27
+ except Exception:
28
+ pass
29
+
30
+ # Timezone: 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 timezone-aware datetime in IST."""
35
+ return datetime.datetime.now(IST)
36
+
37
+ def format_timestamp(dt: Optional[datetime.datetime] = None) -> str:
38
+ if dt is None:
39
+ dt = get_ist_now()
40
+ return dt.strftime("%Y-%m-%d %H:%M:%S IST")
41
+
42
+ def get_report_path(target_root: Path) -> Path:
43
+ """Return absolute path to security_report.md at the workspace root."""
44
+ return target_root / "security_report.md"
45
+
46
+
47
+ def atomic_write_text(file_path: Path, content: str, max_retries: int = 3, backoff_base: float = 0.05) -> bool:
48
+ """
49
+ Atomically write text to disk using a temporary file and atomic replace.
50
+ Handles Windows file-locking (e.g. browser read locks on open HTML reports)
51
+ via exponential backoff retries.
52
+ """
53
+ file_path = Path(file_path).resolve()
54
+ file_path.parent.mkdir(parents=True, exist_ok=True)
55
+
56
+ pid = os.getpid()
57
+ ts = int(time.time() * 1000)
58
+ tmp_path = file_path.with_name(f".tmp.{pid}.{ts}.{file_path.name}")
59
+
60
+ try:
61
+ with open(tmp_path, "w", encoding="utf-8") as f:
62
+ f.write(content)
63
+ f.flush()
64
+ os.fsync(f.fileno())
65
+ except Exception as e:
66
+ if tmp_path.exists():
67
+ try:
68
+ tmp_path.unlink()
69
+ except Exception:
70
+ pass
71
+ raise e
72
+
73
+ for attempt in range(max_retries):
74
+ try:
75
+ os.replace(tmp_path, file_path)
76
+ return True
77
+ except (PermissionError, OSError):
78
+ if attempt == max_retries - 1:
79
+ try:
80
+ file_path.write_text(content, encoding="utf-8")
81
+ if tmp_path.exists():
82
+ tmp_path.unlink()
83
+ return True
84
+ except Exception:
85
+ if tmp_path.exists():
86
+ try:
87
+ tmp_path.unlink()
88
+ except Exception:
89
+ pass
90
+ raise
91
+ time.sleep(backoff_base * (2 ** attempt))
92
+
93
+ return False
94
+
95
+
96
+ def reconcile_from_run_artifacts(target_root: Path) -> List[Dict[str, Any]]:
97
+ """
98
+ Authoritative fallback: if security_report.md is missing or damaged,
99
+ heal state from the latest run's findings.json in .torusguard/runs/.
100
+ """
101
+ runs_dir = target_root / ".torusguard" / "runs"
102
+ if not runs_dir.is_dir():
103
+ return []
104
+
105
+ run_folders = sorted(
106
+ [d for d in runs_dir.iterdir() if d.is_dir() and (d / "findings.json").is_file()],
107
+ key=lambda p: p.stat().st_mtime,
108
+ reverse=True
109
+ )
110
+ if not run_folders:
111
+ return []
112
+
113
+ latest_file = run_folders[0] / "findings.json"
114
+ try:
115
+ data = json.loads(latest_file.read_text(encoding="utf-8"))
116
+ if isinstance(data, list):
117
+ return data
118
+ except Exception:
119
+ pass
120
+ return []
121
+
122
+
123
+ def parse_existing_report(report_path: Path) -> Dict[str, Any]:
124
+ """Parse existing security_report.md into structured data if it exists.
125
+ Supports both the legacy format and the new v2 summary-first format.
126
+ Heals from latest run findings if report is absent or damaged.
127
+ """
128
+ if not report_path.is_file():
129
+ recovered = reconcile_from_run_artifacts(report_path.parent)
130
+ if recovered:
131
+ f_map = {}
132
+ for r in recovered:
133
+ fid = r.get("finding_id", "")
134
+ if fid:
135
+ f_map[fid] = {
136
+ "finding_id": fid,
137
+ "rule_id": r.get("rule_id", "TG-GEN"),
138
+ "title": r.get("title", "Security Finding"),
139
+ "status": "🔴 OPEN",
140
+ "severity": r.get("severity", "Medium"),
141
+ "confidence": f"{r.get('confidence_score', 70)}% ({r.get('confidence_band', 'High Confidence')})",
142
+ "target": f"{r.get('file_path', '')}:{r.get('line_number', 1)}",
143
+ "history": []
144
+ }
145
+ return {"findings": f_map, "metadata": {}}
146
+ return {"findings": {}, "metadata": {}}
147
+
148
+ try:
149
+ content = report_path.read_text(encoding="utf-8", errors="replace")
150
+ except Exception:
151
+ return {"findings": {}, "metadata": {}}
152
+
153
+ findings = {}
154
+
155
+ # ── Format A (legacy): ### [RULE-ID] Title ──
156
+ card_pattern_legacy = re.compile(
157
+ r'###\s+\[(TG-[A-Z]+-\d+)\]\s+(.*?)\n'
158
+ r'- \*\*Finding ID:\*\*\s+`([^`]+)`\n'
159
+ r'- \*\*Status:\*\*\s+([^\n]+)\n'
160
+ r'- \*\*Severity:\*\*\s+([^\n|]+)(?:\|\s+\*\*Confidence:\*\*\s+([^\n]+))?\n'
161
+ r'- \*\*Target:\*\*\s+`([^`]+)`',
162
+ re.MULTILINE
163
+ )
164
+
165
+ for m in card_pattern_legacy.finditer(content):
166
+ rule_id = m.group(1).strip()
167
+ title = m.group(2).strip()
168
+ finding_id = m.group(3).strip()
169
+ status = m.group(4).strip()
170
+ severity = m.group(5).strip()
171
+ confidence = m.group(6).strip() if m.group(6) else "70% (High Confidence)"
172
+ target = m.group(7).strip()
173
+
174
+ findings[finding_id] = {
175
+ "finding_id": finding_id,
176
+ "rule_id": rule_id,
177
+ "title": title,
178
+ "status": status,
179
+ "severity": severity,
180
+ "confidence": confidence,
181
+ "target": target,
182
+ "history": []
183
+ }
184
+
185
+ # ── Format B (new v2): ### TG-RULE-001 · Title `status` `confidence` ──
186
+ card_pattern_v2 = re.compile(
187
+ r'###\s+(TG-[A-Z]+-\d+)\s+·\s+(.*?)\s+`([^`]+)`\s+`([^`]+)`\n'
188
+ r'- \*\*Finding ID:\*\*\s+`([^`]+)`(?:[^\n]*\*\*Severity:\*\*\s+`([^`]+)`)?\n'
189
+ r'(?:📁|- \*\*Target:\*\*)\s*`([^`\n]+)`',
190
+ re.MULTILINE
191
+ )
192
+
193
+ for m in card_pattern_v2.finditer(content):
194
+ rule_id = m.group(1).strip()
195
+ title = m.group(2).strip()
196
+ status = m.group(3).strip()
197
+ confidence = m.group(4).strip()
198
+ finding_id = m.group(5).strip()
199
+ sev_explicit = m.group(6).strip() if m.group(6) else None
200
+ target = m.group(7).strip()
201
+
202
+ if finding_id not in findings: # Don't overwrite legacy matches
203
+ if sev_explicit:
204
+ severity = sev_explicit
205
+ else:
206
+ # Infer severity from section header or status
207
+ severity = "High" # Default
208
+ match_pos = m.start()
209
+ preceding = content[:match_pos]
210
+ if "Critical" in preceding.split("##")[-1]:
211
+ severity = "Critical"
212
+ elif "Medium" in preceding.split("##")[-1]:
213
+ severity = "Medium"
214
+
215
+ findings[finding_id] = {
216
+ "finding_id": finding_id,
217
+ "rule_id": rule_id,
218
+ "title": title,
219
+ "status": status,
220
+ "severity": severity,
221
+ "confidence": confidence,
222
+ "target": target,
223
+ "history": []
224
+ }
225
+
226
+ # Extract history from inline blockquote format (new) or table format (legacy)
227
+ # Legacy: #### Lifecycle History for `finding_id`
228
+ history_pattern = re.compile(r'#### Lifecycle History for `([^`]+)`\n([\s\S]*?)(?=\n---|\n###|\Z)')
229
+ for hm in history_pattern.finditer(content):
230
+ fid = hm.group(1).strip()
231
+ table_text = hm.group(2).strip()
232
+ if fid in findings:
233
+ for row in table_text.splitlines():
234
+ if row.startswith("|") and not row.startswith("| Timestamp"):
235
+ cols = [c.strip().strip("`") for c in row.split("|")[1:-1]]
236
+ if len(cols) >= 4 and not cols[0].startswith(":") and not cols[0].startswith("-"):
237
+ findings[fid]["history"].append({
238
+ "timestamp": cols[0],
239
+ "action": cols[1],
240
+ "run_id": cols[2],
241
+ "details": cols[3]
242
+ })
243
+
244
+ # New format: > Last: Action — timestamp · `run_id`
245
+ inline_history = re.compile(
246
+ r'###\s+TG-[A-Z]+-\d+.*?\n.*?Finding ID:\*\*\s+`([^`]+)`.*?'
247
+ r'>\s+Last:\s+(.*?)\s+—\s+(.*?)\s+·\s+`([^`]+)`',
248
+ re.DOTALL
249
+ )
250
+ for hm in inline_history.finditer(content):
251
+ fid = hm.group(1).strip()
252
+ if fid in findings and not findings[fid]["history"]:
253
+ findings[fid]["history"].append({
254
+ "timestamp": hm.group(3).strip(),
255
+ "action": hm.group(2).strip(),
256
+ "run_id": hm.group(4).strip(),
257
+ "details": "Restored from inline history"
258
+ })
259
+
260
+
261
+ return {"findings": findings, "raw_content": content}
262
+
263
+
264
+ def parse_metadata_block(content: str) -> Dict[str, Any]:
265
+ """Parse the YAML metadata block between TORUSGUARD_METADATA_START/END markers."""
266
+ metadata = {}
267
+ meta_match = re.search(
268
+ r'<!-- TORUSGUARD_METADATA_START\s*\n(.*?)\nTORUSGUARD_METADATA_END -->',
269
+ content, re.DOTALL
270
+ )
271
+ if meta_match:
272
+ for line in meta_match.group(1).strip().splitlines():
273
+ if ':' in line:
274
+ key, _, val = line.partition(':')
275
+ metadata[key.strip()] = val.strip()
276
+ return metadata
277
+
278
+ def compute_health_score(findings: List[Dict[str, Any]]) -> Tuple[int, str]:
279
+ """
280
+ Compute security health score (0-100) based on remaining open/regressed findings.
281
+ Returns (score, status_text).
282
+ """
283
+ critical_count = sum(1 for f in findings if f["severity"].lower() == "critical" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
284
+ high_count = sum(1 for f in findings if f["severity"].lower() == "high" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
285
+ medium_count = sum(1 for f in findings if f["severity"].lower() == "medium" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
286
+ low_count = sum(1 for f in findings if f["severity"].lower() == "low" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
287
+
288
+ penalty = (critical_count * 25) + (high_count * 15) + (medium_count * 5) + (low_count * 2)
289
+ score = max(0, min(100, 100 - penalty))
290
+
291
+ if score == 100:
292
+ status = "🟢 HARDENED & SECURE"
293
+ elif score >= 80:
294
+ status = "🟡 MODERATE RISK — ACTION RECOMMENDED"
295
+ elif score >= 50:
296
+ status = "🟠 ELEVATED RISK — PATCHES REQUIRED"
297
+ else:
298
+ status = "🔴 CRITICAL RISK — IMMEDIATE INTERVENTION REQUIRED"
299
+
300
+ return score, status
301
+
302
+ def render_report(findings: List[Dict[str, Any]], target_root: Path) -> str:
303
+ """Render the living security_report.md with summary-first layout,
304
+ AI-parseable metadata, severity grouping, and concise finding cards."""
305
+ score, status_text = compute_health_score(findings)
306
+ now_str = format_timestamp()
307
+
308
+ # Counts
309
+ total = len(findings)
310
+ open_count = sum(1 for f in findings if "OPEN" in f["status"])
311
+ verified_count = sum(1 for f in findings if "VERIFIED" in f["status"])
312
+ candidate_count = sum(1 for f in findings if "CANDIDATE" in f["status"])
313
+ applied_count = sum(1 for f in findings if "APPLIED" in f["status"])
314
+ resolved_count = sum(1 for f in findings if "RESOLVED" in f["status"])
315
+ regressed_count = sum(1 for f in findings if "REGRESSED" in f["status"])
316
+ fp_count = sum(1 for f in findings if "FALSE POSITIVE" in f["status"])
317
+
318
+ crit_count = sum(1 for f in findings if f["severity"].lower() == "critical")
319
+ high_count = sum(1 for f in findings if f["severity"].lower() == "high")
320
+ med_count = sum(1 for f in findings if f["severity"].lower() == "medium")
321
+ low_count = sum(1 for f in findings if f["severity"].lower() == "low")
322
+
323
+ crit_open = sum(1 for f in findings if f["severity"].lower() == "critical" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
324
+ high_open = sum(1 for f in findings if f["severity"].lower() == "high" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
325
+ med_open = sum(1 for f in findings if f["severity"].lower() == "medium" and ("OPEN" in f["status"] or "REGRESSED" in f["status"]))
326
+
327
+ # Sort findings by status priority and severity
328
+ sev_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
329
+ def sort_key(f):
330
+ is_active = 0 if ("OPEN" in f["status"] or "REGRESSED" in f["status"]) else 1
331
+ s_rank = sev_rank.get(f["severity"].lower(), 9)
332
+ return (is_active, s_rank, f["rule_id"])
333
+
334
+ sorted_findings = sorted(findings, key=sort_key)
335
+
336
+ # Group by severity for active findings
337
+ active = [f for f in sorted_findings if "OPEN" in f["status"] or "REGRESSED" in f["status"]]
338
+ resolved = [f for f in sorted_findings if "RESOLVED" in f["status"]]
339
+ in_progress = [f for f in sorted_findings if any(s in f["status"] for s in ("CANDIDATE", "APPLIED", "VERIFIED"))]
340
+ suppressed = [f for f in sorted_findings if "FALSE POSITIVE" in f["status"]]
341
+
342
+ critical_findings = [f for f in active if f["severity"].lower() == "critical"]
343
+ high_findings = [f for f in active if f["severity"].lower() == "high"]
344
+ medium_findings = [f for f in active if f["severity"].lower() in ("medium", "low")]
345
+
346
+ # ── Build the report ──
347
+ lines = [
348
+ "# 🛡️ TorusGuard Security Report",
349
+ "",
350
+ "<!-- TORUSGUARD_METADATA_START",
351
+ f"health_score: {score}",
352
+ f"status: {status_text}",
353
+ f"total_findings: {total}",
354
+ f"critical: {crit_count}",
355
+ f"critical_open: {crit_open}",
356
+ f"high: {high_count}",
357
+ f"high_open: {high_open}",
358
+ f"medium: {med_count}",
359
+ f"medium_open: {med_open}",
360
+ f"low: {low_count}",
361
+ f"open: {open_count}",
362
+ f"resolved: {resolved_count}",
363
+ f"regressed: {regressed_count}",
364
+ f"suppressed: {fp_count}",
365
+ f"last_scan: {now_str}",
366
+ "TORUSGUARD_METADATA_END -->",
367
+ "",
368
+ "## Executive Summary",
369
+ "",
370
+ "| Metric | Value |",
371
+ "| :--- | :--- |",
372
+ f"| **Health Score** | `{score}/100` — {status_text} |",
373
+ f"| **Critical (Open)** | {crit_open} |",
374
+ f"| **High (Open)** | {high_open} |",
375
+ f"| **Medium (Open)** | {med_open} |",
376
+ f"| **Resolved** | {resolved_count} |",
377
+ f"| **In Progress** | {candidate_count + applied_count} |",
378
+ f"| **Last Scan** | {now_str} |",
379
+ "",
380
+ ]
381
+
382
+ # Action required callout
383
+ if crit_open > 0:
384
+ lines.append(f"> ⚠️ **Action Required:** {crit_open} critical finding{'s' if crit_open != 1 else ''} require immediate attention. Run `npx torusguard harden` to generate patches.")
385
+ lines.append("")
386
+
387
+ lines.append("---")
388
+ lines.append("")
389
+
390
+ # ── Critical Findings Section ──
391
+ if critical_findings:
392
+ lines.append(f"## 🔴 Critical Findings ({len(critical_findings)})")
393
+ lines.append("")
394
+ for f in critical_findings:
395
+ lines.extend(_render_finding_card(f))
396
+
397
+ # ── High Findings Section ──
398
+ if high_findings:
399
+ lines.append(f"## 🟠 High Findings ({len(high_findings)})")
400
+ lines.append("")
401
+ for f in high_findings:
402
+ lines.extend(_render_finding_card(f))
403
+
404
+ # ── Medium/Low Findings Section ──
405
+ if medium_findings:
406
+ lines.append(f"## 🟡 Medium / Low Findings ({len(medium_findings)})")
407
+ lines.append("")
408
+ for f in medium_findings:
409
+ lines.extend(_render_finding_card(f))
410
+
411
+ # ── In Progress Section ──
412
+ if in_progress:
413
+ lines.append(f"## 🔵 In Progress ({len(in_progress)})")
414
+ lines.append("")
415
+ for f in in_progress:
416
+ lines.extend(_render_finding_card(f))
417
+
418
+ # ── Resolved Section ──
419
+ if resolved:
420
+ lines.append(f"## ✅ Resolved ({len(resolved)})")
421
+ lines.append("")
422
+ for f in resolved:
423
+ lines.extend(_render_finding_card(f))
424
+
425
+ # ── Suppressed Section ──
426
+ if suppressed:
427
+ lines.append(f"## ⚪ Suppressed ({len(suppressed)})")
428
+ lines.append("")
429
+ for f in suppressed:
430
+ lines.append(f"- **{f['rule_id']}** · {f['title']} — `{f['target']}` · False Positive")
431
+ lines.append("")
432
+ lines.append("---")
433
+ lines.append("")
434
+
435
+ # Footer
436
+ lines.append("---")
437
+ lines.append(f"*Generated by TorusGuard v1.3.6 · {now_str} · 100% Local-First*")
438
+ lines.append("")
439
+
440
+ return "\n".join(lines)
441
+
442
+
443
+ def _render_finding_card(f: Dict[str, Any]) -> List[str]:
444
+ """Render a concise finding card (4-6 lines max)."""
445
+ status_full = f["status"]
446
+ conf = f.get('confidence', '70% (High Confidence)')
447
+ sev = f.get('severity', 'High')
448
+ card = [
449
+ f"### {f['rule_id']} · {f['title']} `{status_full}` `{conf}`",
450
+ f"- **Finding ID:** `{f['finding_id']}` · **Severity:** `{sev}`",
451
+ f"📁 `{f['target']}`",
452
+ f"**Problem:** {f.get('what_is_wrong', f.get('description', 'Security violation detected.'))}",
453
+ ]
454
+ if f.get("what_should_change"):
455
+ card.append(f"**Fix:** {f['what_should_change']}")
456
+ if f.get("proposed_diff"):
457
+ card.append("")
458
+ card.append("```diff")
459
+ card.append(f["proposed_diff"].strip())
460
+ card.append("```")
461
+ card.append("")
462
+ # Include lifecycle history inline (compact)
463
+ history = f.get("history", [])
464
+ if history and len(history) > 0:
465
+ last_event = history[-1]
466
+ card.append(f"> Last: {last_event.get('action', '?')} — {last_event.get('timestamp', '?')} · `{last_event.get('run_id', '?')}`")
467
+ card.append("")
468
+ card.append("---")
469
+ card.append("")
470
+ return card
471
+
472
+
473
+ def record_audit_findings(target_root: Path, findings: List[Dict[str, Any]], run_id: str):
474
+ """
475
+ Called at the conclusion of `npx torusguard audit`.
476
+ Updates security_report.md with new findings or records re-appearance.
477
+ """
478
+ report_file = get_report_path(target_root)
479
+ existing = parse_existing_report(report_file)
480
+ existing_map = existing["findings"]
481
+
482
+ now_str = format_timestamp()
483
+ current_fids = set()
484
+
485
+ for f in findings:
486
+ fid = f.get("finding_id", f"{f['rule_id']}-{f['line_number']}")
487
+ current_fids.add(fid)
488
+ rel_target = f"{f['file_path']}:{f['line_number']}"
489
+
490
+ if fid in existing_map:
491
+ # Finding already known
492
+ item = existing_map[fid]
493
+ item["target"] = rel_target
494
+ item["snippet"] = f.get("snippet", "")
495
+ item["description"] = f.get("description", "")
496
+
497
+ # If it was RESOLVED or APPLIED and reappeared, mark as REGRESSED
498
+ if "RESOLVED" in item["status"] or "APPLIED" in item["status"]:
499
+ item["status"] = "❌ REGRESSED"
500
+ item["history"].append({
501
+ "timestamp": now_str,
502
+ "action": "Regression Detected",
503
+ "run_id": run_id,
504
+ "details": "Vulnerability pattern re-detected in source code after prior closure."
505
+ })
506
+ else:
507
+ # Brand new finding
508
+ conf_str = f"{f.get('confidence_score', 70)}% ({f.get('confidence_band', 'High Confidence')})"
509
+ existing_map[fid] = {
510
+ "finding_id": fid,
511
+ "rule_id": f["rule_id"],
512
+ "title": f["title"],
513
+ "status": "🔴 OPEN",
514
+ "severity": f["severity"],
515
+ "confidence": conf_str,
516
+ "target": rel_target,
517
+ "cluster": f.get("cluster", "cluster-general"),
518
+ "what_is_wrong": f.get("description", ""),
519
+ "why_it_matters": "Violates TorusGuard strict production safety and isolation invariant.",
520
+ "what_should_change": f"Apply Ponytail-compliant patch via `npx torusguard harden`.",
521
+ "snippet": f.get("snippet", ""),
522
+ "history": [{
523
+ "timestamp": now_str,
524
+ "action": "Discovered",
525
+ "run_id": run_id,
526
+ "details": f"Initial discovery during static AST audit in {f['file_path']}."
527
+ }]
528
+ }
529
+
530
+ # For findings in existing report that were OPEN, APPLIED, or CANDIDATE and no longer detected, transition to RESOLVED
531
+ for fid, item in existing_map.items():
532
+ if fid not in current_fids and ("OPEN" in item["status"] or "APPLIED" in item["status"] or "CANDIDATE" in item["status"]):
533
+ item["status"] = "🟢 RESOLVED"
534
+ item["history"].append({
535
+ "timestamp": now_str,
536
+ "action": "Resolved",
537
+ "run_id": run_id,
538
+ "details": "Finding no longer detected in source code during latest audit pass."
539
+ })
540
+
541
+ # Render and save
542
+ rendered = render_report(list(existing_map.values()), target_root)
543
+ atomic_write_text(report_file, rendered)
544
+ sync_html_if_exists(target_root)
545
+ return report_file
546
+
547
+
548
+ def record_harden_bundles(target_root: Path, bundles: List[Dict[str, Any]], run_id: str):
549
+ """
550
+ Called at the conclusion of `npx torusguard harden`.
551
+ Transitions findings to CANDIDATE 🟡 and attaches minimal patch diff previews.
552
+ """
553
+ report_file = get_report_path(target_root)
554
+ existing = parse_existing_report(report_file)
555
+ existing_map = existing["findings"]
556
+ now_str = format_timestamp()
557
+
558
+ for b in bundles:
559
+ fid = b.get("finding_id")
560
+ rule_id = b.get("rule_id")
561
+ target = f"{b['target_file']}:{b['line_number']}"
562
+
563
+ # Match by finding_id or target
564
+ matched_item = None
565
+ if fid and fid in existing_map:
566
+ matched_item = existing_map[fid]
567
+ else:
568
+ for item in existing_map.values():
569
+ if item["rule_id"] == rule_id and item["target"] == target:
570
+ matched_item = item
571
+ break
572
+
573
+ if matched_item:
574
+ matched_item["status"] = "🟡 CANDIDATE"
575
+ matched_item["what_should_change"] = b.get("what_should_change", "")
576
+ matched_item["proposed_diff"] = b.get("proposed_diff", "")
577
+ matched_item["bundle_id"] = b.get("bundle_id", "")
578
+ matched_item["history"].append({
579
+ "timestamp": now_str,
580
+ "action": "Patch Formulated",
581
+ "run_id": run_id,
582
+ "details": f"Ponytail candidate bundle {b.get('bundle_id')} formulated (+{b.get('additions', 0)} / -{b.get('deletions', 0)} lines)."
583
+ })
584
+
585
+ rendered = render_report(list(existing_map.values()), target_root)
586
+ atomic_write_text(report_file, rendered)
587
+ sync_html_if_exists(target_root)
588
+ return report_file
589
+
590
+
591
+ def record_applied_patches(target_root: Path, applied_bundles: List[Dict[str, Any]], snapshot_dir: Path, run_id: str):
592
+ """
593
+ Called at the conclusion of `npx torusguard apply`.
594
+ Transitions findings to APPLIED 🔵 and records snapshot path.
595
+ """
596
+ report_file = get_report_path(target_root)
597
+ existing = parse_existing_report(report_file)
598
+ existing_map = existing["findings"]
599
+ now_str = format_timestamp()
600
+
601
+ for b in applied_bundles:
602
+ fid = b.get("finding_id")
603
+ rule_id = b.get("rule_id")
604
+ target = f"{b.get('target_file', '')}:{b.get('line_number', '')}"
605
+
606
+ matched_item = None
607
+ if fid and fid in existing_map:
608
+ matched_item = existing_map[fid]
609
+ else:
610
+ for item in existing_map.values():
611
+ if item["rule_id"] == rule_id and (target in item["target"] or item["target"] in target):
612
+ matched_item = item
613
+ break
614
+
615
+ if matched_item:
616
+ matched_item["status"] = "🔵 APPLIED"
617
+ matched_item["history"].append({
618
+ "timestamp": now_str,
619
+ "action": "Patch Applied",
620
+ "run_id": run_id,
621
+ "details": f"Candidate patch applied to disk. Pre-apply snapshot saved in {snapshot_dir.name}."
622
+ })
623
+
624
+ rendered = render_report(list(existing_map.values()), target_root)
625
+ atomic_write_text(report_file, rendered)
626
+ sync_html_if_exists(target_root)
627
+ return report_file
628
+
629
+
630
+ def record_recheck_results(target_root: Path, recheck_results: Dict[str, Any], run_id: str):
631
+ """
632
+ Called at the conclusion of `npx torusguard recheck` or `verify`.
633
+ Transitions findings to RESOLVED 🟢 or REGRESSED ❌.
634
+ """
635
+ report_file = get_report_path(target_root)
636
+ existing = parse_existing_report(report_file)
637
+ existing_map = existing["findings"]
638
+ now_str = format_timestamp()
639
+
640
+ fixed = recheck_results.get("fixed", [])
641
+ regressed = recheck_results.get("regressed", [])
642
+ remaining = recheck_results.get("remaining", [])
643
+
644
+ for f in fixed:
645
+ fid = f.get("finding_id")
646
+ if fid and fid in existing_map:
647
+ existing_map[fid]["status"] = "🟢 RESOLVED"
648
+ existing_map[fid]["history"].append({
649
+ "timestamp": now_str,
650
+ "action": "Closure Verified",
651
+ "run_id": run_id,
652
+ "details": "Targeted differential AST re-scan verified fix closure. Zero regressions."
653
+ })
654
+
655
+ for r in regressed:
656
+ fid = r.get("finding_id")
657
+ if fid and fid in existing_map:
658
+ existing_map[fid]["status"] = "❌ REGRESSED"
659
+ existing_map[fid]["history"].append({
660
+ "timestamp": now_str,
661
+ "action": "Regression Detected",
662
+ "run_id": run_id,
663
+ "details": "Vulnerability or new issue detected during recheck pass."
664
+ })
665
+
666
+ rendered = render_report(list(existing_map.values()), target_root)
667
+ atomic_write_text(report_file, rendered)
668
+ sync_html_if_exists(target_root)
669
+ return report_file
670
+
671
+
672
+ def record_rollback_results(target_root: Path, restored_files: List[str], run_id: str):
673
+ """
674
+ Called at the conclusion of `npx torusguard rollback`.
675
+ Reverts findings for restored files back to OPEN 🔴.
676
+ """
677
+ report_file = get_report_path(target_root)
678
+ existing = parse_existing_report(report_file)
679
+ existing_map = existing["findings"]
680
+ now_str = format_timestamp()
681
+
682
+ norm_restored = {rf.replace("\\", "/") for rf in restored_files}
683
+
684
+ for item in existing_map.values():
685
+ target_path = item.get("target", "").split(":")[0].replace("\\", "/")
686
+ if any(rf in target_path or target_path in rf for rf in norm_restored):
687
+ item["status"] = "🔴 OPEN"
688
+ item["history"].append({
689
+ "timestamp": now_str,
690
+ "action": "Rollback Restored",
691
+ "run_id": run_id,
692
+ "details": f"File {target_path} reverted from pre-apply snapshot. Status restored to OPEN."
693
+ })
694
+
695
+ rendered = render_report(list(existing_map.values()), target_root)
696
+ atomic_write_text(report_file, rendered)
697
+ sync_html_if_exists(target_root)
698
+ return report_file
699
+
700
+
701
+ def sync_html_if_exists(target_root: Path):
702
+ """
703
+ If the user has already generated report.html (at root or in .torusguard/runs),
704
+ refresh it so the HTML dashboard stays in lockstep with security_report.md.
705
+ """
706
+ root_html = target_root / "report.html"
707
+ runs_html = target_root / ".torusguard" / "runs" / "report-latest.html"
708
+ if root_html.is_file() or runs_html.is_file():
709
+ try:
710
+ s_dir = Path(__file__).resolve().parent
711
+ if str(s_dir) not in sys.path:
712
+ sys.path.insert(0, str(s_dir))
713
+ import html_reporter
714
+ if runs_html.is_file():
715
+ html_reporter.emit_html_report(target_path=runs_html, root_dir=target_root)
716
+ if root_html.is_file():
717
+ html_reporter.emit_html_report(target_path=root_html, root_dir=target_root)
718
+ except Exception:
719
+ pass