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