torusguard 1.3.3 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/metadata.json +15 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/minimal_patch_plan.md +21 -0
- package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/patch.diff +9 -0
- package/.torusguard/runs/run-20260910-120238-audit/findings.json +1520 -0
- package/.torusguard/runs/run-20260910-120238-audit/findings.md +560 -0
- package/.torusguard/runs/run-20260910-120238-audit/manifest.json +13 -0
- package/.torusguard/runs/run-20260910-120238-audit/recheck.md +86 -0
- package/.torusguard/runs/run-20260910-120238-audit/remediation.md +262 -0
- package/.torusguard/runs/run-20260910-120238-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260910-121238-audit/findings.json +794 -0
- package/.torusguard/runs/run-20260910-121238-audit/findings.md +344 -0
- package/.torusguard/runs/run-20260910-121238-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260910-121238-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260910-130454-audit/findings.json +893 -0
- package/.torusguard/runs/run-20260910-130454-audit/findings.md +386 -0
- package/.torusguard/runs/run-20260910-130454-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260910-130454-audit/summary.md +5 -0
- package/.torusguard/runs/run-20260910-130519-audit/findings.json +563 -0
- package/.torusguard/runs/run-20260910-130519-audit/findings.md +246 -0
- package/.torusguard/runs/run-20260910-130519-audit/manifest.json +12 -0
- package/.torusguard/runs/run-20260910-130519-audit/summary.md +5 -0
- 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__/diff_guard.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/report_sync.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
- package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
- package/.torusguard/scripts/apply_runner.py +383 -339
- package/.torusguard/scripts/audit_runner.py +1306 -550
- package/.torusguard/scripts/finding_scorer.py +14 -2
- package/.torusguard/scripts/harden_runner.py +628 -375
- package/.torusguard/scripts/recheck_runner.py +248 -221
- package/.torusguard/scripts/recipes_runner.py +137 -130
- package/.torusguard/scripts/report_sync.py +423 -0
- package/.torusguard/scripts/sarif_exporter.py +116 -116
- package/.torusguard/scripts/term_ui.py +162 -0
- package/.torusguard/skills/torusguard/SKILL.md +61 -31
- package/.torusguard/skills/torusguard-apply/SKILL.md +80 -46
- package/.torusguard/skills/torusguard-audit/SKILL.md +83 -70
- package/.torusguard/skills/torusguard-authorize/SKILL.md +5 -4
- package/.torusguard/skills/torusguard-exploit-check/SKILL.md +6 -5
- package/.torusguard/skills/torusguard-full/SKILL.md +1 -1
- package/.torusguard/skills/torusguard-harden/SKILL.md +94 -63
- package/.torusguard/skills/torusguard-init/SKILL.md +64 -52
- package/.torusguard/skills/torusguard-recheck/SKILL.md +77 -53
- package/.torusguard/skills/torusguard-report/SKILL.md +72 -54
- package/.torusguard/skills/torusguard-status/SKILL.md +58 -42
- package/.torusguard/skills/torusguard-verify/SKILL.md +8 -7
- package/.torusguard/skills/torusguard-web-validate/SKILL.md +8 -7
- 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 -344
- package/bin/torusguard.js +209 -97
- package/package.json +1 -1
- package/skills/torusguard/SKILL.md +73 -42
- 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 -339
- package/skills/torusguard/payload/scripts/audit_runner.py +1306 -550
- package/skills/torusguard/payload/scripts/finding_scorer.py +14 -2
- package/skills/torusguard/payload/scripts/harden_runner.py +628 -375
- package/skills/torusguard/payload/scripts/recheck_runner.py +248 -221
- package/skills/torusguard/payload/scripts/recipes_runner.py +137 -130
- 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 -0
- package/skills/torusguard/payload/skills/torusguard/SKILL.md +75 -42
- package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +63 -25
- package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +62 -45
- package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +11 -5
- package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +12 -6
- package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +7 -2
- package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +71 -36
- package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +45 -29
- package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +59 -31
- package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +52 -30
- package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +40 -20
- package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +14 -8
- package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +14 -8
- 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 +63 -27
- package/skills/torusguard-audit/SKILL.md +62 -50
- 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 +71 -40
- package/skills/torusguard-init/SKILL.md +45 -30
- package/skills/torusguard-recheck/SKILL.md +59 -32
- package/skills/torusguard-report/SKILL.md +52 -31
- package/skills/torusguard-status/SKILL.md +40 -21
- package/skills/torusguard-verify/SKILL.md +7 -2
- package/skills/torusguard-web-validate/SKILL.md +7 -2
|
@@ -1,550 +1,1306 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
TorusGuard Autonomous Static Security Audit Engine (v1.3.
|
|
4
|
-
Multi-language static pattern & AST security scanner.
|
|
5
|
-
Evaluates source trees against canonical TorusGuard rule families,
|
|
6
|
-
augments confidence via the persistent security memory subsystem,
|
|
7
|
-
groups findings into root-cause clusters, and emits isolated run folders.
|
|
8
|
-
|
|
9
|
-
Pure Python 3.10+ standard library (zero external dependencies).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import sys
|
|
13
|
-
import os
|
|
14
|
-
import re
|
|
15
|
-
import json
|
|
16
|
-
import hashlib
|
|
17
|
-
import argparse
|
|
18
|
-
import datetime
|
|
19
|
-
import unicodedata
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from typing import Dict, List, Any, Optional, Tuple, TypedDict, Pattern
|
|
22
|
-
|
|
23
|
-
# Windows console UTF-8 support
|
|
24
|
-
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
25
|
-
try:
|
|
26
|
-
getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
|
|
27
|
-
except Exception:
|
|
28
|
-
pass
|
|
29
|
-
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
30
|
-
try:
|
|
31
|
-
getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
|
|
32
|
-
except Exception:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
# ─── Timezone Configuration: Indian Standard Time (IST, UTC+05:30) ───────────
|
|
36
|
-
IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
|
|
37
|
-
|
|
38
|
-
def get_ist_now() -> datetime.datetime:
|
|
39
|
-
"""Return timezone-aware datetime strictly in Indian Standard Time (IST)."""
|
|
40
|
-
return datetime.datetime.now(IST)
|
|
41
|
-
|
|
42
|
-
# ───
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
(re.compile(r'\
|
|
156
|
-
(re.compile(r'\
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
(re.compile(r'
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
"
|
|
521
|
-
"
|
|
522
|
-
"
|
|
523
|
-
"
|
|
524
|
-
"
|
|
525
|
-
"
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TorusGuard Autonomous Static Security Audit Engine (v1.3.5)
|
|
4
|
+
Multi-language static pattern & AST security scanner.
|
|
5
|
+
Evaluates source trees against canonical TorusGuard rule families,
|
|
6
|
+
augments confidence via the persistent security memory subsystem,
|
|
7
|
+
groups findings into root-cause clusters, and emits isolated run folders.
|
|
8
|
+
|
|
9
|
+
Pure Python 3.10+ standard library (zero external dependencies).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import json
|
|
16
|
+
import hashlib
|
|
17
|
+
import argparse
|
|
18
|
+
import datetime
|
|
19
|
+
import unicodedata
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Dict, List, Any, Optional, Tuple, TypedDict, Pattern
|
|
22
|
+
|
|
23
|
+
# Windows console UTF-8 support
|
|
24
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
25
|
+
try:
|
|
26
|
+
getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
30
|
+
try:
|
|
31
|
+
getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
# ─── Timezone Configuration: Indian Standard Time (IST, UTC+05:30) ───────────
|
|
36
|
+
IST = datetime.timezone(datetime.timedelta(hours=5, minutes=30), name="IST")
|
|
37
|
+
|
|
38
|
+
def get_ist_now() -> datetime.datetime:
|
|
39
|
+
"""Return timezone-aware datetime strictly in Indian Standard Time (IST)."""
|
|
40
|
+
return datetime.datetime.now(IST)
|
|
41
|
+
|
|
42
|
+
# ─── UI Formatter Bridge ──────────────────────────────────────────────────────
|
|
43
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
44
|
+
if str(scripts_dir) not in sys.path:
|
|
45
|
+
sys.path.insert(0, str(scripts_dir))
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import term_ui as tui
|
|
49
|
+
BOLD = tui.BOLD
|
|
50
|
+
DIM = tui.DIM
|
|
51
|
+
RESET = tui.RESET
|
|
52
|
+
CYAN = tui.CYAN
|
|
53
|
+
GREEN = tui.GREEN
|
|
54
|
+
YELLOW = tui.YELLOW
|
|
55
|
+
WHITE = tui.WHITE
|
|
56
|
+
GRAY = tui.GRAY
|
|
57
|
+
RED = tui.RED
|
|
58
|
+
MAGENTA = tui.MAGENTA
|
|
59
|
+
format_box_line = tui.format_box_line
|
|
60
|
+
card_border_top = tui.card_border_top
|
|
61
|
+
card_border_bottom = tui.card_border_bottom
|
|
62
|
+
card_divider = tui.card_divider
|
|
63
|
+
card_header = tui.card_header
|
|
64
|
+
truncate_visual = tui.truncate_visual
|
|
65
|
+
get_visual_width = tui.get_visual_width
|
|
66
|
+
except Exception:
|
|
67
|
+
# Standalone fallback
|
|
68
|
+
BOLD = "\033[1m"
|
|
69
|
+
DIM = "\033[2m"
|
|
70
|
+
RESET = "\033[0m"
|
|
71
|
+
CYAN = "\033[36m"
|
|
72
|
+
GREEN = "\033[32m"
|
|
73
|
+
YELLOW = "\033[33m"
|
|
74
|
+
WHITE = "\033[97m"
|
|
75
|
+
GRAY = "\033[90m"
|
|
76
|
+
RED = "\033[31m"
|
|
77
|
+
MAGENTA = "\033[35m"
|
|
78
|
+
ANSI_REGEX = re.compile(r'\x1b\[[0-9;]*m|\033\[[0-9;]*m')
|
|
79
|
+
def strip_ansi(text: str) -> str: return ANSI_REGEX.sub('', text)
|
|
80
|
+
def get_visual_width(text: str) -> int:
|
|
81
|
+
clean = strip_ansi(text)
|
|
82
|
+
w = 0
|
|
83
|
+
for ch in clean:
|
|
84
|
+
cp = ord(ch)
|
|
85
|
+
if (0xFE00 <= cp <= 0xFE0F) or cp in (0x200B, 0x200C, 0x200D, 0x00AD): continue
|
|
86
|
+
ea = unicodedata.east_asian_width(ch)
|
|
87
|
+
w += 2 if (ea in ('W', 'F') or cp >= 0x1F300) else 1
|
|
88
|
+
return w
|
|
89
|
+
def truncate_visual(text: str, max_w: int = 67) -> str:
|
|
90
|
+
if get_visual_width(text) <= max_w: return text
|
|
91
|
+
return text[:max_w-3] + "..."
|
|
92
|
+
def format_box_line(content: str, width: int = 67, border: str = "│", border_color: str = CYAN) -> str:
|
|
93
|
+
trunc = truncate_visual(content, width)
|
|
94
|
+
vis = get_visual_width(trunc)
|
|
95
|
+
pad = " " * max(0, width - vis)
|
|
96
|
+
return f" {border_color}{border}{RESET} {trunc}{pad} {border_color}{border}{RESET}"
|
|
97
|
+
def card_border_top(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
|
|
98
|
+
left = "╔" if double else "┌"
|
|
99
|
+
right = "╗" if double else "┐"
|
|
100
|
+
h = "═" if double else "─"
|
|
101
|
+
if title:
|
|
102
|
+
vis = get_visual_width(title)
|
|
103
|
+
rem = max(0, 68 - vis)
|
|
104
|
+
return f" {border_color}{left}{h} {BOLD}{WHITE}{title}{RESET}{border_color} {h * rem}{right}{RESET}"
|
|
105
|
+
return f" {border_color}{left}{h * 71}{right}{RESET}"
|
|
106
|
+
def card_border_bottom(border_color: str = CYAN, double: bool = False) -> str:
|
|
107
|
+
left = "╚" if double else "└"
|
|
108
|
+
right = "╝" if double else "┘"
|
|
109
|
+
h = "═" if double else "─"
|
|
110
|
+
return f" {border_color}{left}{h * 71}{right}{RESET}"
|
|
111
|
+
def card_divider(title: str = "", border_color: str = CYAN, double: bool = False) -> str:
|
|
112
|
+
left = "╠" if double else "├"
|
|
113
|
+
right = "╣" if double else "┤"
|
|
114
|
+
h = "═" if double else "─"
|
|
115
|
+
if title:
|
|
116
|
+
vis = get_visual_width(title)
|
|
117
|
+
rem = max(0, 68 - vis)
|
|
118
|
+
return f" {border_color}{left}{h} {BOLD}{WHITE}{title}{RESET}{border_color} {h * rem}{right}{RESET}"
|
|
119
|
+
return f" {border_color}{left}{h * 71}{right}{RESET}"
|
|
120
|
+
def card_header(title: str, subtitle: str = "", version: str = "v1.3.5", border_color: str = CYAN) -> str:
|
|
121
|
+
top = f" {border_color}╭{'─' * 71}╮{RESET}"
|
|
122
|
+
bottom = f" {border_color}╰{'─' * 71}╯{RESET}"
|
|
123
|
+
empty = f" {border_color}│{' ' * 71}│{RESET}"
|
|
124
|
+
space_count = max(1, 67 - get_visual_width(title) - get_visual_width(version))
|
|
125
|
+
t_str = f"{BOLD}{WHITE}{title}{RESET}{' ' * space_count}{GRAY}{version}{RESET}"
|
|
126
|
+
lines = [top, empty, format_box_line(t_str, 67, '│', border_color)]
|
|
127
|
+
if subtitle: lines.append(format_box_line(f"{DIM}{subtitle}{RESET}", 67, '│', border_color))
|
|
128
|
+
lines.extend([empty, bottom])
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
# ─── Rule Definitions & Sinks ─────────────────────────────────────────────────
|
|
132
|
+
class RulePattern(TypedDict):
|
|
133
|
+
rule_id: str
|
|
134
|
+
title: str
|
|
135
|
+
severity: str
|
|
136
|
+
category: str
|
|
137
|
+
cluster: str
|
|
138
|
+
patterns: List[Tuple[Pattern[str], str]]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
RULE_PATTERNS: List[RulePattern] = [
|
|
142
|
+
# ── TG-SEC ───────────────────────────────────────────────────────────────
|
|
143
|
+
{
|
|
144
|
+
"rule_id": "TG-SEC-001",
|
|
145
|
+
"title": "Hardcoded Secret or API Key in Tracked Source",
|
|
146
|
+
"severity": "Critical",
|
|
147
|
+
"category": "secrets",
|
|
148
|
+
"cluster": "cluster-credentials-exposure",
|
|
149
|
+
"patterns": [
|
|
150
|
+
(re.compile(r'\b(?:sk_live|ak_live)_[0-9a-zA-Z]{20,}\b'), "Live payment or cloud API secret key"),
|
|
151
|
+
(re.compile(r'\bgh[pousr]_[0-9a-zA-Z]{36}\b'), "GitHub personal access token"),
|
|
152
|
+
(re.compile(r'-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----'), "Hardcoded private key block"),
|
|
153
|
+
(re.compile(r'\b(?:jwt_secret|jwtSecret|JWT_SECRET)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{16,}["\']'), "Hardcoded JWT signing secret"),
|
|
154
|
+
(re.compile(r'(?:password|passwd|api_key|apiKey|secret_key)\s*[:=]\s*["\'][A-Za-z0-9@#$%^&+=_\-]{10,}["\']', re.IGNORECASE), "Hardcoded credential or API secret string"),
|
|
155
|
+
(re.compile(r'\bxox[baprs]-[0-9a-zA-Z]{10,48}\b'), "Slack API or Bot token"),
|
|
156
|
+
(re.compile(r'\bSG\.[0-9a-zA-Z_\-]{22}\.[0-9a-zA-Z_\-]{43}\b'), "SendGrid API token")
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"rule_id": "TG-SEC-002",
|
|
161
|
+
"title": "Public Client Environment Variable Secret Exposure",
|
|
162
|
+
"severity": "Critical",
|
|
163
|
+
"category": "secrets",
|
|
164
|
+
"cluster": "cluster-credentials-exposure",
|
|
165
|
+
"patterns": [
|
|
166
|
+
(re.compile(r'\b(?:NEXT_PUBLIC_|VITE_|REACT_APP_)[A-Z0-9_]*(?:SECRET|PRIVATE|KEY|PASSWORD|TOKEN)\b'), "Client-exposed environment variable containing secret naming pattern")
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"rule_id": "TG-SEC-003",
|
|
171
|
+
"title": "Tracked Environment File",
|
|
172
|
+
"severity": "High",
|
|
173
|
+
"category": "secrets",
|
|
174
|
+
"cluster": "cluster-credentials-exposure",
|
|
175
|
+
"patterns": [
|
|
176
|
+
(re.compile(r'(?:DATABASE_URL|POSTGRES_PASSWORD|MYSQL_PWD|AWS_SECRET_ACCESS_KEY)\s*=\s*["\']?[^\s"\'#]+["\']?'), "Sensitive production credentials stored in tracked environment configuration")
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"rule_id": "TG-SEC-004",
|
|
181
|
+
"title": "Sensitive Information in Logs",
|
|
182
|
+
"severity": "Medium",
|
|
183
|
+
"category": "secrets",
|
|
184
|
+
"cluster": "cluster-credentials-exposure",
|
|
185
|
+
"patterns": [
|
|
186
|
+
(re.compile(r'(?:console\.log|logger\.(?:info|debug|error|warn)|logging\.(?:info|debug|error))\s*\([^)]*(?:password|token|secret|apiKey|api_key)[^)]*\)', re.IGNORECASE), "Plaintext credential or token logged to output sink")
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"rule_id": "TG-SEC-005",
|
|
191
|
+
"title": "Secret in Git History or Query URL",
|
|
192
|
+
"severity": "Critical",
|
|
193
|
+
"category": "secrets",
|
|
194
|
+
"cluster": "cluster-credentials-exposure",
|
|
195
|
+
"patterns": [
|
|
196
|
+
(re.compile(r'[?&](?:token|access_token|api_key|apiKey|secret|password)=[a-zA-Z0-9_\-\.]{8,}', re.IGNORECASE), "Sensitive credential passed via cleartext URL query parameter")
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"rule_id": "TG-SEC-006",
|
|
201
|
+
"title": "Secret in Build Artifact or Container Layer",
|
|
202
|
+
"severity": "Critical",
|
|
203
|
+
"category": "secrets",
|
|
204
|
+
"cluster": "cluster-credentials-exposure",
|
|
205
|
+
"patterns": [
|
|
206
|
+
(re.compile(r'(?:ENV|ARG)\s+(?:[A-Z0-9_]*SECRET[A-Z0-9_]*|[A-Z0-9_]*KEY[A-Z0-9_]*|PASSWORD)\s*=\s*["\']?[a-zA-Z0-9_\-]{8,}["\']?', re.IGNORECASE), "Hardcoded build secret baked into Docker or container image layer")
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"rule_id": "TG-SEC-007",
|
|
211
|
+
"title": "Secret in Test or Example Fixture Without Mock Marker",
|
|
212
|
+
"severity": "Critical",
|
|
213
|
+
"category": "secrets",
|
|
214
|
+
"cluster": "cluster-credentials-exposure",
|
|
215
|
+
"patterns": [
|
|
216
|
+
(re.compile(r'(?:AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16})'), "Live AWS access key ID in source or fixture code")
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
# ── TG-AUTH ──────────────────────────────────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
"rule_id": "TG-AUTH-001",
|
|
223
|
+
"title": "Weak Password Storage or Deprecated Hashing",
|
|
224
|
+
"severity": "Critical",
|
|
225
|
+
"category": "auth",
|
|
226
|
+
"cluster": "cluster-auth-bypass",
|
|
227
|
+
"patterns": [
|
|
228
|
+
(re.compile(r'\b(?:hashlib\.md5|hashlib\.sha1|createHash\(["\']md5["\']\)|createHash\(["\']sha1["\']\))\s*\([^)]*password', re.IGNORECASE), "Cryptographically broken hash function (MD5/SHA1) used for password processing")
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"rule_id": "TG-AUTH-002",
|
|
233
|
+
"title": "Client-Only Authorization Enforcement",
|
|
234
|
+
"severity": "High",
|
|
235
|
+
"category": "auth",
|
|
236
|
+
"cluster": "cluster-auth-bypass",
|
|
237
|
+
"patterns": [
|
|
238
|
+
(re.compile(r'if\s*\(\s*(?:!user\.isAdmin|user\.role\s*!==\s*["\']admin["\'])\s*\)\s*(?:return|history\.push|navigate)', re.IGNORECASE), "Client-only role authorization without guaranteed backend enforcement")
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"rule_id": "TG-AUTH-003",
|
|
243
|
+
"title": "Missing Object-Level Authorization (IDOR)",
|
|
244
|
+
"severity": "High",
|
|
245
|
+
"category": "auth",
|
|
246
|
+
"cluster": "cluster-auth-bypass",
|
|
247
|
+
"patterns": [
|
|
248
|
+
(re.compile(r'jwt\.verify\s*\(\s*[a-zA-Z0-9_$.\s,]+,\s*[a-zA-Z0-9_$.\s,]+\s*\)(?!\s*,\s*\{[^}]*algorithms)', re.IGNORECASE), "JWT verification missing explicit algorithms restriction parameter")
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"rule_id": "TG-AUTH-004",
|
|
253
|
+
"title": "Insecure Session Cookie Configuration",
|
|
254
|
+
"severity": "High",
|
|
255
|
+
"category": "auth",
|
|
256
|
+
"cluster": "cluster-auth-bypass",
|
|
257
|
+
"patterns": [
|
|
258
|
+
(re.compile(r'\bSESSION_COOKIE_SECURE\s*=\s*False\b'), "Session cookie secure flag disabled in Django settings"),
|
|
259
|
+
(re.compile(r'\bsecure\s*:\s*false\b', re.IGNORECASE), "Cookie secure attribute set to false"),
|
|
260
|
+
(re.compile(r'\bhttpOnly\s*:\s*false\b', re.IGNORECASE), "Cookie httpOnly attribute set to false (XSS readable)"),
|
|
261
|
+
(re.compile(r'@(?:csrf_exempt|allow_anonymous)\b'), "Decorator disabling route-level authentication or CSRF protection"),
|
|
262
|
+
(re.compile(r'\[(?:AllowAnonymous)\]'), "Attribute disabling ASP.NET authentication"),
|
|
263
|
+
(re.compile(r'\.csrf\(\)\.disable\(\)'), "Spring Security CSRF protection explicitly disabled")
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"rule_id": "TG-AUTH-005",
|
|
268
|
+
"title": "Unsafe Password Reset Token Generation",
|
|
269
|
+
"severity": "High",
|
|
270
|
+
"category": "auth",
|
|
271
|
+
"cluster": "cluster-auth-bypass",
|
|
272
|
+
"patterns": [
|
|
273
|
+
(re.compile(r'(?:token|resetCode|resetToken)\s*=\s*(?:Math\.random\(\)|random\.randint\(1000|random\.random\(\))', re.IGNORECASE), "Weak pseudorandom generator used for password reset token or OTP")
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"rule_id": "TG-AUTH-006",
|
|
278
|
+
"title": "Mass Assignment & Privilege Escalation via Payloads",
|
|
279
|
+
"severity": "Critical",
|
|
280
|
+
"category": "auth",
|
|
281
|
+
"cluster": "cluster-auth-bypass",
|
|
282
|
+
"patterns": [
|
|
283
|
+
(re.compile(r'(?:User\.create|User\.update|user\.update)\s*\(\s*(?:req\.body|request\.data)\s*\)'), "Direct mass assignment of unverified request body onto user model")
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"rule_id": "TG-AUTH-007",
|
|
288
|
+
"title": "Missing Object-Level and Property-Level Authorization (IDOR)",
|
|
289
|
+
"severity": "Critical",
|
|
290
|
+
"category": "auth",
|
|
291
|
+
"cluster": "cluster-auth-bypass",
|
|
292
|
+
"patterns": [
|
|
293
|
+
(re.compile(r'(?:db\.[a-zA-Z]+\.find(?:One)?|Model\.find(?:One)?)\s*\(\s*\{\s*_?id\s*:\s*(?:req\.params|req\.query)\.id\s*\}\s*\)'), "Direct database lookup by user-controlled ID without tenant or ownership check")
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"rule_id": "TG-AUTH-008",
|
|
298
|
+
"title": "Untrusted Role or Tenant Header Injection",
|
|
299
|
+
"severity": "Critical",
|
|
300
|
+
"category": "auth",
|
|
301
|
+
"cluster": "cluster-auth-bypass",
|
|
302
|
+
"patterns": [
|
|
303
|
+
(re.compile(r'(?:req\.headers|request\.headers)\s*\[?\s*["\']x-(?:user-role|role|is-admin|tenant-id)["\']\s*\]?', re.IGNORECASE), "Directly trusting unverified client HTTP header for role or tenant assignment")
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
# ── TG-DB ────────────────────────────────────────────────────────────────
|
|
308
|
+
{
|
|
309
|
+
"rule_id": "TG-DB-001",
|
|
310
|
+
"title": "Frontend Database Query Logic",
|
|
311
|
+
"severity": "High",
|
|
312
|
+
"category": "database",
|
|
313
|
+
"cluster": "cluster-client-leak",
|
|
314
|
+
"patterns": [
|
|
315
|
+
(re.compile(r'\b(?:createClient|supabase)\.from\([^)]+\)\.delete\(\)'), "Dangerous direct database mutation in client component"),
|
|
316
|
+
(re.compile(r'\bimport\s+.*(?:pg|mysql2|prisma|typeorm).*\s+from'), "Server database driver imported in client file")
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"rule_id": "TG-DB-002",
|
|
321
|
+
"title": "Privileged Database Credential in Browser Context",
|
|
322
|
+
"severity": "Critical",
|
|
323
|
+
"category": "database",
|
|
324
|
+
"cluster": "cluster-client-leak",
|
|
325
|
+
"patterns": [
|
|
326
|
+
(re.compile(r'\b(?:SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL)\b'), "Service role key or raw database connection string reference in frontend code")
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"rule_id": "TG-DB-003",
|
|
331
|
+
"title": "Frontend Use of Admin or Server-Only Database SDKs",
|
|
332
|
+
"severity": "Critical",
|
|
333
|
+
"category": "database",
|
|
334
|
+
"cluster": "cluster-client-leak",
|
|
335
|
+
"patterns": [
|
|
336
|
+
(re.compile(r'import\s+.*(?:firebase-admin|@prisma/client|mongoose).*from\s+["\']', re.IGNORECASE), "Server-only database ORM/SDK imported in client code")
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"rule_id": "TG-DB-004",
|
|
341
|
+
"title": "Missing Tenant Query Isolation in Multi-Tenant Models",
|
|
342
|
+
"severity": "Critical",
|
|
343
|
+
"category": "database",
|
|
344
|
+
"cluster": "cluster-tenant-isolation",
|
|
345
|
+
"patterns": [
|
|
346
|
+
(re.compile(r'\.objects\.get\s*\(\s*id\s*=\s*[^,\)]+\)'), "Django ORM .get(id=...) query lacking explicit tenant scope filter"),
|
|
347
|
+
(re.compile(r'\.findById\s*\(\s*(?:req\.params|id)[^\)]*\)'), "Direct findById query lacking organization/tenant boundaries"),
|
|
348
|
+
(re.compile(r'prisma\.[a-zA-Z]+\.findUnique\s*\(\s*\{\s*where:\s*\{\s*id\b'), "Prisma findUnique query lacking tenantId boundary filter")
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
# ── TG-INPUT ─────────────────────────────────────────────────────────────
|
|
353
|
+
{
|
|
354
|
+
"rule_id": "TG-INPUT-001",
|
|
355
|
+
"title": "Missing Server-Side Request Validation",
|
|
356
|
+
"severity": "High",
|
|
357
|
+
"category": "injection",
|
|
358
|
+
"cluster": "cluster-injection",
|
|
359
|
+
"patterns": [
|
|
360
|
+
(re.compile(r'(?:const|let|var)\s+\{[^}]+\}\s*=\s*req\.body\s*;?\s*(?!\s*(?:const|let|var|[a-zA-Z0-9_]+\.parse|\.validate))'), "Destructuring unvalidated req.body without schema validation")
|
|
361
|
+
]
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"rule_id": "TG-INPUT-002",
|
|
365
|
+
"title": "Raw SQL Concatenation",
|
|
366
|
+
"severity": "Critical",
|
|
367
|
+
"category": "injection",
|
|
368
|
+
"cluster": "cluster-injection",
|
|
369
|
+
"patterns": [
|
|
370
|
+
(re.compile(r'(?:query|execute|raw)\s*\(\s*f["\'].*SELECT.*\{', re.IGNORECASE), "Python f-string interpolation in raw SQL query"),
|
|
371
|
+
(re.compile(r'(?:query|execute|raw)\s*\(\s*[`"\'].*SELECT.*\$\{', re.IGNORECASE), "JavaScript template literal interpolation in SQL query"),
|
|
372
|
+
(re.compile(r'(?:query|execute|raw)\s*\(\s*["\'].*SELECT.*\+\s*(?:req|input|params)', re.IGNORECASE), "String concatenation in raw SQL query")
|
|
373
|
+
]
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"rule_id": "TG-INPUT-003",
|
|
377
|
+
"title": "Unsafe Dynamic Code or HTML Execution",
|
|
378
|
+
"severity": "High",
|
|
379
|
+
"category": "injection",
|
|
380
|
+
"cluster": "cluster-injection",
|
|
381
|
+
"patterns": [
|
|
382
|
+
(re.compile(r'\beval\s*\('), "Direct eval() execution on untrusted input"),
|
|
383
|
+
(re.compile(r'\bexec\s*\('), "Direct exec() execution on untrusted input"),
|
|
384
|
+
(re.compile(r'\bdangerouslySetInnerHTML\s*='), "React dangerouslySetInnerHTML rendering raw unescaped HTML"),
|
|
385
|
+
(re.compile(r'\.innerHTML\s*='), "Direct DOM innerHTML assignment vulnerable to XSS")
|
|
386
|
+
]
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"rule_id": "TG-INPUT-004",
|
|
390
|
+
"title": "Command Injection via Shell Execution",
|
|
391
|
+
"severity": "High",
|
|
392
|
+
"category": "injection",
|
|
393
|
+
"cluster": "cluster-injection",
|
|
394
|
+
"patterns": [
|
|
395
|
+
(re.compile(r'(?:child_process\.exec|execSync|subprocess\.Popen|os\.system)\s*\([^)]*\+\s*(?:req|params|cmd|input)'), "Untrusted user parameter concatenated into shell execution command")
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"rule_id": "TG-INPUT-005",
|
|
400
|
+
"title": "Unsafe Template Rendering & Disabled Autoescaping",
|
|
401
|
+
"severity": "High",
|
|
402
|
+
"category": "injection",
|
|
403
|
+
"cluster": "cluster-injection",
|
|
404
|
+
"patterns": [
|
|
405
|
+
(re.compile(r'render_template_string\s*\('), "Flask render_template_string rendering untrusted input"),
|
|
406
|
+
(re.compile(r'autoescape\s*=\s*False\b'), "Template engine autoescape explicitly disabled")
|
|
407
|
+
]
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"rule_id": "TG-INPUT-006",
|
|
411
|
+
"title": "Path Traversal and Unsafe Upload Storage",
|
|
412
|
+
"severity": "Critical",
|
|
413
|
+
"category": "filesystem",
|
|
414
|
+
"cluster": "cluster-injection",
|
|
415
|
+
"patterns": [
|
|
416
|
+
(re.compile(r'(?:open|readFile|readFileSync)\s*\([^)]*\+\s*(?:req|params|query)'), "File read operation using unsanitized user request path"),
|
|
417
|
+
(re.compile(r'path\.join\s*\([^)]*(?:req\.params|req\.query)\.[a-zA-Z0-9_]+\)'), "Path traversal sink in path.join without basename sanitization")
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
# ── TG-RATE ──────────────────────────────────────────────────────────────
|
|
422
|
+
{
|
|
423
|
+
"rule_id": "TG-RATE-001",
|
|
424
|
+
"title": "Unlimited Authentication Endpoint",
|
|
425
|
+
"severity": "High",
|
|
426
|
+
"category": "rate",
|
|
427
|
+
"cluster": "cluster-rate-limiting",
|
|
428
|
+
"patterns": [
|
|
429
|
+
(re.compile(r'(?:app|router)\.post\s*\(\s*["\']/(?:api/)?(?:auth/)?(?:login|signin|register|reset-password)["\'],\s*(?:async\s*)?\('), "Authentication POST endpoint defined without rate-limiting middleware")
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"rule_id": "TG-RATE-002",
|
|
434
|
+
"title": "Unlimited Public Write Endpoint",
|
|
435
|
+
"severity": "Medium",
|
|
436
|
+
"category": "rate",
|
|
437
|
+
"cluster": "cluster-rate-limiting",
|
|
438
|
+
"patterns": [
|
|
439
|
+
(re.compile(r'(?:app|router)\.post\s*\(\s*["\']/(?:api/)?(?:contact|feedback|comments|messages)["\'],\s*(?:async\s*)?\('), "Public submission endpoint defined without rate-limiting or captcha defense")
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"rule_id": "TG-RATE-003",
|
|
444
|
+
"title": "Unbounded Resource Consumption",
|
|
445
|
+
"severity": "High",
|
|
446
|
+
"category": "rate",
|
|
447
|
+
"cluster": "cluster-rate-limiting",
|
|
448
|
+
"patterns": [
|
|
449
|
+
(re.compile(r'(?:findMany|find)\s*\(\s*\{\s*\}\s*\)'), "Database query returning all records without pagination take/limit"),
|
|
450
|
+
(re.compile(r'SELECT\s+\*\s+FROM\s+[a-zA-Z0-9_]+\s*;', re.IGNORECASE), "Raw unbounded SELECT * query lacking LIMIT clause")
|
|
451
|
+
]
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
# ── TG-AGENT ─────────────────────────────────────────────────────────────
|
|
455
|
+
{
|
|
456
|
+
"rule_id": "TG-AGENT-001",
|
|
457
|
+
"title": "Prompt Injection in System Context Files",
|
|
458
|
+
"severity": "Critical",
|
|
459
|
+
"category": "agent",
|
|
460
|
+
"cluster": "cluster-agent-security",
|
|
461
|
+
"patterns": [
|
|
462
|
+
(re.compile(r'\{[^{}]*role\s*:\s*["\']system["\'][^{}]*content\s*:[^{}]*\$\{'), "User-controlled template string concatenated into LLM system prompt"),
|
|
463
|
+
(re.compile(r'\[\s*\{\s*["\']role["\']\s*:\s*["\']system["\']\s*,\s*["\']content["\']\s*:\s*f["\']'), "Python f-string interpolation inside LLM system prompt message")
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"rule_id": "TG-AGENT-002",
|
|
468
|
+
"title": "Unsafe Tool Dispatch & Shell Execution without Sandboxing",
|
|
469
|
+
"severity": "Critical",
|
|
470
|
+
"category": "agent",
|
|
471
|
+
"cluster": "cluster-agent-security",
|
|
472
|
+
"patterns": [
|
|
473
|
+
(re.compile(r'(?:exec|spawnSync|run_command|os\.system)\s*\([^)]*tool_call\.(?:arguments|args)'), "AI agent tool call executing unsandboxed shell command")
|
|
474
|
+
]
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"rule_id": "TG-AGENT-003",
|
|
478
|
+
"title": "Overly Broad MCP Tool Scoping & Credential Access",
|
|
479
|
+
"severity": "High",
|
|
480
|
+
"category": "agent",
|
|
481
|
+
"cluster": "cluster-agent-security",
|
|
482
|
+
"patterns": [
|
|
483
|
+
(re.compile(r'env\s*:\s*process\.env\b'), "Exposing entire host process.env to sub-agent or MCP tool environment")
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"rule_id": "TG-AGENT-004",
|
|
488
|
+
"title": "Persistent Memory & Cross-Session Information Leakage",
|
|
489
|
+
"severity": "High",
|
|
490
|
+
"category": "agent",
|
|
491
|
+
"cluster": "cluster-agent-security",
|
|
492
|
+
"patterns": [
|
|
493
|
+
(re.compile(r'globalMemory\s*\[\s*["\'][a-zA-Z0-9_]+["\']\s*\]\s*='), "Global shared memory variable persisting cross-session user conversations")
|
|
494
|
+
]
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
# ── TG-SSRF ──────────────────────────────────────────────────────────────
|
|
498
|
+
{
|
|
499
|
+
"rule_id": "TG-SSRF-001",
|
|
500
|
+
"title": "User-Controlled Server-Side URL Fetch",
|
|
501
|
+
"severity": "Critical",
|
|
502
|
+
"category": "ssrf",
|
|
503
|
+
"cluster": "cluster-ssrf",
|
|
504
|
+
"patterns": [
|
|
505
|
+
(re.compile(r'(?:fetch|axios\.get|requests\.get|urllib\.request\.urlopen)\s*\(\s*(?:req\.query|req\.body|request\.GET)\.[a-zA-Z0-9_]+'), "Outbound HTTP request directly invoking user-supplied URL parameter")
|
|
506
|
+
]
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"rule_id": "TG-SSRF-002",
|
|
510
|
+
"title": "Missing Internal Network Protection",
|
|
511
|
+
"severity": "Critical",
|
|
512
|
+
"category": "ssrf",
|
|
513
|
+
"cluster": "cluster-ssrf",
|
|
514
|
+
"patterns": [
|
|
515
|
+
(re.compile(r'(?:fetch|axios(?:\.get|\.post)?|requests\.(?:get|post)|urllib\.request\.urlopen|http\.get)\s*\(\s*["\']https?://(?:localhost|127\.0\.0\.1|169\.254\.169\.254|metadata\.google)'), "Server-side HTTP request targeting internal loopback or cloud metadata endpoint"),
|
|
516
|
+
(re.compile(r'(?:target_url|dest_url|request_url|backend_url|url)\s*=\s*["\']https?://(?:169\.254\.169\.254|metadata\.google)'), "Target URL assigned to internal cloud metadata endpoint")
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"rule_id": "TG-SSRF-003",
|
|
521
|
+
"title": "Unsafe Redirect Following",
|
|
522
|
+
"severity": "Critical",
|
|
523
|
+
"category": "ssrf",
|
|
524
|
+
"cluster": "cluster-ssrf",
|
|
525
|
+
"patterns": [
|
|
526
|
+
(re.compile(r'(?:maxRedirects\s*:\s*(?:[5-9]|[1-9][0-9])|allow_redirects\s*=\s*True)'), "HTTP client configured to follow redirects without domain revalidation")
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"rule_id": "TG-SSRF-004",
|
|
531
|
+
"title": "Unbounded Outbound Request Without Timeout",
|
|
532
|
+
"severity": "Critical",
|
|
533
|
+
"category": "ssrf",
|
|
534
|
+
"cluster": "cluster-ssrf",
|
|
535
|
+
"patterns": [
|
|
536
|
+
(re.compile(r'requests\.(?:get|post|put)\s*\((?![^)]*\btimeout\s*=)[^)]*\)'), "Python requests call without explicit timeout parameter")
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
# ── TG-WEBHOOK ───────────────────────────────────────────────────────────
|
|
541
|
+
{
|
|
542
|
+
"rule_id": "TG-WEBHOOK-001",
|
|
543
|
+
"title": "Missing Webhook Signature Verification",
|
|
544
|
+
"severity": "Critical",
|
|
545
|
+
"category": "webhook",
|
|
546
|
+
"cluster": "cluster-webhook",
|
|
547
|
+
"patterns": [
|
|
548
|
+
(re.compile(r'(?:app|router)\.post\s*\(\s*["\']/(?:api/)?(?:stripe|github|webhooks?)(?:/[a-zA-Z0-9_]+)?["\'],\s*(?:async\s*)?\(\s*(?:req|request)'), "Webhook endpoint handler defined without cryptographic signature verification middleware")
|
|
549
|
+
]
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"rule_id": "TG-WEBHOOK-002",
|
|
553
|
+
"title": "Replayable Webhook Without Timestamp Validation",
|
|
554
|
+
"severity": "Critical",
|
|
555
|
+
"category": "webhook",
|
|
556
|
+
"cluster": "cluster-webhook",
|
|
557
|
+
"patterns": [
|
|
558
|
+
(re.compile(r'verifyWebhook\s*\([^)]*\)(?!.*timestamp)'), "Webhook signature check without tolerance or timestamp replay defense")
|
|
559
|
+
]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"rule_id": "TG-WEBHOOK-003",
|
|
563
|
+
"title": "Missing Webhook Idempotency",
|
|
564
|
+
"severity": "Critical",
|
|
565
|
+
"category": "webhook",
|
|
566
|
+
"cluster": "cluster-webhook",
|
|
567
|
+
"patterns": [
|
|
568
|
+
(re.compile(r'(?:handleWebhook|processWebhookEvent)\s*\(\s*event\s*\)\s*\{(?!.*(?:hasProcessed|processedEvents|idempotency))'), "Webhook processing function without event ID deduplication record")
|
|
569
|
+
]
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"rule_id": "TG-WEBHOOK-004",
|
|
573
|
+
"title": "Unvalidated Webhook Event Type",
|
|
574
|
+
"severity": "Critical",
|
|
575
|
+
"category": "webhook",
|
|
576
|
+
"cluster": "cluster-webhook",
|
|
577
|
+
"patterns": [
|
|
578
|
+
(re.compile(r'handlers\s*\[\s*event\.type\s*\]\s*\('), "Dynamic invocation of webhook handler via unverified event.type index")
|
|
579
|
+
]
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
# ── TG-WS ────────────────────────────────────────────────────────────────
|
|
583
|
+
{
|
|
584
|
+
"rule_id": "TG-WS-001",
|
|
585
|
+
"title": "Unauthenticated WebSocket Handshake",
|
|
586
|
+
"severity": "High",
|
|
587
|
+
"category": "ws",
|
|
588
|
+
"cluster": "cluster-websocket",
|
|
589
|
+
"patterns": [
|
|
590
|
+
(re.compile(r'wss?\.on\s*\(\s*["\']connection["\'],\s*\(\s*ws\s*,\s*req\s*\)\s*=>\s*\{(?!.*(?:token|auth|verify|session))'), "WebSocket server accepting connections without token verification in handshake")
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
"rule_id": "TG-WS-002",
|
|
595
|
+
"title": "Missing Channel-Level Authorization",
|
|
596
|
+
"severity": "Critical",
|
|
597
|
+
"category": "ws",
|
|
598
|
+
"cluster": "cluster-websocket",
|
|
599
|
+
"patterns": [
|
|
600
|
+
(re.compile(r'socket\.join\s*\(\s*data\.channel\s*\)(?!.*(?:canAccess|hasPermission|isMember))'), "Socket joining room or channel without user permission verification")
|
|
601
|
+
]
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
"rule_id": "TG-WS-003",
|
|
605
|
+
"title": "Unvalidated WebSocket Message",
|
|
606
|
+
"severity": "Critical",
|
|
607
|
+
"category": "ws",
|
|
608
|
+
"cluster": "cluster-websocket",
|
|
609
|
+
"patterns": [
|
|
610
|
+
(re.compile(r'ws\.on\s*\(\s*["\']message["\'],\s*(?:async\s*)?\(\s*data\s*\)\s*=>\s*\{\s*const\s+[a-zA-Z0-9_]+\s*=\s*JSON\.parse'), "Parsing WebSocket message payload without schema validation")
|
|
611
|
+
]
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"rule_id": "TG-WS-004",
|
|
615
|
+
"title": "Unbounded WebSocket Connections",
|
|
616
|
+
"severity": "Critical",
|
|
617
|
+
"category": "ws",
|
|
618
|
+
"cluster": "cluster-websocket",
|
|
619
|
+
"patterns": [
|
|
620
|
+
(re.compile(r'new\s+WebSocketServer\s*\(\s*\{\s*server\s*\}\s*\)'), "WebSocketServer instantiated without maxPayload or client connection bounds")
|
|
621
|
+
]
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
# ── TG-CSRF ──────────────────────────────────────────────────────────────
|
|
625
|
+
{
|
|
626
|
+
"rule_id": "TG-CSRF-001",
|
|
627
|
+
"title": "Missing CSRF Protection",
|
|
628
|
+
"severity": "Critical",
|
|
629
|
+
"category": "csrf",
|
|
630
|
+
"cluster": "cluster-auth-bypass",
|
|
631
|
+
"patterns": [
|
|
632
|
+
(re.compile(r'<form\s+[^>]*method=["\']POST["\'][^>]*>(?![\s\S]*name=["\'](?:csrf_token|csrfToken|_csrf)["\'])'), "HTML POST form without hidden CSRF token input field")
|
|
633
|
+
]
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"rule_id": "TG-CSRF-002",
|
|
637
|
+
"title": "Unsafe Credentialed Cross-Origin Request",
|
|
638
|
+
"severity": "Critical",
|
|
639
|
+
"category": "csrf",
|
|
640
|
+
"cluster": "cluster-auth-bypass",
|
|
641
|
+
"patterns": [
|
|
642
|
+
(re.compile(r'res\.setHeader\s*\(\s*["\']Access-Control-Allow-Origin["\']\s*,\s*["\']\*["\']\s*\);\s*res\.setHeader\s*\(\s*["\']Access-Control-Allow-Credentials["\']\s*,\s*["\']true["\']'), "Wildcard CORS origin paired with credentials flag")
|
|
643
|
+
]
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
# ── TG-GQL ───────────────────────────────────────────────────────────────
|
|
647
|
+
{
|
|
648
|
+
"rule_id": "TG-GQL-001",
|
|
649
|
+
"title": "Missing Query Depth or Complexity Limits",
|
|
650
|
+
"severity": "High",
|
|
651
|
+
"category": "gql",
|
|
652
|
+
"cluster": "cluster-graphql",
|
|
653
|
+
"patterns": [
|
|
654
|
+
(re.compile(r'new\s+ApolloServer\s*\(\s*\{\s*typeDefs\s*,\s*resolvers\s*\}\s*\)'), "ApolloServer initialized without query depth or complexity limits")
|
|
655
|
+
]
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
"rule_id": "TG-GQL-002",
|
|
659
|
+
"title": "Resolver Missing Authorization",
|
|
660
|
+
"severity": "Critical",
|
|
661
|
+
"category": "gql",
|
|
662
|
+
"cluster": "cluster-graphql",
|
|
663
|
+
"patterns": [
|
|
664
|
+
(re.compile(r'Mutation\s*:\s*\{[\s\S]*?(?:delete|update|mutate)[a-zA-Z0-9_]*\s*:\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{(?!.*(?:context\.user|context\.auth))'), "GraphQL mutation resolver executing without user authorization check")
|
|
665
|
+
]
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
"rule_id": "TG-GQL-003",
|
|
669
|
+
"title": "Unbounded GraphQL Pagination or Batching",
|
|
670
|
+
"severity": "Critical",
|
|
671
|
+
"category": "gql",
|
|
672
|
+
"cluster": "cluster-graphql",
|
|
673
|
+
"patterns": [
|
|
674
|
+
(re.compile(r'resolve\s*\(\s*parent\s*,\s*args\s*\)\s*\{\s*return\s+db\.[a-zA-Z]+\.find\(\)'), "GraphQL field resolver returning unpaginated database collection")
|
|
675
|
+
]
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"rule_id": "TG-GQL-004",
|
|
679
|
+
"title": "Unnecessary Production Introspection",
|
|
680
|
+
"severity": "Critical",
|
|
681
|
+
"category": "gql",
|
|
682
|
+
"cluster": "cluster-graphql",
|
|
683
|
+
"patterns": [
|
|
684
|
+
(re.compile(r'\bintrospection\s*:\s*true\b', re.IGNORECASE), "GraphQL schema introspection enabled in configuration")
|
|
685
|
+
]
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
# ── TG-SUPPLY ────────────────────────────────────────────────────────────
|
|
689
|
+
{
|
|
690
|
+
"rule_id": "TG-SUPPLY-001",
|
|
691
|
+
"title": "Missing or Ignored Dependency Lockfile",
|
|
692
|
+
"severity": "Critical",
|
|
693
|
+
"category": "supply",
|
|
694
|
+
"cluster": "cluster-supply-chain",
|
|
695
|
+
"patterns": [
|
|
696
|
+
(re.compile(r'^\s*package-lock\.json\s*$', re.MULTILINE), "Lockfile package-lock.json explicitly ignored in .gitignore")
|
|
697
|
+
]
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
"rule_id": "TG-SUPPLY-002",
|
|
701
|
+
"title": "Vulnerable Dependency Review Missing",
|
|
702
|
+
"severity": "Critical",
|
|
703
|
+
"category": "supply",
|
|
704
|
+
"cluster": "cluster-supply-chain",
|
|
705
|
+
"patterns": [
|
|
706
|
+
(re.compile(r'(?:npm|yarn|pnpm)\s+(?:install|ci)\s+.*--(?:no-audit|audit=false)'), "Dependency installation command skipping security audit checks")
|
|
707
|
+
]
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
"rule_id": "TG-SUPPLY-003",
|
|
711
|
+
"title": "Unsafe CI/CD Secret Exposure",
|
|
712
|
+
"severity": "Critical",
|
|
713
|
+
"category": "supply",
|
|
714
|
+
"cluster": "cluster-supply-chain",
|
|
715
|
+
"patterns": [
|
|
716
|
+
(re.compile(r'run\s*:\s*echo\s+["\']?\$\{\{\s*secrets\.[A-Z0-9_]+\s*\}\}'), "CI workflow step echoing secret into pipeline logs")
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
"rule_id": "TG-SUPPLY-004",
|
|
721
|
+
"title": "Unpinned CI Action",
|
|
722
|
+
"severity": "Critical",
|
|
723
|
+
"category": "supply",
|
|
724
|
+
"cluster": "cluster-supply-chain",
|
|
725
|
+
"patterns": [
|
|
726
|
+
(re.compile(r'uses\s*:\s*actions\/[a-zA-Z0-9_\-]+@(?!main)[a-zA-Z0-9_\.]+(?<![a-f0-9]{40})$'), "GitHub Action referenced by mutable tag instead of immutable commit SHA")
|
|
727
|
+
]
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"rule_id": "TG-SUPPLY-005",
|
|
731
|
+
"title": "Unsafe Dependency Install Script",
|
|
732
|
+
"severity": "Critical",
|
|
733
|
+
"category": "supply",
|
|
734
|
+
"cluster": "cluster-supply-chain",
|
|
735
|
+
"patterns": [
|
|
736
|
+
(re.compile(r'curl\s+-[a-zA-Z]*s[a-zA-Z]*\s+https?:\/\/[^\s|]+\s*\|\s*(?:bash|sh)'), "Remote installation script piped directly to bash without checksum verification")
|
|
737
|
+
]
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
"rule_id": "TG-SUPPLY-006",
|
|
741
|
+
"title": "Container Build Secret Persistence",
|
|
742
|
+
"severity": "High",
|
|
743
|
+
"category": "supply",
|
|
744
|
+
"cluster": "cluster-supply-chain",
|
|
745
|
+
"patterns": [
|
|
746
|
+
(re.compile(r'COPY\s+\.env\s+'), "Production Dockerfile copying local .env into container filesystem")
|
|
747
|
+
]
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
# ── TG-BIZ ───────────────────────────────────────────────────────────────
|
|
751
|
+
{
|
|
752
|
+
"rule_id": "TG-BIZ-001",
|
|
753
|
+
"title": "Client-Controlled Sensitive Business Value",
|
|
754
|
+
"severity": "Critical",
|
|
755
|
+
"category": "biz",
|
|
756
|
+
"cluster": "cluster-biz-logic",
|
|
757
|
+
"patterns": [
|
|
758
|
+
(re.compile(r'(?:amount|price|discount|total)\s*:\s*(?:req\.body|request\.data)\.(?:amount|price|discount)'), "Sensitive pricing or discount amount accepted directly from client payload")
|
|
759
|
+
]
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"rule_id": "TG-BIZ-002",
|
|
763
|
+
"title": "Replayable One-Time Operation",
|
|
764
|
+
"severity": "Critical",
|
|
765
|
+
"category": "biz",
|
|
766
|
+
"cluster": "cluster-biz-logic",
|
|
767
|
+
"patterns": [
|
|
768
|
+
(re.compile(r'(?:chargeCard|processPayment|transferFunds)\s*\([^)]*\)(?!.*idempotency)'), "Financial transaction execution without idempotency key parameter")
|
|
769
|
+
]
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
"rule_id": "TG-BIZ-003",
|
|
773
|
+
"title": "Missing Workflow-State Validation",
|
|
774
|
+
"severity": "Critical",
|
|
775
|
+
"category": "biz",
|
|
776
|
+
"cluster": "cluster-biz-logic",
|
|
777
|
+
"patterns": [
|
|
778
|
+
(re.compile(r'(?:order|invoice)\.status\s*=\s*(?:req\.body|request\.data)\.status'), "Direct client modification of order workflow status")
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
"rule_id": "TG-BIZ-004",
|
|
783
|
+
"title": "Unrestricted Sensitive Business Flow",
|
|
784
|
+
"severity": "Critical",
|
|
785
|
+
"category": "biz",
|
|
786
|
+
"cluster": "cluster-biz-logic",
|
|
787
|
+
"patterns": [
|
|
788
|
+
(re.compile(r'(?:deleteMany|updateMany)\s*\(\s*\{\s*\}\s*\)'), "Bulk destructive mutation executing without filter criteria or threshold guard")
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
# ── TG-CACHE ─────────────────────────────────────────────────────────────
|
|
793
|
+
{
|
|
794
|
+
"rule_id": "TG-CACHE-001",
|
|
795
|
+
"title": "Sensitive Response Publicly Cacheable",
|
|
796
|
+
"severity": "Critical",
|
|
797
|
+
"category": "cache",
|
|
798
|
+
"cluster": "cluster-cache",
|
|
799
|
+
"patterns": [
|
|
800
|
+
(re.compile(r'Cache-Control["\']?\s*,\s*["\']public,\s*max-age=\d+["\']', re.IGNORECASE), "Public caching header configured on potentially sensitive response")
|
|
801
|
+
]
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"rule_id": "TG-CACHE-002",
|
|
805
|
+
"title": "Missing User Cache Isolation",
|
|
806
|
+
"severity": "Critical",
|
|
807
|
+
"category": "cache",
|
|
808
|
+
"cluster": "cluster-cache",
|
|
809
|
+
"patterns": [
|
|
810
|
+
(re.compile(r'cache\.set\s*\(\s*["\']user_profile_["\']\s*\+\s*id\s*,\s*data\s*\)(?!.*tenant)'), "Cache key lacking tenant isolation prefix")
|
|
811
|
+
]
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
"rule_id": "TG-CACHE-003",
|
|
815
|
+
"title": "Sensitive Data in URL",
|
|
816
|
+
"severity": "Critical",
|
|
817
|
+
"category": "cache",
|
|
818
|
+
"cluster": "cluster-cache",
|
|
819
|
+
"patterns": [
|
|
820
|
+
(re.compile(r'(?:router|app)\.get\s*\(\s*["\'][^"\']*(?:password|ssn|credit_card)[^"\']*["\']'), "Sensitive PII or password transmitted in GET URL query path")
|
|
821
|
+
]
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
# ── TG-CLIENT ────────────────────────────────────────────────────────────
|
|
825
|
+
{
|
|
826
|
+
"rule_id": "TG-CLIENT-001",
|
|
827
|
+
"title": "Public Production Source Maps",
|
|
828
|
+
"severity": "Medium",
|
|
829
|
+
"category": "client",
|
|
830
|
+
"cluster": "cluster-client-leak",
|
|
831
|
+
"patterns": [
|
|
832
|
+
(re.compile(r'\bproductionSourceMap\s*:\s*true\b', re.IGNORECASE), "Webpack/Vite productionSourceMap enabled in build configuration"),
|
|
833
|
+
(re.compile(r'\bsourcemap\s*:\s*true\b', re.IGNORECASE), "Build config emitting unminified source maps in production output")
|
|
834
|
+
]
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
"rule_id": "TG-CLIENT-002",
|
|
838
|
+
"title": "Sensitive Client Bundle Content",
|
|
839
|
+
"severity": "High",
|
|
840
|
+
"category": "client",
|
|
841
|
+
"cluster": "cluster-client-leak",
|
|
842
|
+
"patterns": [
|
|
843
|
+
(re.compile(r'["\']use client["\'];[\s\S]*?import\s+.*(?:nodemailer|bcrypt|jsonwebtoken|aws-sdk).*from'), "Server-only private dependency imported into React Client Component")
|
|
844
|
+
]
|
|
845
|
+
},
|
|
846
|
+
|
|
847
|
+
# ── TG-PLATFORM ──────────────────────────────────────────────────────────
|
|
848
|
+
{
|
|
849
|
+
"rule_id": "TG-PLATFORM-001",
|
|
850
|
+
"title": "Wildcard CORS With Credentials",
|
|
851
|
+
"severity": "High",
|
|
852
|
+
"category": "platform",
|
|
853
|
+
"cluster": "cluster-auth-bypass",
|
|
854
|
+
"patterns": [
|
|
855
|
+
(re.compile(r'\bCORS_ALLOW_ALL_ORIGINS\s*=\s*True\b'), "Wildcard CORS origin enabled across entire application"),
|
|
856
|
+
(re.compile(r'@CrossOrigin\s*\(\s*["\']\*["\']\s*\)'), "Wildcard CORS origin annotation in Java/Spring"),
|
|
857
|
+
(re.compile(r'cors\(\s*\{\s*origin\s*:\s*["\']\*["\']'), "Wildcard CORS origin configured in Express middleware")
|
|
858
|
+
]
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
"rule_id": "TG-PLATFORM-002",
|
|
862
|
+
"title": "Missing Security Headers",
|
|
863
|
+
"severity": "Medium",
|
|
864
|
+
"category": "platform",
|
|
865
|
+
"cluster": "cluster-platform",
|
|
866
|
+
"patterns": [
|
|
867
|
+
(re.compile(r'const\s+app\s*=\s*express\(\);(?![\s\S]*app\.use\s*\(\s*helmet)'), "Express application initialized without helmet security headers middleware")
|
|
868
|
+
]
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
"rule_id": "TG-PLATFORM-003",
|
|
872
|
+
"title": "Production Stack Trace Exposure",
|
|
873
|
+
"severity": "Medium",
|
|
874
|
+
"category": "platform",
|
|
875
|
+
"cluster": "cluster-platform",
|
|
876
|
+
"patterns": [
|
|
877
|
+
(re.compile(r'res\.status\(500\)\.(?:send|json)\s*\(\s*(?:err\.stack|error\.stack)\s*\)'), "Uncaught internal error stack trace sent directly to client response"),
|
|
878
|
+
(re.compile(r'\bDEBUG\s*=\s*True\b'), "Django DEBUG mode enabled in configuration")
|
|
879
|
+
]
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
"rule_id": "TG-PLATFORM-004",
|
|
883
|
+
"title": "Missing Request Size Limits",
|
|
884
|
+
"severity": "Medium",
|
|
885
|
+
"category": "platform",
|
|
886
|
+
"cluster": "cluster-platform",
|
|
887
|
+
"patterns": [
|
|
888
|
+
(re.compile(r'express\.(?:json|urlencoded)\s*\(\s*\{\s*limit\s*:\s*["\'](?:[5-9][0-9]|[1-9][0-9]{2,})mb["\']', re.IGNORECASE), "Excessively permissive request body size limit (>50MB) risking denial of service")
|
|
889
|
+
]
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
# ── TG-DIFF ──────────────────────────────────────────────────────────────
|
|
893
|
+
{
|
|
894
|
+
"rule_id": "TG-DIFF-001",
|
|
895
|
+
"title": "Accidental Security Check Bypass in Patch Additions",
|
|
896
|
+
"severity": "Critical",
|
|
897
|
+
"category": "transport",
|
|
898
|
+
"cluster": "cluster-transport-security",
|
|
899
|
+
"patterns": [
|
|
900
|
+
(re.compile(r'\bverify\s*=\s*False\b'), "Disabled TLS certificate verification (verify=False)"),
|
|
901
|
+
(re.compile(r'\bInsecureSkipVerify\s*:\s*true\b'), "Disabled Go TLS certificate verification (InsecureSkipVerify: true)"),
|
|
902
|
+
(re.compile(r'\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*[\'"]?0[\'"]?'), "Disabled Node.js TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0)"),
|
|
903
|
+
(re.compile(r'\brejectUnauthorized\s*:\s*false\b'), "Disabled Node.js TLS rejectUnauthorized"),
|
|
904
|
+
(re.compile(r'\bCURLOPT_SSL_VERIFYPEER\s*(?:=>|,)\s*(?:false|0)\b', re.IGNORECASE), "Disabled PHP curl SSL verification")
|
|
905
|
+
]
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
"rule_id": "TG-DIFF-002",
|
|
909
|
+
"title": "Secret and Token Ingestion in Patch Additions",
|
|
910
|
+
"severity": "Critical",
|
|
911
|
+
"category": "diff",
|
|
912
|
+
"cluster": "cluster-credentials-exposure",
|
|
913
|
+
"patterns": [
|
|
914
|
+
(re.compile(r'^\+[^+].*(?:eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}|ghp_[a-zA-Z0-9]{36})'), "Hardcoded secret or auth token introduced in patch addition")
|
|
915
|
+
]
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
"rule_id": "TG-DIFF-003",
|
|
919
|
+
"title": "Unintentional Removal of Tenant Filter in Patch Deletions",
|
|
920
|
+
"severity": "High",
|
|
921
|
+
"category": "diff",
|
|
922
|
+
"cluster": "cluster-tenant-isolation",
|
|
923
|
+
"patterns": [
|
|
924
|
+
(re.compile(r'^\-[^-].*(?:tenantId|organization_id|account_id)'), "Removal of tenant isolation boundary in patch deletion")
|
|
925
|
+
]
|
|
926
|
+
},
|
|
927
|
+
|
|
928
|
+
# ── TG-EDGE ──────────────────────────────────────────────────────────────
|
|
929
|
+
{
|
|
930
|
+
"rule_id": "TG-EDGE-001",
|
|
931
|
+
"title": "Serverless & Edge Global State Memory Leakage",
|
|
932
|
+
"severity": "High",
|
|
933
|
+
"category": "edge",
|
|
934
|
+
"cluster": "cluster-edge",
|
|
935
|
+
"patterns": [
|
|
936
|
+
(re.compile(r'^(?:let|var)\s+(?:userCache|sessionData|requestState)\s*=\s*\{', re.MULTILINE), "Mutable global state variable declared outside serverless handler function")
|
|
937
|
+
]
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
"rule_id": "TG-EDGE-003",
|
|
941
|
+
"title": "AWS Lambda Ephemeral Execution & Cold Start Security",
|
|
942
|
+
"severity": "High",
|
|
943
|
+
"category": "edge",
|
|
944
|
+
"cluster": "cluster-edge",
|
|
945
|
+
"patterns": [
|
|
946
|
+
(re.compile(r'export\s+const\s+handler\s*=\s*async\s*\([^)]*\)\s*=>\s*\{[\s\S]*?secretsmanager\.getSecretValue'), "Fetching cloud secrets inside request handler loop instead of cold-start cache")
|
|
947
|
+
]
|
|
948
|
+
}
|
|
949
|
+
]
|
|
950
|
+
|
|
951
|
+
# File extensions to scan
|
|
952
|
+
SOURCE_EXTENSIONS = {
|
|
953
|
+
".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
954
|
+
".go", ".java", ".cs", ".php", ".rb", ".json", ".yaml", ".yml"
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
# Directories to skip
|
|
958
|
+
SKIP_DIRS = {
|
|
959
|
+
".git", "node_modules", ".venv", "venv", "env", "__pycache__",
|
|
960
|
+
"dist", "build", ".next", ".nuxt", "coverage", ".torusguard",
|
|
961
|
+
".idea", ".vscode", "target", "bin", "obj", "payload"
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
def is_test_file(path_str: str) -> bool:
|
|
965
|
+
"""Check if file belongs to tests, fixtures, or test harnesses."""
|
|
966
|
+
clean = path_str.replace("\\", "/").lower()
|
|
967
|
+
markers = ["/test/", "/tests/", "/spec/", "/specs/", "/fixtures/", "/mock/", "/mocks/",
|
|
968
|
+
"/harness/", "__tests__", "test_", "_test.", ".test.", ".spec."]
|
|
969
|
+
return any(m in clean for m in markers)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def find_files_to_scan(target_root: Path, include_tests: bool = False) -> List[Path]:
|
|
973
|
+
"""Recursively collect source files eligible for static security audit."""
|
|
974
|
+
files = []
|
|
975
|
+
for root, dirs, filenames in os.walk(target_root):
|
|
976
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
|
|
977
|
+
for f in filenames:
|
|
978
|
+
ext = os.path.splitext(f)[1].lower()
|
|
979
|
+
if ext in SOURCE_EXTENSIONS:
|
|
980
|
+
if f in ("audit_runner.py", "diff_guard.py", "finding_scorer.py", "harden_runner.py", "apply_runner.py", "recheck_runner.py", "report_sync.py", "term_ui.py", "sarif_exporter.py", "safety_gate.py", "stack_detect.py", "recipes_runner.py", "html_reporter.py", "run_manager.py", "rules_sync.py", "bootstrap.py", "scope.json", "rules_catalog.json"):
|
|
981
|
+
continue
|
|
982
|
+
full_path = Path(root) / f
|
|
983
|
+
if not include_tests and is_test_file(str(full_path)):
|
|
984
|
+
continue
|
|
985
|
+
files.append(full_path)
|
|
986
|
+
return sorted(files)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def scan_file(file_path: Path, target_root: Path) -> List[Dict[str, Any]]:
|
|
990
|
+
"""Scan a single source file against the TorusGuard rule families."""
|
|
991
|
+
findings = []
|
|
992
|
+
try:
|
|
993
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
994
|
+
except Exception:
|
|
995
|
+
return findings
|
|
996
|
+
|
|
997
|
+
lines = content.splitlines()
|
|
998
|
+
rel_path = str(file_path.relative_to(target_root)).replace("\\", "/")
|
|
999
|
+
is_test = is_test_file(rel_path)
|
|
1000
|
+
|
|
1001
|
+
for rule in RULE_PATTERNS:
|
|
1002
|
+
rule_id = rule["rule_id"]
|
|
1003
|
+
if rule_id == "TG-PLATFORM-002" and "helmet" in content and "app.use(helmet" in content:
|
|
1004
|
+
continue
|
|
1005
|
+
patterns: List[Tuple[Pattern[str], str]] = rule["patterns"]
|
|
1006
|
+
for regex, desc in patterns:
|
|
1007
|
+
for idx, line in enumerate(lines):
|
|
1008
|
+
stripped = line.strip()
|
|
1009
|
+
if stripped.startswith(("#", "//", "/*", "*")):
|
|
1010
|
+
if "bypass" not in stripped.lower() and "todo" not in stripped.lower():
|
|
1011
|
+
continue
|
|
1012
|
+
|
|
1013
|
+
match = regex.search(line)
|
|
1014
|
+
if match:
|
|
1015
|
+
line_num = idx + 1
|
|
1016
|
+
seed = f"{rule_id}:{rel_path}:{line_num}:{stripped}"
|
|
1017
|
+
fp = f"{rule_id}-{hashlib.sha256(seed.encode('utf-8')).hexdigest()[:8]}"
|
|
1018
|
+
|
|
1019
|
+
start_ctx = max(0, idx - 2)
|
|
1020
|
+
end_ctx = min(len(lines), idx + 3)
|
|
1021
|
+
snippet = "\n".join(lines[start_ctx:end_ctx])
|
|
1022
|
+
|
|
1023
|
+
findings.append({
|
|
1024
|
+
"finding_id": fp,
|
|
1025
|
+
"rule_id": rule_id,
|
|
1026
|
+
"title": rule["title"],
|
|
1027
|
+
"description": desc,
|
|
1028
|
+
"severity": "Low" if is_test else rule["severity"],
|
|
1029
|
+
"category": rule["category"],
|
|
1030
|
+
"cluster": rule["cluster"],
|
|
1031
|
+
"file_path": rel_path,
|
|
1032
|
+
"line_number": line_num,
|
|
1033
|
+
"matched_text": match.group(0)[:80],
|
|
1034
|
+
"snippet": snippet,
|
|
1035
|
+
"is_test": is_test,
|
|
1036
|
+
"target": {
|
|
1037
|
+
"file_path": rel_path,
|
|
1038
|
+
"start_line": line_num,
|
|
1039
|
+
"end_line": line_num
|
|
1040
|
+
}
|
|
1041
|
+
})
|
|
1042
|
+
break
|
|
1043
|
+
return findings
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def score_and_cluster_findings(findings: List[Dict[str, Any]], target_root: Path) -> Tuple[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]]:
|
|
1047
|
+
"""
|
|
1048
|
+
Score each finding using finding_scorer.py and persistent memory patterns.
|
|
1049
|
+
Returns (scored_findings, clusters_map).
|
|
1050
|
+
"""
|
|
1051
|
+
try:
|
|
1052
|
+
import finding_scorer
|
|
1053
|
+
except Exception:
|
|
1054
|
+
finding_scorer = None
|
|
1055
|
+
|
|
1056
|
+
scored = []
|
|
1057
|
+
clusters: Dict[str, List[Dict[str, Any]]] = {}
|
|
1058
|
+
|
|
1059
|
+
for f in findings:
|
|
1060
|
+
score = 70
|
|
1061
|
+
band = "High Confidence"
|
|
1062
|
+
factors = {}
|
|
1063
|
+
|
|
1064
|
+
if finding_scorer:
|
|
1065
|
+
try:
|
|
1066
|
+
eq = 35
|
|
1067
|
+
rs = 0
|
|
1068
|
+
ic = 5
|
|
1069
|
+
ec = 15
|
|
1070
|
+
mr = 0
|
|
1071
|
+
s, b, facts = finding_scorer.compute_confidence_score(
|
|
1072
|
+
evidence_quality=eq,
|
|
1073
|
+
reproduction_success=rs,
|
|
1074
|
+
independent_confirmations=ic,
|
|
1075
|
+
environmental_clarity=ec,
|
|
1076
|
+
manual_review_status=mr,
|
|
1077
|
+
rule_id=f["rule_id"],
|
|
1078
|
+
file_path=f["file_path"],
|
|
1079
|
+
root_dir=target_root
|
|
1080
|
+
)
|
|
1081
|
+
score = s
|
|
1082
|
+
band = b
|
|
1083
|
+
factors = facts
|
|
1084
|
+
except Exception:
|
|
1085
|
+
pass
|
|
1086
|
+
|
|
1087
|
+
f["confidence_score"] = score
|
|
1088
|
+
f["confidence_band"] = band
|
|
1089
|
+
f["confidence_factors"] = factors
|
|
1090
|
+
scored.append(f)
|
|
1091
|
+
|
|
1092
|
+
c_id = f["cluster"]
|
|
1093
|
+
if c_id not in clusters:
|
|
1094
|
+
clusters[c_id] = []
|
|
1095
|
+
clusters[c_id].append(f)
|
|
1096
|
+
|
|
1097
|
+
# Sort descending by severity (Critical -> High -> Medium -> Low), then score
|
|
1098
|
+
sev_rank = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1}
|
|
1099
|
+
scored.sort(key=lambda x: (sev_rank.get(x["severity"], 0), x["confidence_score"]), reverse=True)
|
|
1100
|
+
return scored, clusters
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def emit_run_artifacts(run_folder: Path, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], target_root: Path) -> None:
|
|
1104
|
+
"""Generate findings.json, findings.md, and summary.md into run folder."""
|
|
1105
|
+
now_ist = get_ist_now()
|
|
1106
|
+
|
|
1107
|
+
# 1. findings.json
|
|
1108
|
+
(run_folder / "findings.json").write_text(json.dumps(scored_findings, indent=2), encoding="utf-8")
|
|
1109
|
+
|
|
1110
|
+
# 2. findings.md
|
|
1111
|
+
md_lines = [
|
|
1112
|
+
f"# TorusGuard Security Audit Findings: {run_folder.name}",
|
|
1113
|
+
f"- Target Root: `{target_root}`",
|
|
1114
|
+
f"- Total Scored Findings: `{len(scored_findings)}`",
|
|
1115
|
+
f"- Identified Clusters: `{len(clusters)}`",
|
|
1116
|
+
f"- Generated At: `{now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}`\n",
|
|
1117
|
+
"---",
|
|
1118
|
+
"## Summary of Findings\n"
|
|
1119
|
+
]
|
|
1120
|
+
|
|
1121
|
+
for f in scored_findings:
|
|
1122
|
+
md_lines.append(f"### [{f['severity']}] {f['rule_id']} - {f['title']}")
|
|
1123
|
+
md_lines.append(f"- **Finding ID:** `{f['finding_id']}`")
|
|
1124
|
+
md_lines.append(f"- **Location:** `{f['file_path']}:{f['line_number']}`")
|
|
1125
|
+
md_lines.append(f"- **Confidence Score:** `{f['confidence_score']}/100` ({f['confidence_band']})")
|
|
1126
|
+
md_lines.append(f"- **Cluster:** `{f['cluster']}`")
|
|
1127
|
+
md_lines.append(f"- **Description:** {f['description']}")
|
|
1128
|
+
md_lines.append("```")
|
|
1129
|
+
md_lines.append(f['snippet'])
|
|
1130
|
+
md_lines.append("```\n")
|
|
1131
|
+
|
|
1132
|
+
(run_folder / "findings.md").write_text("\n".join(md_lines), encoding="utf-8")
|
|
1133
|
+
|
|
1134
|
+
# 3. summary.md
|
|
1135
|
+
summary_lines = [
|
|
1136
|
+
f"# TorusGuard Audit Summary: {run_folder.name}",
|
|
1137
|
+
f"- Scanned Scope: {target_root}",
|
|
1138
|
+
f"- Total Findings: {len(scored_findings)}",
|
|
1139
|
+
f"- Root-Cause Clusters: {len(clusters)}",
|
|
1140
|
+
f"- Generated: {now_ist.strftime('%Y-%m-%d %H:%M:%S IST')}"
|
|
1141
|
+
]
|
|
1142
|
+
(run_folder / "summary.md").write_text("\n".join(summary_lines), encoding="utf-8")
|
|
1143
|
+
|
|
1144
|
+
# 4. Update manifest.json status
|
|
1145
|
+
manifest_file = run_folder / "manifest.json"
|
|
1146
|
+
if manifest_file.exists():
|
|
1147
|
+
try:
|
|
1148
|
+
with open(manifest_file, "r", encoding="utf-8") as mf:
|
|
1149
|
+
mdata = json.load(mf)
|
|
1150
|
+
mdata["status"] = "completed"
|
|
1151
|
+
mdata["findings_count"] = len(scored_findings)
|
|
1152
|
+
with open(manifest_file, "w", encoding="utf-8") as mf:
|
|
1153
|
+
json.dump(mdata, mf, indent=2)
|
|
1154
|
+
except Exception:
|
|
1155
|
+
pass
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def print_audit_dashboard(target_root: Path, file_count: int, scored_findings: List[Dict[str, Any]], clusters: Dict[str, List[Dict[str, Any]]], run_folder: Path, detected_stack: Optional[Dict[str, Any]] = None) -> None:
|
|
1159
|
+
"""Print the unified 75-column TorusGuard Static Audit results card."""
|
|
1160
|
+
crit_count = sum(1 for f in scored_findings if f["severity"] == "Critical")
|
|
1161
|
+
high_count = sum(1 for f in scored_findings if f["severity"] == "High")
|
|
1162
|
+
med_count = sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low"))
|
|
1163
|
+
|
|
1164
|
+
stack_str = "Auto-detected"
|
|
1165
|
+
if detected_stack and detected_stack.get("framework") and detected_stack.get("framework") != "None":
|
|
1166
|
+
stack_str = f"{detected_stack.get('framework')} ({detected_stack.get('language')})"
|
|
1167
|
+
|
|
1168
|
+
print()
|
|
1169
|
+
print(card_header("🛡️ TORUSGUARD STATIC SECURITY AUDIT", "Autonomous AST & Invariant Security Scanner", "v1.3.5"))
|
|
1170
|
+
print()
|
|
1171
|
+
|
|
1172
|
+
print(card_border_top("Audit Execution Scope"))
|
|
1173
|
+
print(format_box_line(f"{BOLD}Target:{RESET} {WHITE}{str(target_root)}{RESET}"))
|
|
1174
|
+
print(format_box_line(f"{BOLD}Stack:{RESET} {GREEN}{stack_str}{RESET}"))
|
|
1175
|
+
print(format_box_line(f"{BOLD}Files:{RESET} {WHITE}{file_count} files evaluated across 18 rule families (74 rules){RESET}"))
|
|
1176
|
+
print(card_border_bottom())
|
|
1177
|
+
print()
|
|
1178
|
+
|
|
1179
|
+
# Findings Box
|
|
1180
|
+
status_icon = f"{RED}✖ CRITICAL FINDINGS DETECTED{RESET}" if crit_count > 0 else (f"{YELLOW}⚠ HIGH VULNERABILITIES DETECTED{RESET}" if high_count > 0 else f"{GREEN}✔ SECURITY POSTURE CLEAN{RESET}")
|
|
1181
|
+
print(card_border_top("Findings & Confidence Scores"))
|
|
1182
|
+
print(format_box_line(f"Status: {status_icon}"))
|
|
1183
|
+
print(format_box_line(f"Findings: {RED}{crit_count} Critical{RESET} {YELLOW}{high_count} High{RESET} {CYAN}{med_count} Medium/Low{RESET} ({len(scored_findings)} total)"))
|
|
1184
|
+
print(format_box_line(f"Clusters: {WHITE}{len(clusters)} architectural root causes identified{RESET}"))
|
|
1185
|
+
print(card_divider())
|
|
1186
|
+
|
|
1187
|
+
if not scored_findings:
|
|
1188
|
+
print(format_box_line(f"{GREEN}✔ Zero security invariant violations detected.{RESET}"))
|
|
1189
|
+
else:
|
|
1190
|
+
for idx, f in enumerate(scored_findings[:5]):
|
|
1191
|
+
sev_color = RED if f["severity"] == "Critical" else (YELLOW if f["severity"] == "High" else CYAN)
|
|
1192
|
+
line_str = f"[{sev_color}{f['severity']}{RESET}] {BOLD}{f['rule_id']}{RESET} at {f['file_path']}:{f['line_number']}"
|
|
1193
|
+
print(format_box_line(line_str))
|
|
1194
|
+
desc_str = f" └─ {DIM}{f['description'][:58]}{RESET} ({CYAN}{f['confidence_score']}/100{RESET})"
|
|
1195
|
+
print(format_box_line(desc_str))
|
|
1196
|
+
if len(scored_findings) > 5:
|
|
1197
|
+
print(format_box_line(f"{DIM}... and {len(scored_findings) - 5} more findings in report file{RESET}"))
|
|
1198
|
+
|
|
1199
|
+
print(card_border_bottom())
|
|
1200
|
+
print()
|
|
1201
|
+
|
|
1202
|
+
# Run Artifacts & Next Steps Box
|
|
1203
|
+
rel_run = str(run_folder.relative_to(target_root)).replace("\\", "/") if str(run_folder).startswith(str(target_root)) else str(run_folder).replace("\\", "/")
|
|
1204
|
+
findings_artifact = f"{rel_run}/findings.md"
|
|
1205
|
+
print(card_border_top("Artifacts & Next Actions", border_color=GREEN, double=True))
|
|
1206
|
+
print(format_box_line(f"{BOLD}Run Artifact:{RESET} {WHITE}{findings_artifact}{RESET}", border="║", border_color=GREEN))
|
|
1207
|
+
print(format_box_line(f"{BOLD}Living Ledger:{RESET} {CYAN}security_report.md (updated){RESET}", border="║", border_color=GREEN))
|
|
1208
|
+
print(format_box_line(f"{BOLD}Visual Dash:{RESET} {CYAN}npx torusguard report --html{RESET}", border="║", border_color=GREEN))
|
|
1209
|
+
print(format_box_line(f"{BOLD}Surgical Fix:{RESET} In AI Chat, run {CYAN}/torusguard-harden{RESET}", border="║", border_color=GREEN))
|
|
1210
|
+
print(card_border_bottom(border_color=GREEN, double=True))
|
|
1211
|
+
print()
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def execute_audit(target_root: Path, severity_floor: str = "medium", json_output: bool = False, include_tests: bool = False) -> Dict[str, Any]:
|
|
1215
|
+
"""Execute the full TorusGuard static security audit."""
|
|
1216
|
+
target_root = target_root.resolve()
|
|
1217
|
+
s_dir = Path(__file__).resolve().parent
|
|
1218
|
+
|
|
1219
|
+
# 1. Stack detection
|
|
1220
|
+
detected_stack = None
|
|
1221
|
+
stack_script = s_dir / "stack_detect.py"
|
|
1222
|
+
if stack_script.is_file():
|
|
1223
|
+
try:
|
|
1224
|
+
import importlib.util
|
|
1225
|
+
spec = importlib.util.spec_from_file_location("stack_detect", str(stack_script))
|
|
1226
|
+
if spec and spec.loader:
|
|
1227
|
+
sd = importlib.util.module_from_spec(spec)
|
|
1228
|
+
spec.loader.exec_module(sd)
|
|
1229
|
+
if hasattr(sd, "detect_stack"):
|
|
1230
|
+
detected_stack = sd.detect_stack(target_root)
|
|
1231
|
+
except Exception:
|
|
1232
|
+
pass
|
|
1233
|
+
|
|
1234
|
+
# 2. Collect files & scan
|
|
1235
|
+
files = find_files_to_scan(target_root, include_tests=include_tests)
|
|
1236
|
+
all_findings = []
|
|
1237
|
+
for f in files:
|
|
1238
|
+
all_findings.extend(scan_file(f, target_root))
|
|
1239
|
+
|
|
1240
|
+
# 3. Score & Cluster
|
|
1241
|
+
scored_findings, clusters = score_and_cluster_findings(all_findings, target_root)
|
|
1242
|
+
|
|
1243
|
+
# 4. Allocate run folder
|
|
1244
|
+
runs_dir = target_root / ".torusguard" / "runs"
|
|
1245
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
1246
|
+
|
|
1247
|
+
if str(s_dir) not in sys.path:
|
|
1248
|
+
sys.path.insert(0, str(s_dir))
|
|
1249
|
+
try:
|
|
1250
|
+
import run_manager
|
|
1251
|
+
run_folder = run_manager.create_run(runs_dir, "audit", target_root.name)
|
|
1252
|
+
except Exception:
|
|
1253
|
+
timestamp = get_ist_now().strftime("%Y%m%d-%H%M%S")
|
|
1254
|
+
run_folder = runs_dir / f"run-{timestamp}-audit"
|
|
1255
|
+
run_folder.mkdir(parents=True, exist_ok=True)
|
|
1256
|
+
|
|
1257
|
+
# 5. Emit artifacts & sync memory
|
|
1258
|
+
emit_run_artifacts(run_folder, scored_findings, clusters, target_root)
|
|
1259
|
+
try:
|
|
1260
|
+
import report_sync
|
|
1261
|
+
report_sync.record_audit_findings(target_root, scored_findings, run_folder.name)
|
|
1262
|
+
except Exception:
|
|
1263
|
+
pass
|
|
1264
|
+
try:
|
|
1265
|
+
if "run_manager" in sys.modules:
|
|
1266
|
+
sys.modules["run_manager"].sync_run_to_memory(run_folder, scored_findings)
|
|
1267
|
+
except Exception:
|
|
1268
|
+
pass
|
|
1269
|
+
|
|
1270
|
+
result = {
|
|
1271
|
+
"status": "completed",
|
|
1272
|
+
"target_root": str(target_root),
|
|
1273
|
+
"files_scanned": len(files),
|
|
1274
|
+
"total_findings": len(scored_findings),
|
|
1275
|
+
"critical_count": sum(1 for f in scored_findings if f["severity"] == "Critical"),
|
|
1276
|
+
"high_count": sum(1 for f in scored_findings if f["severity"] == "High"),
|
|
1277
|
+
"medium_count": sum(1 for f in scored_findings if f["severity"] in ("Medium", "Low")),
|
|
1278
|
+
"clusters_count": len(clusters),
|
|
1279
|
+
"run_folder": str(run_folder),
|
|
1280
|
+
"findings": scored_findings
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if json_output:
|
|
1284
|
+
print(json.dumps(result, indent=2))
|
|
1285
|
+
else:
|
|
1286
|
+
print_audit_dashboard(target_root, len(files), scored_findings, clusters, run_folder, detected_stack)
|
|
1287
|
+
|
|
1288
|
+
return result
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def main():
|
|
1292
|
+
parser = argparse.ArgumentParser(description="TorusGuard Static Security Audit Engine")
|
|
1293
|
+
parser.add_argument("path", nargs="?", default=".", help="Target project root directory to audit")
|
|
1294
|
+
parser.add_argument("--scope", "-s", help="Alternative path to target project")
|
|
1295
|
+
parser.add_argument("--severity", choices=["critical", "high", "medium", "low"], default="medium", help="Severity floor")
|
|
1296
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
1297
|
+
parser.add_argument("--include-tests", action="store_true", help="Include test files and harness fixtures in scan")
|
|
1298
|
+
args = parser.parse_args()
|
|
1299
|
+
|
|
1300
|
+
target = Path(args.scope or args.path).resolve()
|
|
1301
|
+
res = execute_audit(target, severity_floor=args.severity, json_output=args.json, include_tests=args.include_tests)
|
|
1302
|
+
sys.exit(0 if res["critical_count"] == 0 else 1)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
if __name__ == "__main__":
|
|
1306
|
+
main()
|