torusguard 1.3.4 → 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.
- package/.torusguard/.manifest.json +66 -63
- package/.torusguard/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
- package/.torusguard/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
- package/.torusguard/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
- package/.torusguard/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
- package/.torusguard/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
- package/.torusguard/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
- package/.torusguard/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
- package/.torusguard/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
- package/.torusguard/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
- package/.torusguard/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
- package/.torusguard/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
- package/.torusguard/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
- package/.torusguard/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
- package/.torusguard/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
- package/.torusguard/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
- package/.torusguard/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
- package/.torusguard/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
- package/.torusguard/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
- package/.torusguard/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
- package/.torusguard/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
- package/.torusguard/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
- package/.torusguard/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
- package/.torusguard/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
- package/.torusguard/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
- package/.torusguard/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
- package/.torusguard/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
- package/.torusguard/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
- package/.torusguard/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
- package/.torusguard/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
- package/.torusguard/rules_catalog.json +520 -0
- package/.torusguard/runs/report-latest.html +93 -7
- package/.torusguard/runs/run-20260912-214110-audit/findings.json +1256 -0
- package/.torusguard/runs/run-20260912-214110-audit/findings.md +534 -0
- package/.torusguard/runs/run-20260912-214110-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260912-214110-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260912-214813-audit/findings.json +992 -0
- package/.torusguard/runs/run-20260912-214813-audit/findings.md +422 -0
- package/.torusguard/runs/run-20260912-214813-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260912-214813-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-auth-003-49-7c96f3/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215555-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215555-audit/findings.json +530 -0
- package/.torusguard/runs/run-20260912-215555-audit/findings.md +232 -0
- package/.torusguard/runs/run-20260912-215555-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260912-215555-audit/remediation.md +102 -0
- package/.torusguard/runs/run-20260912-215555-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260912-215731-audit/apply_plan.md +6 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-client-001-7-587129/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-db-002-9-5f7850/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/minimal_patch_plan.md +22 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-001-27-2486f1/patch.diff +10 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-001-12-cf8156/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/minimal_patch_plan.md +23 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-17-876973/patch.diff +11 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/minimal_patch_plan.md +23 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-platform-002-5-40c794/patch.diff +11 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-001-26-affcfa/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-rate-002-54-0b38d2/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-18-a3c1e8/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-001-9-e22e50/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-28-a1a0a1/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-sec-004-30-00569e/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/metadata.json +17 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260912-215731-audit/bundles/bnd-tg-ssrf-004-9-9803ad/patch.diff +9 -0
- package/.torusguard/runs/run-20260912-215731-audit/diff_summary.md +186 -0
- package/.torusguard/runs/run-20260912-215731-audit/findings.json +497 -0
- package/.torusguard/runs/run-20260912-215731-audit/findings.md +218 -0
- package/.torusguard/runs/run-20260912-215731-audit/manifest.json +14 -0
- package/.torusguard/runs/run-20260912-215731-audit/recheck.md +76 -0
- package/.torusguard/runs/run-20260912-215731-audit/remediation.md +234 -0
- package/.torusguard/runs/run-20260912-215731-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260912-220134-audit/findings.json +68 -0
- package/.torusguard/runs/run-20260912-220134-audit/findings.md +36 -0
- package/.torusguard/runs/run-20260912-220134-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260912-220134-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260912-220200-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260912-220200-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260912-220200-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260912-220200-audit/recheck.md +6 -0
- package/.torusguard/runs/run-20260912-220200-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260912-220227-audit/findings.json +1 -0
- package/.torusguard/runs/run-20260912-220227-audit/findings.md +8 -0
- package/.torusguard/runs/run-20260912-220227-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260912-220227-audit/recheck.md +6 -0
- package/.torusguard/runs/run-20260912-220227-audit/summary.md +5 -0
- package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +383 -377
- package/.torusguard/scripts/audit_runner.py +1306 -653
- package/.torusguard/scripts/finding_scorer.py +14 -2
- package/.torusguard/scripts/harden_runner.py +628 -455
- package/.torusguard/scripts/recheck_runner.py +248 -238
- package/.torusguard/scripts/recipes_runner.py +137 -137
- package/.torusguard/scripts/report_sync.py +423 -0
- package/.torusguard/scripts/sarif_exporter.py +116 -116
- package/.torusguard/scripts/term_ui.py +162 -162
- package/.torusguard/snapshots/run-20260912-215731-audit/demo/playground/vulnerable_nextjs/actions.ts.bak +24 -0
- package/.torusguard/snapshots/run-20260912-215731-audit/examples/hardened-react-express/server/index.js.bak +121 -0
- package/.torusguard/snapshots/run-20260912-215731-audit/examples/python/fastapi-vuln/main.py.bak +20 -0
- package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/src/App.jsx.bak +63 -0
- package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/client/vite.config.js.bak +12 -0
- package/.torusguard/snapshots/run-20260912-215731-audit/examples/vulnerable-react-express/server/index.js.bak +82 -0
- package/.torusguard/workflows/apply.md +11 -4
- package/.torusguard/workflows/audit.md +7 -1
- package/.torusguard/workflows/authorize.md +2 -1
- package/.torusguard/workflows/exploit-check.md +2 -1
- package/.torusguard/workflows/harden.md +7 -1
- package/.torusguard/workflows/init.md +6 -1
- package/.torusguard/workflows/memory.md +2 -1
- package/.torusguard/workflows/recheck.md +7 -1
- package/.torusguard/workflows/report.md +7 -1
- package/.torusguard/workflows/status.md +6 -1
- package/.torusguard/workflows/verify.md +6 -1
- package/.torusguard/workflows/web-validate.md +2 -1
- package/README.md +470 -345
- package/bin/torusguard.js +550 -545
- package/package.json +1 -1
- package/skills/torusguard/SKILL.md +73 -72
- package/skills/torusguard/payload/.manifest.json +66 -63
- package/skills/torusguard/payload/rules/business-logic/TG-BIZ-001-client-controlled-sensitive-business-value.md +30 -14
- package/skills/torusguard/payload/rules/business-logic/TG-BIZ-002-replayable-one-time-operation.md +33 -16
- package/skills/torusguard/payload/rules/business-logic/TG-BIZ-003-missing-workflow-state-validation.md +31 -15
- package/skills/torusguard/payload/rules/business-logic/TG-BIZ-004-unrestricted-sensitive-business-flow.md +26 -16
- package/skills/torusguard/payload/rules/cache/TG-CACHE-001-sensitive-response-publicly-cacheable.md +24 -16
- package/skills/torusguard/payload/rules/cache/TG-CACHE-002-missing-user-cache-isolation.md +32 -18
- package/skills/torusguard/payload/rules/cache/TG-CACHE-003-sensitive-data-in-url.md +24 -17
- package/skills/torusguard/payload/rules/csrf/TG-CSRF-001-missing-csrf-protection.md +31 -18
- package/skills/torusguard/payload/rules/csrf/TG-CSRF-002-unsafe-credentialed-cross-origin-request.md +28 -17
- package/skills/torusguard/payload/rules/graphql/TG-GQL-002-resolver-missing-authorization.md +33 -17
- package/skills/torusguard/payload/rules/graphql/TG-GQL-003-unbounded-graphql-pagination-or-batching.md +31 -16
- package/skills/torusguard/payload/rules/graphql/TG-GQL-004-unnecessary-production-introspection.md +25 -16
- package/skills/torusguard/payload/rules/secrets/TG-SEC-005-secret-in-git-history.md +24 -19
- package/skills/torusguard/payload/rules/secrets/TG-SEC-006-secret-in-build-artifact.md +36 -19
- package/skills/torusguard/payload/rules/secrets/TG-SEC-007-secret-in-test-or-example-fixture.md +21 -16
- package/skills/torusguard/payload/rules/ssrf/TG-SSRF-002-missing-internal-network-protection.md +45 -18
- package/skills/torusguard/payload/rules/ssrf/TG-SSRF-003-unsafe-redirect-following.md +20 -16
- package/skills/torusguard/payload/rules/ssrf/TG-SSRF-004-unbounded-outbound-request.md +28 -19
- package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-001-missing-or-ignored-dependency-lockfile.md +25 -18
- package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-003-unsafe-ci-cd-secret-exposure.md +29 -19
- package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-004-unpinned-ci-action.md +23 -18
- package/skills/torusguard/payload/rules/supply-chain/TG-SUPPLY-005-unsafe-dependency-install-script.md +23 -18
- package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-001-missing-webhook-signature-verification.md +36 -16
- package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-002-replayable-webhook.md +26 -17
- package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-003-missing-webhook-idempotency.md +30 -16
- package/skills/torusguard/payload/rules/webhook/TG-WEBHOOK-004-unvalidated-webhook-event-type.md +25 -16
- package/skills/torusguard/payload/rules/websocket/TG-WS-002-missing-channel-level-authorization.md +28 -17
- package/skills/torusguard/payload/rules/websocket/TG-WS-003-unvalidated-websocket-message.md +31 -17
- package/skills/torusguard/payload/rules/websocket/TG-WS-004-unbounded-websocket-connections.md +31 -17
- package/skills/torusguard/payload/rules_catalog.json +520 -0
- package/skills/torusguard/payload/scripts/apply_runner.py +383 -377
- package/skills/torusguard/payload/scripts/audit_runner.py +1306 -653
- package/skills/torusguard/payload/scripts/finding_scorer.py +14 -2
- package/skills/torusguard/payload/scripts/harden_runner.py +628 -455
- package/skills/torusguard/payload/scripts/recheck_runner.py +248 -238
- package/skills/torusguard/payload/scripts/recipes_runner.py +137 -137
- package/skills/torusguard/payload/scripts/report_sync.py +423 -0
- package/skills/torusguard/payload/scripts/sarif_exporter.py +116 -116
- package/skills/torusguard/payload/scripts/term_ui.py +162 -162
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +75 -72
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +84 -80
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +87 -83
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +7 -2
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +7 -2
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +7 -2
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +98 -94
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +68 -64
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +81 -77
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +76 -72
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +62 -58
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +7 -2
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +7 -2
- package/skills/torusguard/payload/workflows/apply.md +11 -4
- package/skills/torusguard/payload/workflows/audit.md +7 -1
- package/skills/torusguard/payload/workflows/authorize.md +2 -1
- package/skills/torusguard/payload/workflows/exploit-check.md +2 -1
- package/skills/torusguard/payload/workflows/harden.md +7 -1
- package/skills/torusguard/payload/workflows/init.md +6 -1
- package/skills/torusguard/payload/workflows/memory.md +2 -1
- package/skills/torusguard/payload/workflows/recheck.md +7 -1
- package/skills/torusguard/payload/workflows/report.md +7 -1
- package/skills/torusguard/payload/workflows/status.md +6 -1
- package/skills/torusguard/payload/workflows/verify.md +6 -1
- package/skills/torusguard/payload/workflows/web-validate.md +2 -1
- package/skills/torusguard-apply/SKILL.md +84 -80
- package/skills/torusguard-audit/SKILL.md +87 -83
- package/skills/torusguard-authorize/SKILL.md +7 -2
- package/skills/torusguard-exploit-check/SKILL.md +7 -2
- package/skills/torusguard-full/SKILL.md +7 -2
- package/skills/torusguard-harden/SKILL.md +98 -94
- package/skills/torusguard-init/SKILL.md +68 -64
- package/skills/torusguard-recheck/SKILL.md +81 -77
- package/skills/torusguard-report/SKILL.md +76 -72
- package/skills/torusguard-status/SKILL.md +62 -58
- package/skills/torusguard-verify/SKILL.md +7 -2
- package/skills/torusguard-web-validate/SKILL.md +7 -2
|
@@ -1,377 +1,383 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
TorusGuard Governed Patch Application & Snapshot Engine (v1.3.
|
|
4
|
-
Interactive Human Gate authorization, pre-apply .bak rollback snapshots,
|
|
5
|
-
atomic patch application, and Golden Fix Recipe distillation into persistent memory.
|
|
6
|
-
Strict 75-column terminal UI formatting.
|
|
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 shutil
|
|
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
|
-
# ─── Formatting Helpers ────────────────────────────────────────────────────────
|
|
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.
|
|
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
|
-
if not runs_dir.is_dir():
|
|
79
|
-
return None
|
|
80
|
-
runs = sorted(runs_dir.glob("run-*-audit"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
81
|
-
return runs[0] if runs else None
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def execute_rollback(target_root: Path, run_id_arg: Optional[str] = None) -> None:
|
|
85
|
-
"""Restore all target files from the pre-apply backup snapshots."""
|
|
86
|
-
snapshots_base = target_root / ".torusguard" / "snapshots"
|
|
87
|
-
if not snapshots_base.is_dir():
|
|
88
|
-
print(f"\n {YELLOW}ℹ No snapshots directory found at: {snapshots_base}{RESET}\n")
|
|
89
|
-
sys.exit(0)
|
|
90
|
-
|
|
91
|
-
if run_id_arg:
|
|
92
|
-
snapshot_dir = snapshots_base / run_id_arg
|
|
93
|
-
else:
|
|
94
|
-
snapshots = sorted([d for d in snapshots_base.iterdir() if d.is_dir()], key=lambda p: p.stat().st_mtime, reverse=True)
|
|
95
|
-
snapshot_dir = snapshots[0] if snapshots else None
|
|
96
|
-
|
|
97
|
-
if not snapshot_dir or not snapshot_dir.is_dir():
|
|
98
|
-
print(f"\n {RED}✖ No rollback snapshot found for run: {run_id_arg or 'latest'}{RESET}\n")
|
|
99
|
-
sys.exit(1)
|
|
100
|
-
|
|
101
|
-
restored_files = []
|
|
102
|
-
for bak_file in snapshot_dir.rglob("*.bak"):
|
|
103
|
-
rel_path = bak_file.relative_to(snapshot_dir)
|
|
104
|
-
orig_rel = str(rel_path)[:-4] # Strip .bak
|
|
105
|
-
dest_file = target_root / orig_rel
|
|
106
|
-
|
|
107
|
-
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
-
shutil.copy2(bak_file, dest_file)
|
|
109
|
-
restored_files.append(orig_rel)
|
|
110
|
-
|
|
111
|
-
print()
|
|
112
|
-
print(box_header("🛡️ TORUSGUARD ROLLBACK RESTORATION", "Restored original files from pre-apply snapshot", "v1.3.
|
|
113
|
-
print()
|
|
114
|
-
|
|
115
|
-
print(border_top("Rollback Snapshot Restored"))
|
|
116
|
-
print(box_line(f"Snapshot Run: {WHITE}{snapshot_dir.name}{RESET}"))
|
|
117
|
-
print(box_line(f"Files Restored: {GREEN}{len(restored_files)} file(s) cleanly reverted{RESET}"))
|
|
118
|
-
for rf in restored_files:
|
|
119
|
-
print(box_line(f" ✔ {rf}"))
|
|
120
|
-
print(border_bottom())
|
|
121
|
-
print()
|
|
122
|
-
sys.exit(0)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_approve: bool = False) -> Dict[str, Any]:
|
|
126
|
-
runs_dir = target_root / ".torusguard" / "runs"
|
|
127
|
-
if run_id_arg:
|
|
128
|
-
run_folder = runs_dir / run_id_arg
|
|
129
|
-
else:
|
|
130
|
-
run_folder = find_latest_audit_run(runs_dir)
|
|
131
|
-
|
|
132
|
-
if not run_folder or not run_folder.is_dir():
|
|
133
|
-
print(f"\n {RED}✖ No audit runs found.{RESET}")
|
|
134
|
-
print(f" Run {CYAN}npx torusguard audit{RESET} followed by {CYAN}npx torusguard harden{RESET} first.\n")
|
|
135
|
-
sys.exit(1)
|
|
136
|
-
|
|
137
|
-
bundles_dir = run_folder / "bundles"
|
|
138
|
-
bundle_meta_files = list(bundles_dir.glob("*/metadata.json")) if bundles_dir.is_dir() else []
|
|
139
|
-
if not bundle_meta_files:
|
|
140
|
-
print()
|
|
141
|
-
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.3.
|
|
142
|
-
print()
|
|
143
|
-
print(f" {YELLOW}ℹ Zero candidate remediation bundles found in run: {run_folder.name}{RESET}")
|
|
144
|
-
print(f" {GRAY}The findings in this run require manual review or AI-assisted remediation.{RESET}\n")
|
|
145
|
-
print(border_top("Recommended Next Steps"))
|
|
146
|
-
print(box_line("1. In AI Chat: Run /torusguard-harden to synthesize custom fixes"))
|
|
147
|
-
print(box_line(f"2. Inspect findings: .torusguard/runs/{run_folder.name}/findings.md"))
|
|
148
|
-
print(box_line("3. View visual posture report: npx torusguard report --html"))
|
|
149
|
-
print(border_bottom())
|
|
150
|
-
print()
|
|
151
|
-
sys.exit(0)
|
|
152
|
-
|
|
153
|
-
bundles = []
|
|
154
|
-
for mf in bundle_meta_files:
|
|
155
|
-
try:
|
|
156
|
-
bundles.append(json.loads(mf.read_text(encoding="utf-8")))
|
|
157
|
-
except Exception:
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
# Snapshot directory
|
|
161
|
-
snapshot_dir = target_root / ".torusguard" / "snapshots" / run_folder.name
|
|
162
|
-
snapshot_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
-
|
|
164
|
-
# Header Card
|
|
165
|
-
print()
|
|
166
|
-
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.3.
|
|
167
|
-
print()
|
|
168
|
-
|
|
169
|
-
print(border_top("Candidate Bundles Ready for Application"))
|
|
170
|
-
print(box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
|
|
171
|
-
print(box_line(f"Total Bundles: {GREEN}{len(bundles)} candidate surgical patches formulated{RESET}"))
|
|
172
|
-
print(box_line(f"Rollback Safety:Automatic pre-apply snapshot to .torusguard/snapshots/"))
|
|
173
|
-
print(border_bottom())
|
|
174
|
-
print()
|
|
175
|
-
|
|
176
|
-
applied_count = 0
|
|
177
|
-
skipped_count = 0
|
|
178
|
-
all_approved = auto_approve
|
|
179
|
-
diff_summary_lines = [f"# TorusGuard Applied Patch Summary: {run_folder.name}\n"]
|
|
180
|
-
applied_bundles = []
|
|
181
|
-
|
|
182
|
-
# Import memory_engine for Golden Recipe distillation
|
|
183
|
-
try:
|
|
184
|
-
import memory_engine
|
|
185
|
-
except ImportError:
|
|
186
|
-
memory_engine = None
|
|
187
|
-
|
|
188
|
-
for idx, b in enumerate(bundles, 1):
|
|
189
|
-
rule_id = b.get("rule_id", "TG-SEC")
|
|
190
|
-
rel_path = b.get("target_file", "")
|
|
191
|
-
line_no = b.get("line_number", 1)
|
|
192
|
-
diff = b.get("proposed_diff", "")
|
|
193
|
-
desc = b.get("what_should_change", "")
|
|
194
|
-
patched_content = b.get("patched_content")
|
|
195
|
-
|
|
196
|
-
print(f" {YELLOW}─── [{idx}/{len(bundles)}] Candidate Patch: [{rule_id}] on {rel_path}:{line_no} ───{RESET}")
|
|
197
|
-
print(f" Strategy: {WHITE}{desc}{RESET}")
|
|
198
|
-
print(f" Ponytail: {GREEN}+{b.get('additions', 0)} / -{b.get('deletions', 0)}{RESET}\n")
|
|
199
|
-
|
|
200
|
-
# Display syntax-highlighted diff
|
|
201
|
-
for dline in diff.splitlines()[:12]:
|
|
202
|
-
if dline.startswith("+") and not dline.startswith("+++"):
|
|
203
|
-
print(f" {GREEN}{dline}{RESET}")
|
|
204
|
-
elif dline.startswith("-") and not dline.startswith("---"):
|
|
205
|
-
print(f" {RED}{dline}{RESET}")
|
|
206
|
-
elif dline.startswith("@"):
|
|
207
|
-
print(f" {CYAN}{dline}{RESET}")
|
|
208
|
-
else:
|
|
209
|
-
print(f" {DIM}{dline}{RESET}")
|
|
210
|
-
print()
|
|
211
|
-
|
|
212
|
-
if not all_approved:
|
|
213
|
-
try:
|
|
214
|
-
choice = input(f" {BOLD}Apply this patch?{RESET} ([y]es / [n]o / [a]ll / [q]uit): ").strip().lower()
|
|
215
|
-
except (EOFError, KeyboardInterrupt):
|
|
216
|
-
print(f"\n {YELLOW}Aborted by user.{RESET}\n")
|
|
217
|
-
break
|
|
218
|
-
|
|
219
|
-
if choice in ("q", "quit"):
|
|
220
|
-
print(f"\n {YELLOW}Exited review.{RESET}\n")
|
|
221
|
-
break
|
|
222
|
-
elif choice in ("a", "all"):
|
|
223
|
-
all_approved = True
|
|
224
|
-
elif choice not in ("y", "yes", ""):
|
|
225
|
-
print(f" {GRAY}Skipped.{RESET}\n")
|
|
226
|
-
skipped_count += 1
|
|
227
|
-
continue
|
|
228
|
-
|
|
229
|
-
# Execute Application
|
|
230
|
-
target_file = target_root / rel_path
|
|
231
|
-
if not target_file.is_file():
|
|
232
|
-
print(f" {RED}✖ Target file not found: {target_file}{RESET}\n")
|
|
233
|
-
continue
|
|
234
|
-
|
|
235
|
-
# 1. Pre-apply Snapshot (.bak) if not already created
|
|
236
|
-
bak_dest = snapshot_dir / (rel_path + ".bak")
|
|
237
|
-
if not bak_dest.is_file():
|
|
238
|
-
bak_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
-
shutil.copy2(target_file, bak_dest)
|
|
240
|
-
|
|
241
|
-
# 2. Write Patched Content (Sequential Line-Aware or Full Patch)
|
|
242
|
-
target_line = b.get("target_line")
|
|
243
|
-
replacement_line = b.get("replacement_line")
|
|
244
|
-
curr_text = target_file.read_text(encoding="utf-8", errors="replace")
|
|
245
|
-
curr_lines = curr_text.splitlines()
|
|
246
|
-
|
|
247
|
-
if target_line and replacement_line is not None:
|
|
248
|
-
applied_line = False
|
|
249
|
-
# Try expected line index
|
|
250
|
-
if 0 <= line_no - 1 < len(curr_lines) and curr_lines[line_no - 1] == target_line:
|
|
251
|
-
curr_lines[line_no - 1] = replacement_line
|
|
252
|
-
applied_line = True
|
|
253
|
-
else:
|
|
254
|
-
# Search across whole file for the target line
|
|
255
|
-
for i, cl in enumerate(curr_lines):
|
|
256
|
-
if cl == target_line:
|
|
257
|
-
curr_lines[i] = replacement_line
|
|
258
|
-
applied_line = True
|
|
259
|
-
break
|
|
260
|
-
if applied_line:
|
|
261
|
-
target_file.write_text("\n".join(curr_lines) + "\n", encoding="utf-8")
|
|
262
|
-
elif patched_content:
|
|
263
|
-
target_file.write_text(patched_content, encoding="utf-8")
|
|
264
|
-
elif patched_content:
|
|
265
|
-
target_file.write_text(patched_content, encoding="utf-8")
|
|
266
|
-
else:
|
|
267
|
-
continue
|
|
268
|
-
|
|
269
|
-
# 3. Distill into Golden Fix Recipe in Memory Engine
|
|
270
|
-
if memory_engine:
|
|
271
|
-
try:
|
|
272
|
-
fn = getattr(memory_engine, "record_golden_recipe", getattr(memory_engine, "store_golden_recipe", None))
|
|
273
|
-
if fn:
|
|
274
|
-
before_lines = target_file.read_text(encoding="utf-8").splitlines()
|
|
275
|
-
before_snip = before_lines[max(0, line_no - 1)] if before_lines else ""
|
|
276
|
-
fn(
|
|
277
|
-
rule_id=rule_id,
|
|
278
|
-
before_snippet=before_snip,
|
|
279
|
-
after_snippet=desc,
|
|
280
|
-
diff_snippet=diff,
|
|
281
|
-
file_type=target_file.suffix,
|
|
282
|
-
description=desc,
|
|
283
|
-
root_dir=target_root
|
|
284
|
-
)
|
|
285
|
-
# Record event
|
|
286
|
-
record_ev = getattr(memory_engine, "record_event", None)
|
|
287
|
-
if record_ev:
|
|
288
|
-
record_ev("fix_applied", {
|
|
289
|
-
"rule_id": rule_id,
|
|
290
|
-
"file_path": rel_path,
|
|
291
|
-
"line_number": line_no,
|
|
292
|
-
"bundle_id": b.get("bundle_id")
|
|
293
|
-
}, root_dir=target_root)
|
|
294
|
-
except Exception:
|
|
295
|
-
pass
|
|
296
|
-
|
|
297
|
-
applied_count += 1
|
|
298
|
-
applied_bundles.append(b)
|
|
299
|
-
diff_summary_lines.append(f"## Applied: `[{rule_id}]` on `{rel_path}:{line_no}`")
|
|
300
|
-
diff_summary_lines.append("```diff")
|
|
301
|
-
diff_summary_lines.append(diff.strip())
|
|
302
|
-
diff_summary_lines.append("```\n")
|
|
303
|
-
try:
|
|
304
|
-
rel_snapshot = bak_dest.relative_to(target_root)
|
|
305
|
-
except Exception:
|
|
306
|
-
rel_snapshot = bak_dest.name
|
|
307
|
-
print(f" {GREEN}✔ Applied successfully.{RESET} (Snapshot saved: {rel_snapshot})\n")
|
|
308
|
-
|
|
309
|
-
# Write apply artifacts in run folder
|
|
310
|
-
(run_folder / "diff_summary.md").write_text("\n".join(diff_summary_lines), encoding="utf-8")
|
|
311
|
-
apply_plan_lines = [
|
|
312
|
-
f"# TorusGuard Governed Application Plan: {run_folder.name}",
|
|
313
|
-
f"- **Total Candidate Patches:** {len(bundles)}",
|
|
314
|
-
f"- **Successfully Applied:** {applied_count}",
|
|
315
|
-
f"- **Skipped by Human Gate:** {skipped_count}",
|
|
316
|
-
f"- **Execution Timestamp:** {get_ist_now().strftime('%Y-%m-%d %H:%M:%S IST')}",
|
|
317
|
-
f"- **Rollback Directory:** `.torusguard/snapshots/{run_folder.name}`"
|
|
318
|
-
]
|
|
319
|
-
(run_folder / "apply_plan.md").write_text("\n".join(apply_plan_lines), encoding="utf-8")
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
print(
|
|
339
|
-
print()
|
|
340
|
-
|
|
341
|
-
print(
|
|
342
|
-
print(box_line(f"
|
|
343
|
-
print(
|
|
344
|
-
print(
|
|
345
|
-
|
|
346
|
-
print()
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TorusGuard Governed Patch Application & Snapshot Engine (v1.3.5)
|
|
4
|
+
Interactive Human Gate authorization, pre-apply .bak rollback snapshots,
|
|
5
|
+
atomic patch application, and Golden Fix Recipe distillation into persistent memory.
|
|
6
|
+
Strict 75-column terminal UI formatting.
|
|
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 shutil
|
|
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
|
+
# ─── Formatting Helpers ────────────────────────────────────────────────────────
|
|
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
|
+
if not runs_dir.is_dir():
|
|
79
|
+
return None
|
|
80
|
+
runs = sorted(runs_dir.glob("run-*-audit"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
81
|
+
return runs[0] if runs else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def execute_rollback(target_root: Path, run_id_arg: Optional[str] = None) -> None:
|
|
85
|
+
"""Restore all target files from the pre-apply backup snapshots."""
|
|
86
|
+
snapshots_base = target_root / ".torusguard" / "snapshots"
|
|
87
|
+
if not snapshots_base.is_dir():
|
|
88
|
+
print(f"\n {YELLOW}ℹ No snapshots directory found at: {snapshots_base}{RESET}\n")
|
|
89
|
+
sys.exit(0)
|
|
90
|
+
|
|
91
|
+
if run_id_arg:
|
|
92
|
+
snapshot_dir = snapshots_base / run_id_arg
|
|
93
|
+
else:
|
|
94
|
+
snapshots = sorted([d for d in snapshots_base.iterdir() if d.is_dir()], key=lambda p: p.stat().st_mtime, reverse=True)
|
|
95
|
+
snapshot_dir = snapshots[0] if snapshots else None
|
|
96
|
+
|
|
97
|
+
if not snapshot_dir or not snapshot_dir.is_dir():
|
|
98
|
+
print(f"\n {RED}✖ No rollback snapshot found for run: {run_id_arg or 'latest'}{RESET}\n")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
restored_files = []
|
|
102
|
+
for bak_file in snapshot_dir.rglob("*.bak"):
|
|
103
|
+
rel_path = bak_file.relative_to(snapshot_dir)
|
|
104
|
+
orig_rel = str(rel_path)[:-4] # Strip .bak
|
|
105
|
+
dest_file = target_root / orig_rel
|
|
106
|
+
|
|
107
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
shutil.copy2(bak_file, dest_file)
|
|
109
|
+
restored_files.append(orig_rel)
|
|
110
|
+
|
|
111
|
+
print()
|
|
112
|
+
print(box_header("🛡️ TORUSGUARD ROLLBACK RESTORATION", "Restored original files from pre-apply snapshot", "v1.3.5"))
|
|
113
|
+
print()
|
|
114
|
+
|
|
115
|
+
print(border_top("Rollback Snapshot Restored"))
|
|
116
|
+
print(box_line(f"Snapshot Run: {WHITE}{snapshot_dir.name}{RESET}"))
|
|
117
|
+
print(box_line(f"Files Restored: {GREEN}{len(restored_files)} file(s) cleanly reverted{RESET}"))
|
|
118
|
+
for rf in restored_files:
|
|
119
|
+
print(box_line(f" ✔ {rf}"))
|
|
120
|
+
print(border_bottom())
|
|
121
|
+
print()
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def execute_apply(target_root: Path, run_id_arg: Optional[str] = None, auto_approve: bool = False) -> Dict[str, Any]:
|
|
126
|
+
runs_dir = target_root / ".torusguard" / "runs"
|
|
127
|
+
if run_id_arg:
|
|
128
|
+
run_folder = runs_dir / run_id_arg
|
|
129
|
+
else:
|
|
130
|
+
run_folder = find_latest_audit_run(runs_dir)
|
|
131
|
+
|
|
132
|
+
if not run_folder or not run_folder.is_dir():
|
|
133
|
+
print(f"\n {RED}✖ No audit runs found.{RESET}")
|
|
134
|
+
print(f" Run {CYAN}npx torusguard audit{RESET} followed by {CYAN}npx torusguard harden{RESET} first.\n")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
bundles_dir = run_folder / "bundles"
|
|
138
|
+
bundle_meta_files = list(bundles_dir.glob("*/metadata.json")) if bundles_dir.is_dir() else []
|
|
139
|
+
if not bundle_meta_files:
|
|
140
|
+
print()
|
|
141
|
+
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.3.5"))
|
|
142
|
+
print()
|
|
143
|
+
print(f" {YELLOW}ℹ Zero candidate remediation bundles found in run: {run_folder.name}{RESET}")
|
|
144
|
+
print(f" {GRAY}The findings in this run require manual review or AI-assisted remediation.{RESET}\n")
|
|
145
|
+
print(border_top("Recommended Next Steps"))
|
|
146
|
+
print(box_line("1. In AI Chat: Run /torusguard-harden to synthesize custom fixes"))
|
|
147
|
+
print(box_line(f"2. Inspect findings: .torusguard/runs/{run_folder.name}/findings.md"))
|
|
148
|
+
print(box_line("3. View visual posture report: npx torusguard report --html"))
|
|
149
|
+
print(border_bottom())
|
|
150
|
+
print()
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
bundles = []
|
|
154
|
+
for mf in bundle_meta_files:
|
|
155
|
+
try:
|
|
156
|
+
bundles.append(json.loads(mf.read_text(encoding="utf-8")))
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Snapshot directory
|
|
161
|
+
snapshot_dir = target_root / ".torusguard" / "snapshots" / run_folder.name
|
|
162
|
+
snapshot_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
# Header Card
|
|
165
|
+
print()
|
|
166
|
+
print(box_header("🛡️ TORUSGUARD GOVERNED PATCH APPLIER", "Human-Gate Authorization, Snapshots & Golden Recipe Distillation", "v1.3.5"))
|
|
167
|
+
print()
|
|
168
|
+
|
|
169
|
+
print(border_top("Candidate Bundles Ready for Application"))
|
|
170
|
+
print(box_line(f"Active Run: {WHITE}{run_folder.name}{RESET}"))
|
|
171
|
+
print(box_line(f"Total Bundles: {GREEN}{len(bundles)} candidate surgical patches formulated{RESET}"))
|
|
172
|
+
print(box_line(f"Rollback Safety:Automatic pre-apply snapshot to .torusguard/snapshots/"))
|
|
173
|
+
print(border_bottom())
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
applied_count = 0
|
|
177
|
+
skipped_count = 0
|
|
178
|
+
all_approved = auto_approve
|
|
179
|
+
diff_summary_lines = [f"# TorusGuard Applied Patch Summary: {run_folder.name}\n"]
|
|
180
|
+
applied_bundles = []
|
|
181
|
+
|
|
182
|
+
# Import memory_engine for Golden Recipe distillation
|
|
183
|
+
try:
|
|
184
|
+
import memory_engine
|
|
185
|
+
except ImportError:
|
|
186
|
+
memory_engine = None
|
|
187
|
+
|
|
188
|
+
for idx, b in enumerate(bundles, 1):
|
|
189
|
+
rule_id = b.get("rule_id", "TG-SEC")
|
|
190
|
+
rel_path = b.get("target_file", "")
|
|
191
|
+
line_no = b.get("line_number", 1)
|
|
192
|
+
diff = b.get("proposed_diff", "")
|
|
193
|
+
desc = b.get("what_should_change", "")
|
|
194
|
+
patched_content = b.get("patched_content")
|
|
195
|
+
|
|
196
|
+
print(f" {YELLOW}─── [{idx}/{len(bundles)}] Candidate Patch: [{rule_id}] on {rel_path}:{line_no} ───{RESET}")
|
|
197
|
+
print(f" Strategy: {WHITE}{desc}{RESET}")
|
|
198
|
+
print(f" Ponytail: {GREEN}+{b.get('additions', 0)} / -{b.get('deletions', 0)}{RESET}\n")
|
|
199
|
+
|
|
200
|
+
# Display syntax-highlighted diff
|
|
201
|
+
for dline in diff.splitlines()[:12]:
|
|
202
|
+
if dline.startswith("+") and not dline.startswith("+++"):
|
|
203
|
+
print(f" {GREEN}{dline}{RESET}")
|
|
204
|
+
elif dline.startswith("-") and not dline.startswith("---"):
|
|
205
|
+
print(f" {RED}{dline}{RESET}")
|
|
206
|
+
elif dline.startswith("@"):
|
|
207
|
+
print(f" {CYAN}{dline}{RESET}")
|
|
208
|
+
else:
|
|
209
|
+
print(f" {DIM}{dline}{RESET}")
|
|
210
|
+
print()
|
|
211
|
+
|
|
212
|
+
if not all_approved:
|
|
213
|
+
try:
|
|
214
|
+
choice = input(f" {BOLD}Apply this patch?{RESET} ([y]es / [n]o / [a]ll / [q]uit): ").strip().lower()
|
|
215
|
+
except (EOFError, KeyboardInterrupt):
|
|
216
|
+
print(f"\n {YELLOW}Aborted by user.{RESET}\n")
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
if choice in ("q", "quit"):
|
|
220
|
+
print(f"\n {YELLOW}Exited review.{RESET}\n")
|
|
221
|
+
break
|
|
222
|
+
elif choice in ("a", "all"):
|
|
223
|
+
all_approved = True
|
|
224
|
+
elif choice not in ("y", "yes", ""):
|
|
225
|
+
print(f" {GRAY}Skipped.{RESET}\n")
|
|
226
|
+
skipped_count += 1
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Execute Application
|
|
230
|
+
target_file = target_root / rel_path
|
|
231
|
+
if not target_file.is_file():
|
|
232
|
+
print(f" {RED}✖ Target file not found: {target_file}{RESET}\n")
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# 1. Pre-apply Snapshot (.bak) if not already created
|
|
236
|
+
bak_dest = snapshot_dir / (rel_path + ".bak")
|
|
237
|
+
if not bak_dest.is_file():
|
|
238
|
+
bak_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
shutil.copy2(target_file, bak_dest)
|
|
240
|
+
|
|
241
|
+
# 2. Write Patched Content (Sequential Line-Aware or Full Patch)
|
|
242
|
+
target_line = b.get("target_line")
|
|
243
|
+
replacement_line = b.get("replacement_line")
|
|
244
|
+
curr_text = target_file.read_text(encoding="utf-8", errors="replace")
|
|
245
|
+
curr_lines = curr_text.splitlines()
|
|
246
|
+
|
|
247
|
+
if target_line and replacement_line is not None:
|
|
248
|
+
applied_line = False
|
|
249
|
+
# Try expected line index
|
|
250
|
+
if 0 <= line_no - 1 < len(curr_lines) and curr_lines[line_no - 1] == target_line:
|
|
251
|
+
curr_lines[line_no - 1] = replacement_line
|
|
252
|
+
applied_line = True
|
|
253
|
+
else:
|
|
254
|
+
# Search across whole file for the target line
|
|
255
|
+
for i, cl in enumerate(curr_lines):
|
|
256
|
+
if cl == target_line:
|
|
257
|
+
curr_lines[i] = replacement_line
|
|
258
|
+
applied_line = True
|
|
259
|
+
break
|
|
260
|
+
if applied_line:
|
|
261
|
+
target_file.write_text("\n".join(curr_lines) + "\n", encoding="utf-8")
|
|
262
|
+
elif patched_content:
|
|
263
|
+
target_file.write_text(patched_content, encoding="utf-8")
|
|
264
|
+
elif patched_content:
|
|
265
|
+
target_file.write_text(patched_content, encoding="utf-8")
|
|
266
|
+
else:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# 3. Distill into Golden Fix Recipe in Memory Engine
|
|
270
|
+
if memory_engine:
|
|
271
|
+
try:
|
|
272
|
+
fn = getattr(memory_engine, "record_golden_recipe", getattr(memory_engine, "store_golden_recipe", None))
|
|
273
|
+
if fn:
|
|
274
|
+
before_lines = target_file.read_text(encoding="utf-8").splitlines()
|
|
275
|
+
before_snip = before_lines[max(0, line_no - 1)] if before_lines else ""
|
|
276
|
+
fn(
|
|
277
|
+
rule_id=rule_id,
|
|
278
|
+
before_snippet=before_snip,
|
|
279
|
+
after_snippet=desc,
|
|
280
|
+
diff_snippet=diff,
|
|
281
|
+
file_type=target_file.suffix,
|
|
282
|
+
description=desc,
|
|
283
|
+
root_dir=target_root
|
|
284
|
+
)
|
|
285
|
+
# Record event
|
|
286
|
+
record_ev = getattr(memory_engine, "record_event", None)
|
|
287
|
+
if record_ev:
|
|
288
|
+
record_ev("fix_applied", {
|
|
289
|
+
"rule_id": rule_id,
|
|
290
|
+
"file_path": rel_path,
|
|
291
|
+
"line_number": line_no,
|
|
292
|
+
"bundle_id": b.get("bundle_id")
|
|
293
|
+
}, root_dir=target_root)
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
applied_count += 1
|
|
298
|
+
applied_bundles.append(b)
|
|
299
|
+
diff_summary_lines.append(f"## Applied: `[{rule_id}]` on `{rel_path}:{line_no}`")
|
|
300
|
+
diff_summary_lines.append("```diff")
|
|
301
|
+
diff_summary_lines.append(diff.strip())
|
|
302
|
+
diff_summary_lines.append("```\n")
|
|
303
|
+
try:
|
|
304
|
+
rel_snapshot = bak_dest.relative_to(target_root)
|
|
305
|
+
except Exception:
|
|
306
|
+
rel_snapshot = bak_dest.name
|
|
307
|
+
print(f" {GREEN}✔ Applied successfully.{RESET} (Snapshot saved: {rel_snapshot})\n")
|
|
308
|
+
|
|
309
|
+
# Write apply artifacts in run folder
|
|
310
|
+
(run_folder / "diff_summary.md").write_text("\n".join(diff_summary_lines), encoding="utf-8")
|
|
311
|
+
apply_plan_lines = [
|
|
312
|
+
f"# TorusGuard Governed Application Plan: {run_folder.name}",
|
|
313
|
+
f"- **Total Candidate Patches:** {len(bundles)}",
|
|
314
|
+
f"- **Successfully Applied:** {applied_count}",
|
|
315
|
+
f"- **Skipped by Human Gate:** {skipped_count}",
|
|
316
|
+
f"- **Execution Timestamp:** {get_ist_now().strftime('%Y-%m-%d %H:%M:%S IST')}",
|
|
317
|
+
f"- **Rollback Directory:** `.torusguard/snapshots/{run_folder.name}`"
|
|
318
|
+
]
|
|
319
|
+
(run_folder / "apply_plan.md").write_text("\n".join(apply_plan_lines), encoding="utf-8")
|
|
320
|
+
try:
|
|
321
|
+
import report_sync
|
|
322
|
+
report_sync.record_applied_patches(target_root, applied_bundles, snapshot_dir, run_folder.name)
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
# Update manifest
|
|
327
|
+
manifest_file = run_folder / "manifest.json"
|
|
328
|
+
if manifest_file.is_file():
|
|
329
|
+
try:
|
|
330
|
+
mdata = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
331
|
+
mdata["status"] = "patches_applied"
|
|
332
|
+
mdata["remediated"] = applied_count
|
|
333
|
+
manifest_file.write_text(json.dumps(mdata, indent=2), encoding="utf-8")
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
# Summary Box
|
|
338
|
+
print(border_top("Application Summary"))
|
|
339
|
+
print(box_line(f"Applied Patches:{GREEN}{applied_count} applied cleanly{RESET}"))
|
|
340
|
+
print(box_line(f"Skipped / Pass: {GRAY}{skipped_count} skipped{RESET}"))
|
|
341
|
+
print(box_line(f"Golden Recipes: {GREEN}{applied_count} distilled into persistent memory{RESET}"))
|
|
342
|
+
print(box_line(f"Snapshot Safety:.torusguard/snapshots/{run_folder.name}"))
|
|
343
|
+
print(border_bottom())
|
|
344
|
+
print()
|
|
345
|
+
|
|
346
|
+
print(border_top("Next Governed Action", border_color=GREEN, double=True))
|
|
347
|
+
print(box_line(f"Living Report: {CYAN}security_report.md (applied state recorded){RESET}", border="║", border_color=GREEN))
|
|
348
|
+
print(box_line(f"Differential Recheck: {BOLD}{WHITE}npx torusguard recheck{RESET} (Verify fix closure)", border="║", border_color=GREEN))
|
|
349
|
+
print(box_line(f"View Golden Recipes: {CYAN}npx torusguard recipes{RESET} (Inspect memory)", border="║", border_color=GREEN))
|
|
350
|
+
print(box_line(f"Emergency Rollback: {YELLOW}npx torusguard rollback{RESET} (Instant revert)", border="║", border_color=GREEN))
|
|
351
|
+
print(border_bottom(border_color=GREEN, double=True))
|
|
352
|
+
print()
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"applied_count": applied_count,
|
|
356
|
+
"skipped_count": skipped_count,
|
|
357
|
+
"applied_bundles": applied_bundles,
|
|
358
|
+
"snapshot_dir": str(snapshot_dir)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main():
|
|
363
|
+
parser = argparse.ArgumentParser(description="TorusGuard Governed Patch Application Engine")
|
|
364
|
+
parser.add_argument("path", nargs="?", default=".", help="Target project root directory")
|
|
365
|
+
parser.add_argument("--run", "-r", help="Explicit run ID to apply")
|
|
366
|
+
parser.add_argument("--yes", "-y", action="store_true", help="Auto-approve all patches (non-interactive)")
|
|
367
|
+
parser.add_argument("--rollback", action="store_true", help="Rollback all applied patches using snapshots")
|
|
368
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
369
|
+
args = parser.parse_args()
|
|
370
|
+
|
|
371
|
+
target = Path(args.path).resolve()
|
|
372
|
+
|
|
373
|
+
if args.rollback:
|
|
374
|
+
execute_rollback(target, run_id_arg=args.run)
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
result = execute_apply(target, run_id_arg=args.run, auto_approve=args.yes)
|
|
378
|
+
if args.json:
|
|
379
|
+
print(json.dumps(result, indent=2))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
main()
|