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