@runa-ai/runa-cli 0.5.72 → 0.7.0
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/dist/build-V66FAQXB.js +1719 -0
- package/dist/cache-N7WNPEYF.js +111 -0
- package/dist/check-LOMVIRHX.js +12 -0
- package/dist/chunk-2APB25TT.js +442 -0
- package/dist/chunk-3FDQW524.js +544 -0
- package/dist/chunk-3WDV32GA.js +33 -0
- package/dist/chunk-5FT3F36G.js +59 -0
- package/dist/chunk-5NKWR4FF.js +254 -0
- package/dist/chunk-644FVGIQ.js +194 -0
- package/dist/chunk-6AALH2ED.js +121 -0
- package/dist/chunk-6FAU4IGR.js +63 -0
- package/dist/chunk-6Y3LAUGL.js +35 -0
- package/dist/chunk-7B5C6U2K.js +274 -0
- package/dist/chunk-AAIE4F2U.js +140 -0
- package/dist/chunk-AIP6MR42.js +12 -0
- package/dist/chunk-CCKG5R4Y.js +59 -0
- package/dist/chunk-DRSUEMAK.js +123 -0
- package/dist/chunk-FHG3ILE4.js +2011 -0
- package/dist/chunk-H2AHNI75.js +31 -0
- package/dist/chunk-HD74F6W2.js +460 -0
- package/dist/chunk-HKUWEGUX.js +36 -0
- package/dist/chunk-IBVVGH6X.js +33 -0
- package/dist/chunk-II7VYQEM.js +179 -0
- package/dist/chunk-JMJP4A47.js +204 -0
- package/dist/chunk-JQXOVCOP.js +574 -0
- package/dist/chunk-KE6QJBZG.js +41 -0
- package/dist/chunk-KWX3JHCY.js +85 -0
- package/dist/chunk-MXRWBNIY.js +74 -0
- package/dist/chunk-NPSRD26F.js +149 -0
- package/dist/chunk-QDF7QXBL.js +67 -0
- package/dist/chunk-QM53IQHM.js +209 -0
- package/dist/chunk-RZLYEO4U.js +219 -0
- package/dist/chunk-SGJG3BKD.js +351 -0
- package/dist/chunk-TYIAD6SB.js +74 -0
- package/dist/chunk-UWWSAPDR.js +31 -0
- package/dist/chunk-VM3IWOT5.js +458 -0
- package/dist/chunk-VRXHCR5K.js +42 -0
- package/dist/chunk-WJXC4MVY.js +75 -0
- package/dist/chunk-XDCHRVE3.js +215 -0
- package/dist/chunk-Z4Z5DNW4.js +1196 -0
- package/dist/chunk-ZZOXM6Q4.js +8 -0
- package/dist/ci-ZWRVWNFX.js +9298 -0
- package/dist/cli/contract-output.d.ts +1 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/requested-command.d.ts +8 -0
- package/dist/cli-2JNBJUBB.js +704 -0
- package/dist/commands/build/actors/db-sync.d.ts +2 -0
- package/dist/commands/build/actors/static-checks.d.ts +7 -6
- package/dist/commands/build/actors/validate.d.ts +2 -0
- package/dist/commands/build/contract.d.ts +30 -30
- package/dist/commands/build/machine-dry-run.d.ts +3 -0
- package/dist/commands/build/machine-e2e-meta.d.ts +120 -0
- package/dist/commands/build/machine.d.ts +22 -22
- package/dist/commands/build/types.d.ts +2 -4
- package/dist/commands/check/commands/check.d.ts +8 -3
- package/dist/commands/ci/machine/actors/db/collect-schema-stats.d.ts +9 -6
- package/dist/commands/ci/machine/actors/db/schema-canonical-diff.d.ts +55 -0
- package/dist/commands/ci/machine/actors/db/schema-stats.d.ts +11 -0
- package/dist/commands/ci/machine/actors/db/sync-schema.d.ts +9 -1
- package/dist/commands/ci/machine/contract.d.ts +26 -26
- package/dist/commands/ci/machine/formatters/sections/final-comment.d.ts +1 -5
- package/dist/commands/ci/machine/formatters/sections/format-helpers.d.ts +5 -0
- package/dist/commands/ci/machine/formatters/sections/index.d.ts +2 -2
- package/dist/commands/ci/machine/formatters/sections/schema-matrix.d.ts +3 -3
- package/dist/commands/ci/machine/machine-execution-helpers.d.ts +40 -0
- package/dist/commands/ci/machine/machine-state-helpers.d.ts +14 -0
- package/dist/commands/ci/machine/machine.d.ts +12 -12
- package/dist/commands/ci/machine/types.d.ts +2 -5
- package/dist/commands/ci/utils/ci-summary.d.ts +15 -15
- package/dist/commands/ci/utils/execa-helpers.d.ts +2 -0
- package/dist/commands/db/apply/actors/idempotent-actors.d.ts +34 -0
- package/dist/commands/db/apply/actors/lock-actors.d.ts +16 -0
- package/dist/commands/db/apply/actors/pg-schema-diff-actors.d.ts +31 -0
- package/dist/commands/db/apply/actors/seed-actors.d.ts +11 -0
- package/dist/commands/db/apply/actors/shared.d.ts +9 -0
- package/dist/commands/db/apply/actors.d.ts +16 -65
- package/dist/commands/db/apply/contract.d.ts +8 -1
- package/dist/commands/db/apply/helpers/data-compatibility-checker.d.ts +3 -4
- package/dist/commands/db/apply/helpers/data-integrity-verifier.d.ts +37 -0
- package/dist/commands/db/apply/helpers/fresh-db-handler.d.ts +34 -0
- package/dist/commands/db/apply/helpers/hazard-handler.d.ts +60 -0
- package/dist/commands/db/apply/helpers/idempotent-object-registry.d.ts +96 -0
- package/dist/commands/db/apply/helpers/idempotent-transaction.d.ts +20 -0
- package/dist/commands/db/apply/helpers/index.d.ts +6 -0
- package/dist/commands/db/apply/helpers/partition-validator.d.ts +2 -15
- package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +18 -162
- package/dist/commands/db/apply/helpers/pg-schema-diff-patterns.d.ts +55 -0
- package/dist/commands/db/apply/helpers/pg-schema-diff-version.d.ts +50 -0
- package/dist/commands/db/apply/helpers/plan-validator.d.ts +4 -10
- package/dist/commands/db/apply/helpers/rbac-password-manager.d.ts +34 -0
- package/dist/commands/db/apply/helpers/retry-logic.d.ts +16 -2
- package/dist/commands/db/apply/helpers/shadow-db-manager.d.ts +1 -1
- package/dist/commands/db/apply/helpers/sql-utils.d.ts +26 -0
- package/dist/commands/db/apply/machine.d.ts +52 -1
- package/dist/commands/db/commands/db-apply.d.ts +18 -0
- package/dist/commands/db/commands/db-sync/boundary-classifier.d.ts +21 -0
- package/dist/commands/db/commands/db-sync/error-classifier.d.ts +9 -0
- package/dist/commands/db/commands/db-sync/plan-hazard-analyzer.d.ts +13 -0
- package/dist/commands/db/commands/db-sync/risk-reporter.d.ts +19 -0
- package/dist/commands/db/commands/db-sync/sql-parser.d.ts +25 -0
- package/dist/commands/db/commands/db-sync/types.d.ts +47 -0
- package/dist/commands/db/commands/db-sync.d.ts +14 -0
- package/dist/commands/db/sync/contract.d.ts +6 -2
- package/dist/commands/db/sync/machine.d.ts +2 -1
- package/dist/commands/db/types.d.ts +2 -0
- package/dist/commands/db/utils/boundary-policy/rule-compiler.d.ts +11 -0
- package/dist/commands/db/utils/boundary-policy/types.d.ts +105 -0
- package/dist/commands/db/utils/boundary-policy/validation.d.ts +20 -0
- package/dist/commands/db/utils/boundary-policy-runtime.d.ts +28 -0
- package/dist/commands/db/utils/boundary-policy.d.ts +5 -0
- package/dist/commands/db/utils/idempotent-risk-context.d.ts +29 -0
- package/dist/commands/db/utils/preflight-check.d.ts +14 -0
- package/dist/commands/db/utils/preflight-checks/domain-naming-checks.d.ts +106 -0
- package/dist/commands/db/utils/preflight-checks/orphan-checks.d.ts +36 -0
- package/dist/commands/db/utils/preflight-checks/schema-risk-checks.d.ts +22 -0
- package/dist/commands/db/utils/preflight-checks/supabase-checks.d.ts +55 -0
- package/dist/commands/db/utils/risk-detector-loader.d.ts +8 -0
- package/dist/commands/db/utils/schema-precheck-budget.d.ts +17 -0
- package/dist/commands/db/utils/sql-boundary-parser.d.ts +12 -0
- package/dist/commands/db/utils/sql-file-collector.d.ts +8 -0
- package/dist/commands/db/utils/sql-filename-parser.d.ts +20 -0
- package/dist/commands/db/utils/sql-table-extractor-ast.d.ts +19 -0
- package/dist/commands/db/utils/sql-table-extractor-regex.d.ts +50 -0
- package/dist/commands/db/utils/sql-table-extractor-rls.d.ts +13 -0
- package/dist/commands/db/utils/sql-table-extractor.d.ts +79 -1
- package/dist/commands/db/utils/table-registry-introspection.d.ts +68 -0
- package/dist/commands/db/utils/table-registry.d.ts +3 -38
- package/dist/commands/dev/actors/app-lifecycle.d.ts +18 -0
- package/dist/commands/dev/actors/index.d.ts +12 -2
- package/dist/commands/dev/actors/process-check.d.ts +12 -0
- package/dist/commands/dev/actors/shared.d.ts +15 -0
- package/dist/commands/dev/actors/tables-manifest.d.ts +16 -0
- package/dist/commands/dev/contract.d.ts +3 -3
- package/dist/commands/dev/guards.d.ts +24 -0
- package/dist/commands/dev/machine.d.ts +27 -32
- package/dist/commands/dev/types.d.ts +2 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/env/commands/env-pull/auth.d.ts +13 -0
- package/dist/commands/env/commands/env-pull/dotenv-files.d.ts +14 -0
- package/dist/commands/env/commands/env-pull/security.d.ts +12 -0
- package/dist/commands/env/commands/env-pull/service.d.ts +8 -0
- package/dist/commands/env/commands/env-pull/shared.d.ts +79 -0
- package/dist/commands/env/commands/setup/types.d.ts +1 -1
- package/dist/commands/env/constants/local-supabase.d.ts +2 -0
- package/dist/commands/inject-test-attrs/defaults.d.ts +9 -0
- package/dist/commands/template-check/contract.d.ts +6 -6
- package/dist/commands/template-check/machine.d.ts +2 -2
- package/dist/commands/template-check/types.d.ts +0 -4
- package/dist/commands/template-check/utils/diff-analyzer.d.ts +0 -4
- package/dist/commands/utils/machine-state-logging.d.ts +20 -0
- package/dist/commands/utils/repo-root.d.ts +2 -0
- package/dist/config/env.d.ts +4 -4
- package/dist/config-loader-GT3HAQ7U.js +7 -0
- package/dist/db-XULCILOU.js +14137 -0
- package/dist/dev-5YXNPTCJ.js +992 -0
- package/dist/doctor-MZLOA53G.js +44 -0
- package/dist/env-HMMRSYCI.js +7 -0
- package/dist/env-SS66PZ4B.js +2623 -0
- package/dist/env-files-2UIUYLLR.js +8 -0
- package/dist/error-handler-HEXBRNVV.js +460 -0
- package/dist/hotfix-YA3DGLOM.js +1477 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +48 -42995
- package/dist/init-ZIL6LRFO.js +631 -0
- package/dist/inject-test-attrs-P44BVTQS.js +23 -0
- package/dist/internal/machines/snapshot-helpers.d.ts +6 -0
- package/dist/lib/sql-comment-utils.d.ts +25 -0
- package/dist/license-OB7GVJQ2.js +468 -0
- package/dist/link-VSNDVZZD.js +59 -0
- package/dist/manifest-TMFLESHW.js +19 -0
- package/dist/prepare-32DOVHTE.js +250 -0
- package/dist/risk-detector-4U6ZJ2G5.js +6 -0
- package/dist/risk-detector-core-TK4OAI3N.js +166 -0
- package/dist/risk-detector-plpgsql-HWKS4OLR.js +1886 -0
- package/dist/sdk-XK6HQU7S.js +348 -0
- package/dist/services-7VK5KZTO.js +177 -0
- package/dist/session-SFW5QSXZ.js +142 -0
- package/dist/signal-handler-DO3OANW5.js +6 -0
- package/dist/status-UTKS63AB.js +94 -0
- package/dist/telemetry-P56UBLZ2.js +93 -0
- package/dist/template-check-3P4HZXVY.js +1944 -0
- package/dist/test-V4KQL574.js +650 -0
- package/dist/test-gen-FS4CEY3P.js +88 -0
- package/dist/ui-RJAMCWUI.js +331 -0
- package/dist/upgrade-NUK3ZBCL.js +637 -0
- package/dist/utils/config-loader.d.ts +0 -3
- package/dist/validate-CAAW4Y44.js +54 -0
- package/dist/validators/risk-detector-content-risks.d.ts +13 -0
- package/dist/validators/risk-detector-core.d.ts +25 -0
- package/dist/validators/risk-detector-patterns.d.ts +15 -0
- package/dist/validators/risk-detector-plpgsql-expression-resolver.d.ts +22 -0
- package/dist/validators/risk-detector-plpgsql-parser.d.ts +5 -0
- package/dist/validators/risk-detector-plpgsql-tokenizer.d.ts +18 -0
- package/dist/validators/risk-detector-plpgsql.d.ts +9 -0
- package/dist/validators/risk-detector-text-utils.d.ts +6 -0
- package/dist/validators/risk-detector-types.d.ts +16 -0
- package/dist/validators/risk-detector.d.ts +7 -26
- package/dist/vuln-check-2W7N5TA2.js +121 -0
- package/dist/vuln-checker-IQJ56RUV.js +3223 -0
- package/dist/watch-PNTKZYFB.js +911 -0
- package/dist/workflow-H75N4BXX.js +897 -0
- package/package.json +5 -2
- package/dist/cli/contract-mode.d.ts.map +0 -1
- package/dist/cli/contract-output.d.ts.map +0 -1
- package/dist/cli/early-flags.d.ts.map +0 -1
- package/dist/cli/error-handler.d.ts.map +0 -1
- package/dist/cli/exec.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/json-output.d.ts.map +0 -1
- package/dist/cli/non-interactive.d.ts.map +0 -1
- package/dist/cli/output-format.d.ts.map +0 -1
- package/dist/cli/signal-handler.d.ts.map +0 -1
- package/dist/commands/build/actors/build.d.ts.map +0 -1
- package/dist/commands/build/actors/clean.d.ts.map +0 -1
- package/dist/commands/build/actors/db-sync.d.ts.map +0 -1
- package/dist/commands/build/actors/index.d.ts.map +0 -1
- package/dist/commands/build/actors/manifest.d.ts.map +0 -1
- package/dist/commands/build/actors/setup.d.ts.map +0 -1
- package/dist/commands/build/actors/static-checks.d.ts.map +0 -1
- package/dist/commands/build/actors/validate.d.ts.map +0 -1
- package/dist/commands/build/commands/build.d.ts.map +0 -1
- package/dist/commands/build/contract.d.ts.map +0 -1
- package/dist/commands/build/guards.d.ts.map +0 -1
- package/dist/commands/build/index.d.ts.map +0 -1
- package/dist/commands/build/machine.d.ts.map +0 -1
- package/dist/commands/build/types.d.ts.map +0 -1
- package/dist/commands/cache.d.ts.map +0 -1
- package/dist/commands/check/commands/check.d.ts.map +0 -1
- package/dist/commands/check/index.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-checks.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-layer-content.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-pr-capabilities.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-prod-apply.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-prod-db-operations.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-prod-github.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-prod-types.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-prod-utils.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-prod-workflow.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-resolvers.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-static.d.ts.map +0 -1
- package/dist/commands/ci/commands/ci-supabase-local.d.ts.map +0 -1
- package/dist/commands/ci/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/build/app-build.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/build/app-start.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/build/build-and-playwright.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/build/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/build/playwright-install.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/build/static-checks.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/apply-seeds.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/collect-schema-stats.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/pgtap-install.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/production-preview.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/pull-production.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/reset.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/schema-stats.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/setup-roles.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/db/sync-schema.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/finalize/github.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/finalize/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/finalize/summary.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/setup/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/setup/local.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/setup/pr-common.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/setup/pr-local.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/test/capabilities.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/test/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/actors/test/run-layers.d.ts.map +0 -1
- package/dist/commands/ci/machine/commands/ci-local.d.ts.map +0 -1
- package/dist/commands/ci/machine/commands/ci-pr.d.ts.map +0 -1
- package/dist/commands/ci/machine/commands/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/commands/machine-runner.d.ts.map +0 -1
- package/dist/commands/ci/machine/commands/runtime-env.d.ts.map +0 -1
- package/dist/commands/ci/machine/contract.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/github-comment-types.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/github-comment.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/sections/final-comment.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/sections/format-helpers.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/sections/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/sections/progress-comment.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/sections/schema-matrix.d.ts.map +0 -1
- package/dist/commands/ci/machine/formatters/summary.d.ts.map +0 -1
- package/dist/commands/ci/machine/guards.d.ts.map +0 -1
- package/dist/commands/ci/machine/helpers.d.ts.map +0 -1
- package/dist/commands/ci/machine/index.d.ts.map +0 -1
- package/dist/commands/ci/machine/machine.d.ts.map +0 -1
- package/dist/commands/ci/machine/types.d.ts.map +0 -1
- package/dist/commands/ci/utils/ai-report.d.ts.map +0 -1
- package/dist/commands/ci/utils/app-process.d.ts.map +0 -1
- package/dist/commands/ci/utils/app-runtime.d.ts.map +0 -1
- package/dist/commands/ci/utils/ci-config.d.ts.map +0 -1
- package/dist/commands/ci/utils/ci-env-schema.d.ts.map +0 -1
- package/dist/commands/ci/utils/ci-logging.d.ts.map +0 -1
- package/dist/commands/ci/utils/ci-summary.d.ts.map +0 -1
- package/dist/commands/ci/utils/config-readers.d.ts.map +0 -1
- package/dist/commands/ci/utils/db-url-utils.d.ts.map +0 -1
- package/dist/commands/ci/utils/e2e-auth-setup.d.ts.map +0 -1
- package/dist/commands/ci/utils/env-security.d.ts.map +0 -1
- package/dist/commands/ci/utils/execa-helpers.d.ts.map +0 -1
- package/dist/commands/ci/utils/exit-code-computation.d.ts.map +0 -1
- package/dist/commands/ci/utils/github-api.d.ts.map +0 -1
- package/dist/commands/ci/utils/github.d.ts.map +0 -1
- package/dist/commands/ci/utils/index.d.ts.map +0 -1
- package/dist/commands/ci/utils/pgtap-installer.d.ts.map +0 -1
- package/dist/commands/ci/utils/rls-verification.d.ts.map +0 -1
- package/dist/commands/ci/utils/schema-operations.d.ts.map +0 -1
- package/dist/commands/ci/utils/seed-operations.d.ts.map +0 -1
- package/dist/commands/ci/utils/test-parallel.d.ts.map +0 -1
- package/dist/commands/ci/utils/timestamp-invariants.d.ts.map +0 -1
- package/dist/commands/ci/utils/workflow-idempotency.d.ts.map +0 -1
- package/dist/commands/db/apply/actors.d.ts.map +0 -1
- package/dist/commands/db/apply/contract.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/advisory-lock.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/data-compatibility-checker.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/index.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/partition-acl-cleaner.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/partition-prefilter.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/partition-validator.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/plan-validator.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/retry-logic.d.ts.map +0 -1
- package/dist/commands/db/apply/helpers/shadow-db-manager.d.ts.map +0 -1
- package/dist/commands/db/apply/index.d.ts.map +0 -1
- package/dist/commands/db/apply/machine.d.ts.map +0 -1
- package/dist/commands/db/commands/db-apply.d.ts.map +0 -1
- package/dist/commands/db/commands/db-audit.d.ts.map +0 -1
- package/dist/commands/db/commands/db-backup.d.ts.map +0 -1
- package/dist/commands/db/commands/db-cleanup.d.ts.map +0 -1
- package/dist/commands/db/commands/db-derive-role-passwords.d.ts.map +0 -1
- package/dist/commands/db/commands/db-derive-urls.d.ts.map +0 -1
- package/dist/commands/db/commands/db-diagram.d.ts.map +0 -1
- package/dist/commands/db/commands/db-drizzle.d.ts.map +0 -1
- package/dist/commands/db/commands/db-extension.d.ts.map +0 -1
- package/dist/commands/db/commands/db-generate-password.d.ts.map +0 -1
- package/dist/commands/db/commands/db-lifecycle.d.ts.map +0 -1
- package/dist/commands/db/commands/db-rollback.d.ts.map +0 -1
- package/dist/commands/db/commands/db-schema.d.ts.map +0 -1
- package/dist/commands/db/commands/db-seed-metadata.d.ts.map +0 -1
- package/dist/commands/db/commands/db-seed-verify.d.ts.map +0 -1
- package/dist/commands/db/commands/db-seed.d.ts.map +0 -1
- package/dist/commands/db/commands/db-snapshot.d.ts.map +0 -1
- package/dist/commands/db/commands/db-stack.d.ts.map +0 -1
- package/dist/commands/db/commands/db-stats.d.ts.map +0 -1
- package/dist/commands/db/commands/db-sync.d.ts.map +0 -1
- package/dist/commands/db/commands/db-test.d.ts.map +0 -1
- package/dist/commands/db/constants.d.ts.map +0 -1
- package/dist/commands/db/extension-registry.d.ts.map +0 -1
- package/dist/commands/db/index.d.ts.map +0 -1
- package/dist/commands/db/preflight/actors.d.ts.map +0 -1
- package/dist/commands/db/preflight/contract.d.ts.map +0 -1
- package/dist/commands/db/preflight/index.d.ts.map +0 -1
- package/dist/commands/db/sync/actors.d.ts.map +0 -1
- package/dist/commands/db/sync/contract.d.ts.map +0 -1
- package/dist/commands/db/sync/index.d.ts.map +0 -1
- package/dist/commands/db/sync/machine.d.ts.map +0 -1
- package/dist/commands/db/types.d.ts.map +0 -1
- package/dist/commands/db/utils/db-target.d.ts.map +0 -1
- package/dist/commands/db/utils/db-url-builder.d.ts.map +0 -1
- package/dist/commands/db/utils/error-handlers.d.ts.map +0 -1
- package/dist/commands/db/utils/import-impact-analyzer.d.ts.map +0 -1
- package/dist/commands/db/utils/preflight-check.d.ts.map +0 -1
- package/dist/commands/db/utils/psql.d.ts.map +0 -1
- package/dist/commands/db/utils/schema-detector.d.ts.map +0 -1
- package/dist/commands/db/utils/schema-sync.d.ts.map +0 -1
- package/dist/commands/db/utils/script-runner.d.ts.map +0 -1
- package/dist/commands/db/utils/seed-manager.d.ts.map +0 -1
- package/dist/commands/db/utils/semantic-mapper.d.ts.map +0 -1
- package/dist/commands/db/utils/sql-table-extractor.d.ts.map +0 -1
- package/dist/commands/db/utils/stack-detector.d.ts.map +0 -1
- package/dist/commands/db/utils/table-registry.d.ts.map +0 -1
- package/dist/commands/db/utils/table-source-classifier.d.ts.map +0 -1
- package/dist/commands/dev/actors/index.d.ts.map +0 -1
- package/dist/commands/dev/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev/contract.d.ts.map +0 -1
- package/dist/commands/dev/guards.d.ts.map +0 -1
- package/dist/commands/dev/helpers/stale-process-detector.d.ts.map +0 -1
- package/dist/commands/dev/machine.d.ts.map +0 -1
- package/dist/commands/dev/types.d.ts.map +0 -1
- package/dist/commands/env/commands/env-check.d.ts.map +0 -1
- package/dist/commands/env/commands/env-encrypt.d.ts.map +0 -1
- package/dist/commands/env/commands/env-pull.d.ts.map +0 -1
- package/dist/commands/env/commands/env-setup.d.ts.map +0 -1
- package/dist/commands/env/commands/env-sync.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/action.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/auth.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/file-export.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/github-api.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/helpers.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/index.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/parsers.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/prompts.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/supabase-api.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/types.d.ts.map +0 -1
- package/dist/commands/env/commands/setup/vercel-api.d.ts.map +0 -1
- package/dist/commands/env/constants/local-supabase.d.ts.map +0 -1
- package/dist/commands/env/index.d.ts.map +0 -1
- package/dist/commands/hotfix/actors.d.ts.map +0 -1
- package/dist/commands/hotfix/commands/hotfix-complete.d.ts.map +0 -1
- package/dist/commands/hotfix/commands/hotfix-create.d.ts.map +0 -1
- package/dist/commands/hotfix/commands/hotfix-deploy.d.ts.map +0 -1
- package/dist/commands/hotfix/commands/hotfix-status.d.ts.map +0 -1
- package/dist/commands/hotfix/contract.d.ts.map +0 -1
- package/dist/commands/hotfix/index.d.ts.map +0 -1
- package/dist/commands/hotfix/machine.d.ts.map +0 -1
- package/dist/commands/hotfix/metadata.d.ts.map +0 -1
- package/dist/commands/hotfix/utils/hotfix-machine-helper.d.ts.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/action.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/commands/inject-test-attrs.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/contract.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/detection-diagnostics.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/formatter.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/index.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/manifest-generator.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/processor-utils.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/processor.d.ts.map +0 -1
- package/dist/commands/inject-test-attrs/types.d.ts.map +0 -1
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/manifest/index.d.ts.map +0 -1
- package/dist/commands/prepare/commands/prepare.d.ts.map +0 -1
- package/dist/commands/prepare/index.d.ts.map +0 -1
- package/dist/commands/sdk/commands/publish.d.ts.map +0 -1
- package/dist/commands/sdk/index.d.ts.map +0 -1
- package/dist/commands/services/index.d.ts.map +0 -1
- package/dist/commands/session/index.d.ts.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/telemetry.d.ts.map +0 -1
- package/dist/commands/template-check/actors/compare.d.ts.map +0 -1
- package/dist/commands/template-check/actors/discover.d.ts.map +0 -1
- package/dist/commands/template-check/actors/index.d.ts.map +0 -1
- package/dist/commands/template-check/actors/report.d.ts.map +0 -1
- package/dist/commands/template-check/commands/template-check.d.ts.map +0 -1
- package/dist/commands/template-check/config.d.ts.map +0 -1
- package/dist/commands/template-check/contract.d.ts.map +0 -1
- package/dist/commands/template-check/index.d.ts.map +0 -1
- package/dist/commands/template-check/machine.d.ts.map +0 -1
- package/dist/commands/template-check/types.d.ts.map +0 -1
- package/dist/commands/template-check/utils/diff-analyzer.d.ts.map +0 -1
- package/dist/commands/template-check/utils/normalizer.d.ts.map +0 -1
- package/dist/commands/template-check/utils/path-mapping.d.ts.map +0 -1
- package/dist/commands/test/commands/test-db.d.ts.map +0 -1
- package/dist/commands/test/commands/test-e2e.d.ts.map +0 -1
- package/dist/commands/test/commands/test-fast.d.ts.map +0 -1
- package/dist/commands/test/commands/test-integration.d.ts.map +0 -1
- package/dist/commands/test/commands/test-layer.d.ts.map +0 -1
- package/dist/commands/test/commands/test-owasp-generate.d.ts.map +0 -1
- package/dist/commands/test/commands/test-service.d.ts.map +0 -1
- package/dist/commands/test/commands/test-static.d.ts.map +0 -1
- package/dist/commands/test/commands/test.d.ts.map +0 -1
- package/dist/commands/test/index.d.ts.map +0 -1
- package/dist/commands/test-gen.d.ts.map +0 -1
- package/dist/commands/ui.d.ts.map +0 -1
- package/dist/commands/upgrade.d.ts.map +0 -1
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/vuln-check.d.ts.map +0 -1
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/workflow/commands/deploy-production.d.ts.map +0 -1
- package/dist/commands/workflow/commands/final-status.d.ts.map +0 -1
- package/dist/commands/workflow/commands/log.d.ts.map +0 -1
- package/dist/commands/workflow/commands/notify.d.ts.map +0 -1
- package/dist/commands/workflow/commands/paths.d.ts.map +0 -1
- package/dist/commands/workflow/commands/sync.d.ts.map +0 -1
- package/dist/commands/workflow/commands/validate.d.ts.map +0 -1
- package/dist/commands/workflow/commands/verify-credentials.d.ts.map +0 -1
- package/dist/commands/workflow/index.d.ts.map +0 -1
- package/dist/commands/workflow/types.d.ts.map +0 -1
- package/dist/config/env-files.d.ts.map +0 -1
- package/dist/config/env.d.ts.map +0 -1
- package/dist/constants/versions.d.ts.map +0 -1
- package/dist/contracts/envelope.d.ts.map +0 -1
- package/dist/errors/catalog.d.ts.map +0 -1
- package/dist/errors/exit-codes.d.ts.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/incremental/affected-tests.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/internal/machines/index.d.ts.map +0 -1
- package/dist/internal/machines/machine-runner.d.ts.map +0 -1
- package/dist/internal/machines/snapshot-helpers.d.ts.map +0 -1
- package/dist/internal/machines/types.d.ts.map +0 -1
- package/dist/internal/vuln-checker/analyzers/dependency-analyzer.d.ts.map +0 -1
- package/dist/internal/vuln-checker/analyzers/rls-analyzer.d.ts.map +0 -1
- package/dist/internal/vuln-checker/analyzers/secret-analyzer.d.ts.map +0 -1
- package/dist/internal/vuln-checker/analyzers/typescript-analyzer.d.ts.map +0 -1
- package/dist/internal/vuln-checker/config/loader.d.ts.map +0 -1
- package/dist/internal/vuln-checker/constants.d.ts.map +0 -1
- package/dist/internal/vuln-checker/ignore/matcher.d.ts.map +0 -1
- package/dist/internal/vuln-checker/index.d.ts.map +0 -1
- package/dist/internal/vuln-checker/reporters/console-reporter.d.ts.map +0 -1
- package/dist/internal/vuln-checker/reporters/json-reporter.d.ts.map +0 -1
- package/dist/internal/vuln-checker/reporters/markdown-reporter.d.ts.map +0 -1
- package/dist/internal/vuln-checker/reporters/sarif-reporter.d.ts.map +0 -1
- package/dist/internal/vuln-checker/security/path-validation.d.ts.map +0 -1
- package/dist/internal/vuln-checker/types.d.ts.map +0 -1
- package/dist/notifiers/desktop-notifier.d.ts.map +0 -1
- package/dist/ui/components/db-panel.d.ts.map +0 -1
- package/dist/ui/components/status-bar.d.ts.map +0 -1
- package/dist/ui/components/test-panel.d.ts.map +0 -1
- package/dist/ui/dashboard.d.ts.map +0 -1
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/utils/config-loader.d.ts.map +0 -1
- package/dist/utils/config-updater.d.ts.map +0 -1
- package/dist/utils/diagnostics.d.ts.map +0 -1
- package/dist/utils/dotenvx.d.ts.map +0 -1
- package/dist/utils/env-local-bridge.d.ts.map +0 -1
- package/dist/utils/execution-plan.d.ts.map +0 -1
- package/dist/utils/github-output-security.d.ts.map +0 -1
- package/dist/utils/help-system.d.ts.map +0 -1
- package/dist/utils/license/admin-auth.d.ts.map +0 -1
- package/dist/utils/license/allowlist-checker.d.ts.map +0 -1
- package/dist/utils/license/ci-detector.d.ts.map +0 -1
- package/dist/utils/license/index.d.ts.map +0 -1
- package/dist/utils/license/owner-resolver.d.ts.map +0 -1
- package/dist/utils/license/types.d.ts.map +0 -1
- package/dist/utils/license/validate-owner.d.ts.map +0 -1
- package/dist/utils/path-security.d.ts.map +0 -1
- package/dist/utils/port-allocator.d.ts.map +0 -1
- package/dist/utils/secure-exec.d.ts.map +0 -1
- package/dist/utils/template-fetcher.d.ts.map +0 -1
- package/dist/utils/type-guards.d.ts.map +0 -1
- package/dist/utils/vercel-project.d.ts.map +0 -1
- package/dist/utils/workspace-detector.d.ts.map +0 -1
- package/dist/validators/risk-detector.d.ts.map +0 -1
- package/dist/validators/schema-validator.d.ts.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/watchers/schema-watcher.d.ts.map +0 -1
- package/dist/watchers/test-watcher.d.ts.map +0 -1
|
@@ -0,0 +1,3223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { CLI_VERSION } from './chunk-AIP6MR42.js';
|
|
4
|
+
import { init_esm_shims } from './chunk-VRXHCR5K.js';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path2 from 'path';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { Project, Node, SyntaxKind } from 'ts-morph';
|
|
11
|
+
import yaml from 'js-yaml';
|
|
12
|
+
import 'fs';
|
|
13
|
+
import { minimatch } from 'minimatch';
|
|
14
|
+
|
|
15
|
+
createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
// src/internal/vuln-checker/index.ts
|
|
18
|
+
init_esm_shims();
|
|
19
|
+
|
|
20
|
+
// src/internal/vuln-checker/analyzers/dependency-analyzer.ts
|
|
21
|
+
init_esm_shims();
|
|
22
|
+
|
|
23
|
+
// src/internal/vuln-checker/constants.ts
|
|
24
|
+
init_esm_shims();
|
|
25
|
+
var OWASP_2021 = {
|
|
26
|
+
/** A01:2021 - Broken Access Control */
|
|
27
|
+
BROKEN_ACCESS_CONTROL: "A01:2021",
|
|
28
|
+
/** A02:2021 - Cryptographic Failures */
|
|
29
|
+
CRYPTO_FAILURES: "A02:2021",
|
|
30
|
+
/** A03:2021 - Injection */
|
|
31
|
+
INJECTION: "A03:2021",
|
|
32
|
+
/** A06:2021 - Vulnerable and Outdated Components */
|
|
33
|
+
VULNERABLE_COMPONENTS: "A06:2021",
|
|
34
|
+
/** A07:2021 - Identification and Authentication Failures */
|
|
35
|
+
AUTH_FAILURES: "A07:2021",
|
|
36
|
+
/** A08:2021 - Software and Data Integrity Failures */
|
|
37
|
+
INTEGRITY_FAILURES: "A08:2021",
|
|
38
|
+
/** A09:2021 - Security Logging and Monitoring Failures */
|
|
39
|
+
LOGGING_FAILURES: "A09:2021"};
|
|
40
|
+
var CWE = {
|
|
41
|
+
/** CWE-78: OS Command Injection */
|
|
42
|
+
COMMAND_INJECTION: "CWE-78",
|
|
43
|
+
/** CWE-79: Cross-site Scripting (XSS) */
|
|
44
|
+
XSS: "CWE-79",
|
|
45
|
+
/** CWE-89: SQL Injection */
|
|
46
|
+
SQL_INJECTION: "CWE-89",
|
|
47
|
+
/** CWE-95: Eval Injection */
|
|
48
|
+
EVAL_INJECTION: "CWE-95",
|
|
49
|
+
/** CWE-284: Improper Access Control */
|
|
50
|
+
IMPROPER_ACCESS_CONTROL: "CWE-284",
|
|
51
|
+
/** CWE-306: Missing Authentication */
|
|
52
|
+
MISSING_AUTH: "CWE-306",
|
|
53
|
+
/** CWE-321: Use of Hard-coded Cryptographic Key */
|
|
54
|
+
HARDCODED_CRYPTO_KEY: "CWE-321",
|
|
55
|
+
/** CWE-330: Use of Insufficiently Random Values */
|
|
56
|
+
WEAK_RANDOM: "CWE-330",
|
|
57
|
+
/** CWE-532: Insertion of Sensitive Information into Log File */
|
|
58
|
+
SENSITIVE_LOG: "CWE-532",
|
|
59
|
+
/** CWE-798: Use of Hard-coded Credentials */
|
|
60
|
+
HARDCODED_CREDENTIALS: "CWE-798",
|
|
61
|
+
/** CWE-1104: Use of Unmaintained Third Party Components */
|
|
62
|
+
UNMAINTAINED_COMPONENTS: "CWE-1104",
|
|
63
|
+
/** CWE-494: Download of Code Without Integrity Check */
|
|
64
|
+
UNTRUSTED_DOWNLOAD: "CWE-494"
|
|
65
|
+
};
|
|
66
|
+
var SEVERITY_SCORE = {
|
|
67
|
+
critical: 10,
|
|
68
|
+
high: 8,
|
|
69
|
+
medium: 5,
|
|
70
|
+
low: 3,
|
|
71
|
+
info: 1
|
|
72
|
+
};
|
|
73
|
+
var TOOL = {
|
|
74
|
+
NAME: "runa-vuln-checker",
|
|
75
|
+
VERSION: CLI_VERSION,
|
|
76
|
+
REPO_URL: "https://github.com/r06-dev/runa"
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/internal/vuln-checker/analyzers/dependency-analyzer.ts
|
|
80
|
+
var execAsync = promisify(exec);
|
|
81
|
+
function levenshteinDistance(a, b) {
|
|
82
|
+
const matrix = [];
|
|
83
|
+
for (let i = 0; i <= a.length; i++) {
|
|
84
|
+
matrix[i] = [i];
|
|
85
|
+
}
|
|
86
|
+
for (let j = 0; j <= b.length; j++) {
|
|
87
|
+
matrix[0][j] = j;
|
|
88
|
+
}
|
|
89
|
+
for (let i = 1; i <= a.length; i++) {
|
|
90
|
+
for (let j = 1; j <= b.length; j++) {
|
|
91
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
92
|
+
matrix[i][j] = Math.min(
|
|
93
|
+
matrix[i - 1][j] + 1,
|
|
94
|
+
// deletion
|
|
95
|
+
matrix[i][j - 1] + 1,
|
|
96
|
+
// insertion
|
|
97
|
+
matrix[i - 1][j - 1] + cost
|
|
98
|
+
// substitution
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return matrix[a.length][b.length];
|
|
103
|
+
}
|
|
104
|
+
var POPULAR_PACKAGES = [
|
|
105
|
+
// Web frameworks
|
|
106
|
+
"express",
|
|
107
|
+
"fastify",
|
|
108
|
+
"koa",
|
|
109
|
+
"hono",
|
|
110
|
+
"next",
|
|
111
|
+
"nuxt",
|
|
112
|
+
// React ecosystem
|
|
113
|
+
"react",
|
|
114
|
+
"react-dom",
|
|
115
|
+
"react-router",
|
|
116
|
+
"redux",
|
|
117
|
+
"zustand",
|
|
118
|
+
// Vue ecosystem
|
|
119
|
+
"vue",
|
|
120
|
+
"vuex",
|
|
121
|
+
"pinia",
|
|
122
|
+
// Utilities
|
|
123
|
+
"lodash",
|
|
124
|
+
"underscore",
|
|
125
|
+
"ramda",
|
|
126
|
+
"axios",
|
|
127
|
+
"node-fetch",
|
|
128
|
+
"got",
|
|
129
|
+
"moment",
|
|
130
|
+
"dayjs",
|
|
131
|
+
"date-fns",
|
|
132
|
+
// Build tools
|
|
133
|
+
"webpack",
|
|
134
|
+
"vite",
|
|
135
|
+
"esbuild",
|
|
136
|
+
"rollup",
|
|
137
|
+
"parcel",
|
|
138
|
+
"turbo",
|
|
139
|
+
// Testing
|
|
140
|
+
"jest",
|
|
141
|
+
"mocha",
|
|
142
|
+
"vitest",
|
|
143
|
+
"playwright",
|
|
144
|
+
"cypress",
|
|
145
|
+
// Type/schema
|
|
146
|
+
"typescript",
|
|
147
|
+
"zod",
|
|
148
|
+
"yup",
|
|
149
|
+
"joi",
|
|
150
|
+
// Database
|
|
151
|
+
"mongoose",
|
|
152
|
+
"sequelize",
|
|
153
|
+
"prisma",
|
|
154
|
+
"drizzle-orm",
|
|
155
|
+
"knex",
|
|
156
|
+
// CLI
|
|
157
|
+
"commander",
|
|
158
|
+
"yargs",
|
|
159
|
+
"inquirer",
|
|
160
|
+
"chalk",
|
|
161
|
+
"ora",
|
|
162
|
+
// Config
|
|
163
|
+
"dotenv",
|
|
164
|
+
"config",
|
|
165
|
+
"convict",
|
|
166
|
+
// Security
|
|
167
|
+
"jsonwebtoken",
|
|
168
|
+
"bcrypt",
|
|
169
|
+
"helmet",
|
|
170
|
+
"cors",
|
|
171
|
+
// Other popular
|
|
172
|
+
"uuid",
|
|
173
|
+
"nanoid",
|
|
174
|
+
"debug",
|
|
175
|
+
"winston",
|
|
176
|
+
"pino",
|
|
177
|
+
"socket.io",
|
|
178
|
+
"graphql",
|
|
179
|
+
"apollo-server"
|
|
180
|
+
];
|
|
181
|
+
var KNOWN_TYPOSQUATS = {
|
|
182
|
+
// lodash variants
|
|
183
|
+
lodsh: "lodash",
|
|
184
|
+
loadash: "lodash",
|
|
185
|
+
lodahs: "lodash",
|
|
186
|
+
"lodash-js": "lodash",
|
|
187
|
+
// express variants
|
|
188
|
+
expres: "express",
|
|
189
|
+
expresss: "express",
|
|
190
|
+
exress: "express",
|
|
191
|
+
// react variants
|
|
192
|
+
reakt: "react",
|
|
193
|
+
reactt: "react",
|
|
194
|
+
raect: "react",
|
|
195
|
+
// axios variants
|
|
196
|
+
axois: "axios",
|
|
197
|
+
axioss: "axios",
|
|
198
|
+
axio: "axios",
|
|
199
|
+
// moment variants
|
|
200
|
+
momnet: "moment",
|
|
201
|
+
momet: "moment",
|
|
202
|
+
momment: "moment",
|
|
203
|
+
// commander variants
|
|
204
|
+
comander: "commander",
|
|
205
|
+
comandr: "commander",
|
|
206
|
+
// mongoose variants
|
|
207
|
+
mongose: "mongoose",
|
|
208
|
+
mongoos: "mongoose",
|
|
209
|
+
// dotenv variants
|
|
210
|
+
dotenve: "dotenv",
|
|
211
|
+
// webpack variants
|
|
212
|
+
webpck: "webpack",
|
|
213
|
+
weback: "webpack",
|
|
214
|
+
// typescript variants
|
|
215
|
+
typescrip: "typescript",
|
|
216
|
+
typescrit: "typescript",
|
|
217
|
+
// npm variants
|
|
218
|
+
npmm: "npm"
|
|
219
|
+
};
|
|
220
|
+
function mapSeverity(npmSeverity) {
|
|
221
|
+
switch (npmSeverity) {
|
|
222
|
+
case "critical":
|
|
223
|
+
return "critical";
|
|
224
|
+
case "high":
|
|
225
|
+
return "high";
|
|
226
|
+
case "moderate":
|
|
227
|
+
return "medium";
|
|
228
|
+
case "low":
|
|
229
|
+
return "low";
|
|
230
|
+
default:
|
|
231
|
+
return "info";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function extractVulnDetails(pkgName, vuln) {
|
|
235
|
+
let title = `Vulnerable dependency: ${pkgName}`;
|
|
236
|
+
let description = `Package ${pkgName} has known vulnerabilities.`;
|
|
237
|
+
let cveUrl;
|
|
238
|
+
if (Array.isArray(vuln.via)) {
|
|
239
|
+
for (const via of vuln.via) {
|
|
240
|
+
if (typeof via === "object" && "title" in via) {
|
|
241
|
+
title = via.title;
|
|
242
|
+
description = `${via.title} in ${pkgName}`;
|
|
243
|
+
cveUrl = via.url;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { title, description, cveUrl };
|
|
249
|
+
}
|
|
250
|
+
function buildFixDescription(vuln) {
|
|
251
|
+
if (!vuln.fixAvailable || typeof vuln.fixAvailable !== "object") {
|
|
252
|
+
return "Update to a patched version";
|
|
253
|
+
}
|
|
254
|
+
const fix = vuln.fixAvailable;
|
|
255
|
+
let desc = `Update to ${fix.name}@${fix.version}`;
|
|
256
|
+
if (fix.isSemVerMajor) {
|
|
257
|
+
desc += " (major version update required)";
|
|
258
|
+
}
|
|
259
|
+
return desc;
|
|
260
|
+
}
|
|
261
|
+
function createVulnFinding(pkgName, vuln, packageJsonPath) {
|
|
262
|
+
const severity = mapSeverity(vuln.severity);
|
|
263
|
+
const { title, description, cveUrl } = extractVulnDetails(pkgName, vuln);
|
|
264
|
+
const depType = vuln.isDirect ? " (direct dependency)" : " (transitive dependency)";
|
|
265
|
+
return {
|
|
266
|
+
ruleId: "dependency/cve",
|
|
267
|
+
severity,
|
|
268
|
+
title,
|
|
269
|
+
description: `${description}${depType}`,
|
|
270
|
+
location: { file: packageJsonPath, line: 1, column: 1 },
|
|
271
|
+
fix: { description: buildFixDescription(vuln) },
|
|
272
|
+
metadata: {
|
|
273
|
+
package: pkgName,
|
|
274
|
+
range: vuln.range,
|
|
275
|
+
isDirect: vuln.isDirect,
|
|
276
|
+
url: cveUrl
|
|
277
|
+
},
|
|
278
|
+
cweId: CWE.UNMAINTAINED_COMPONENTS,
|
|
279
|
+
owaspCategory: OWASP_2021.VULNERABLE_COMPONENTS,
|
|
280
|
+
confidence: 0.95
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function createKnownTyposquatFinding(depName, legitimate, packageJsonPath) {
|
|
284
|
+
return {
|
|
285
|
+
ruleId: "dependency/typosquat",
|
|
286
|
+
severity: "high",
|
|
287
|
+
title: "Known Typosquatting Package",
|
|
288
|
+
description: `Package "${depName}" is a known typosquatting attempt of "${legitimate}"`,
|
|
289
|
+
location: { file: packageJsonPath, line: 1, column: 1 },
|
|
290
|
+
fix: { description: `Replace with the legitimate package "${legitimate}"` },
|
|
291
|
+
metadata: { suspicious: depName, legitimate },
|
|
292
|
+
cweId: CWE.UNTRUSTED_DOWNLOAD,
|
|
293
|
+
owaspCategory: OWASP_2021.INTEGRITY_FAILURES,
|
|
294
|
+
confidence: 0.95
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function createSimilarityTyposquatFinding(depName, popular, distance, packageJsonPath) {
|
|
298
|
+
return {
|
|
299
|
+
ruleId: "dependency/typosquat",
|
|
300
|
+
severity: "medium",
|
|
301
|
+
title: "Potential Typosquatting Package",
|
|
302
|
+
description: `Package "${depName}" is very similar to popular package "${popular}" (distance: ${distance})`,
|
|
303
|
+
location: { file: packageJsonPath, line: 1, column: 1 },
|
|
304
|
+
fix: { description: `Verify this is the correct package. Did you mean "${popular}"?` },
|
|
305
|
+
metadata: { suspicious: depName, legitimate: popular, levenshteinDistance: distance },
|
|
306
|
+
cweId: CWE.UNTRUSTED_DOWNLOAD,
|
|
307
|
+
owaspCategory: OWASP_2021.INTEGRITY_FAILURES,
|
|
308
|
+
confidence: 0.6
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function isLikelyPackageVariant(name, baseName) {
|
|
312
|
+
return name.startsWith(`${baseName}-`) || name.endsWith(`-${baseName}`) || name.includes(`${baseName}-`) || name.includes(`-${baseName}`);
|
|
313
|
+
}
|
|
314
|
+
function getMaxTyposquatDistance(length) {
|
|
315
|
+
if (length <= 4) return 1;
|
|
316
|
+
if (length <= 8) return 2;
|
|
317
|
+
return 3;
|
|
318
|
+
}
|
|
319
|
+
function checkTyposquat(depName, packageJsonPath) {
|
|
320
|
+
const nameWithoutScope = depName.replace(/^@[^/]+\//, "").toLowerCase();
|
|
321
|
+
const knownLegitimate = KNOWN_TYPOSQUATS[nameWithoutScope];
|
|
322
|
+
if (knownLegitimate) {
|
|
323
|
+
return createKnownTyposquatFinding(depName, knownLegitimate, packageJsonPath);
|
|
324
|
+
}
|
|
325
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
326
|
+
if (nameWithoutScope === popular.toLowerCase()) continue;
|
|
327
|
+
const maxDistance = getMaxTyposquatDistance(popular.length);
|
|
328
|
+
const distance = levenshteinDistance(nameWithoutScope, popular.toLowerCase());
|
|
329
|
+
if (distance > 0 && distance <= maxDistance && !isLikelyPackageVariant(nameWithoutScope, popular)) {
|
|
330
|
+
return createSimilarityTyposquatFinding(depName, popular, distance, packageJsonPath);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
var SUSPICIOUS_NAME_PATTERNS = [
|
|
336
|
+
// Single character variants (e.g., "lodash-a", "react-x")
|
|
337
|
+
{
|
|
338
|
+
pattern: /^[^@].*-[a-zA-Z]$/,
|
|
339
|
+
description: "Single letter suffix",
|
|
340
|
+
confidence: 0.5
|
|
341
|
+
},
|
|
342
|
+
// Random number suffix (e.g., "express-123", "react-42")
|
|
343
|
+
// But not version-like (e.g., "es6-promise" is OK)
|
|
344
|
+
{
|
|
345
|
+
pattern: /-[0-9]{2,}$/,
|
|
346
|
+
description: "Random number suffix",
|
|
347
|
+
confidence: 0.4
|
|
348
|
+
},
|
|
349
|
+
// Single letter prefix (e.g., "a-lodash", "x-react")
|
|
350
|
+
{
|
|
351
|
+
pattern: /^[a-zA-Z]-[a-zA-Z]{2,}/,
|
|
352
|
+
description: "Single letter prefix",
|
|
353
|
+
confidence: 0.5
|
|
354
|
+
},
|
|
355
|
+
// Homoglyph characters (common attack vector)
|
|
356
|
+
{
|
|
357
|
+
pattern: /[а-яА-Я]/,
|
|
358
|
+
// Cyrillic characters that look like Latin
|
|
359
|
+
description: "Contains non-ASCII characters that may be homoglyphs",
|
|
360
|
+
confidence: 0.9
|
|
361
|
+
},
|
|
362
|
+
// Very short scoped packages (e.g., "@x/y")
|
|
363
|
+
{
|
|
364
|
+
pattern: /^@[a-zA-Z]\/[a-zA-Z]$/,
|
|
365
|
+
description: "Very short scoped package name",
|
|
366
|
+
confidence: 0.6
|
|
367
|
+
},
|
|
368
|
+
// Package names that look like they're trying to impersonate orgs
|
|
369
|
+
{
|
|
370
|
+
pattern: /^(?:react|vue|angular|node|npm|yarn|pnpm|vercel|next|nuxt)-(?:official|org|team|core|dev)$/i,
|
|
371
|
+
description: "Potentially impersonating official package",
|
|
372
|
+
confidence: 0.7
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
function checkSuspiciousName(depName, packageJsonPath) {
|
|
376
|
+
for (const { pattern, description, confidence } of SUSPICIOUS_NAME_PATTERNS) {
|
|
377
|
+
if (pattern.test(depName)) {
|
|
378
|
+
return {
|
|
379
|
+
ruleId: "dependency/suspicious",
|
|
380
|
+
severity: confidence >= 0.7 ? "high" : "medium",
|
|
381
|
+
title: "Suspicious Package Name",
|
|
382
|
+
description: `Package "${depName}" has a suspicious naming pattern: ${description}`,
|
|
383
|
+
location: { file: packageJsonPath, line: 1, column: 1 },
|
|
384
|
+
fix: { description: "Verify this package is legitimate before using" },
|
|
385
|
+
metadata: { package: depName, reason: description },
|
|
386
|
+
cweId: CWE.UNTRUSTED_DOWNLOAD,
|
|
387
|
+
owaspCategory: OWASP_2021.INTEGRITY_FAILURES,
|
|
388
|
+
confidence
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
var DependencyAnalyzer = class {
|
|
395
|
+
name = "DependencyAnalyzer";
|
|
396
|
+
categories = ["dependency"];
|
|
397
|
+
async analyze(options) {
|
|
398
|
+
const packageJsonPath = path2.join(options.rootDir, "package.json");
|
|
399
|
+
try {
|
|
400
|
+
await fs.access(packageJsonPath);
|
|
401
|
+
} catch {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const auditFindings = await this.runNpmAudit(options.rootDir, packageJsonPath);
|
|
405
|
+
const typosquatFindings = await this.checkTyposquatting(packageJsonPath);
|
|
406
|
+
return [...auditFindings, ...typosquatFindings];
|
|
407
|
+
}
|
|
408
|
+
async runNpmAudit(rootDir, packageJsonPath) {
|
|
409
|
+
try {
|
|
410
|
+
const stdout = await this.executeNpmAudit(rootDir);
|
|
411
|
+
const auditResult = JSON.parse(stdout);
|
|
412
|
+
return Object.entries(auditResult.vulnerabilities).map(
|
|
413
|
+
([pkgName, vuln]) => createVulnFinding(pkgName, vuln, packageJsonPath)
|
|
414
|
+
);
|
|
415
|
+
} catch {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async executeNpmAudit(rootDir) {
|
|
420
|
+
try {
|
|
421
|
+
const { stdout } = await execAsync("npm audit --json", { cwd: rootDir });
|
|
422
|
+
return stdout;
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const execError = error;
|
|
425
|
+
if (execError.stdout) {
|
|
426
|
+
return execError.stdout;
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async checkTyposquatting(packageJsonPath) {
|
|
432
|
+
try {
|
|
433
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
434
|
+
const packageJson = JSON.parse(content);
|
|
435
|
+
const allDeps = {
|
|
436
|
+
...packageJson.dependencies || {},
|
|
437
|
+
...packageJson.devDependencies || {}
|
|
438
|
+
};
|
|
439
|
+
const findings = [];
|
|
440
|
+
for (const depName of Object.keys(allDeps)) {
|
|
441
|
+
const typosquatFinding = checkTyposquat(depName, packageJsonPath);
|
|
442
|
+
if (typosquatFinding) findings.push(typosquatFinding);
|
|
443
|
+
const suspiciousFinding = checkSuspiciousName(depName, packageJsonPath);
|
|
444
|
+
if (suspiciousFinding) findings.push(suspiciousFinding);
|
|
445
|
+
}
|
|
446
|
+
return findings;
|
|
447
|
+
} catch {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/internal/vuln-checker/analyzers/rls-analyzer.ts
|
|
454
|
+
init_esm_shims();
|
|
455
|
+
var TENANT_COLUMN_PATTERNS = [
|
|
456
|
+
"tenant_id",
|
|
457
|
+
"client_id",
|
|
458
|
+
"org_id",
|
|
459
|
+
"organization_id",
|
|
460
|
+
"scope_id",
|
|
461
|
+
"account_id",
|
|
462
|
+
"workspace_id",
|
|
463
|
+
"team_id",
|
|
464
|
+
"company_id"
|
|
465
|
+
];
|
|
466
|
+
var TENANT_ISOLATION_PATTERNS = [
|
|
467
|
+
// Direct comparisons with tenant columns (various formats)
|
|
468
|
+
// Handles: tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
|
|
469
|
+
// Handles: client_id = auth.jwt()->>'client_id'
|
|
470
|
+
// Handles: scope_id = current_setting('app.scope_id')
|
|
471
|
+
/(?:tenant_id|client_id|org_id|organization_id|scope_id|account_id|workspace_id|team_id|company_id)\s*=\s*.*(?:jwt|auth|current_setting)/i,
|
|
472
|
+
// Schema-qualified column comparisons
|
|
473
|
+
// Handles: accounts.clients.tenant_id = ...
|
|
474
|
+
/[\w]+\.[\w]+\.(?:tenant_id|client_id|org_id|scope_id)\s*=\s*.*(?:jwt|auth)/i,
|
|
475
|
+
// Helper function patterns (common naming conventions)
|
|
476
|
+
// Handles: get_current_tenant(), get_my_tenant_id(), current_tenant_id(), etc.
|
|
477
|
+
/(?:get_)?(?:current_|my_)?(?:tenant|client|org|scope|account)(?:_id)?\s*\(\)/i,
|
|
478
|
+
// Supabase auth functions
|
|
479
|
+
/auth\.uid\s*\(\)/i,
|
|
480
|
+
/auth\.jwt\s*\(\)/i,
|
|
481
|
+
/auth\.role\s*\(\)/i,
|
|
482
|
+
// IN clause with tenant subquery
|
|
483
|
+
// Handles: tenant_id IN (SELECT id FROM get_user_tenants())
|
|
484
|
+
/(?:tenant_id|client_id|org_id|scope_id)\s+IN\s*\(/i,
|
|
485
|
+
// EXISTS with tenant check
|
|
486
|
+
// Handles: EXISTS (SELECT 1 FROM ... WHERE tenant_id = ...)
|
|
487
|
+
/EXISTS\s*\(\s*SELECT.*(?:tenant_id|client_id|scope_id)/i,
|
|
488
|
+
// CTE reference patterns
|
|
489
|
+
// Handles: WITH tenant AS (...) ... WHERE tenant_id = tenant.id
|
|
490
|
+
/WITH\s+(?:tenant|current_tenant|user_tenant)/i,
|
|
491
|
+
// PostgreSQL current_setting for tenant context
|
|
492
|
+
// Handles: current_setting('app.tenant_id', true)
|
|
493
|
+
/current_setting\s*\(\s*['"](?:app\.|rls\.)?(?:tenant|client|org|scope)/i,
|
|
494
|
+
// Request header / session patterns (some frameworks)
|
|
495
|
+
// Handles: request.header('x-tenant-id')
|
|
496
|
+
/request\.(?:header|cookie|session)\s*\(\s*['"].*(?:tenant|client|org)/i,
|
|
497
|
+
// Type-casted JWT claim extraction
|
|
498
|
+
// Handles: ((auth.jwt() ->> 'tenant_id')::uuid)
|
|
499
|
+
/\(\s*auth\.jwt\s*\(\)\s*->>?\s*['"][^'"]+['"]\s*\)\s*::/i
|
|
500
|
+
];
|
|
501
|
+
var SYSTEM_TABLE_PATTERNS = ["_", "schema_migrations", "drizzle_migrations"];
|
|
502
|
+
var SUPABASE_STORAGE_TABLES = ["storage.objects", "storage.buckets"];
|
|
503
|
+
function isStorageTable(tableName) {
|
|
504
|
+
return SUPABASE_STORAGE_TABLES.includes(tableName.toLowerCase());
|
|
505
|
+
}
|
|
506
|
+
function isPgTableCall(callExpr) {
|
|
507
|
+
const expression = callExpr.getExpression();
|
|
508
|
+
if (Node.isIdentifier(expression)) {
|
|
509
|
+
const name = expression.getText();
|
|
510
|
+
return name === "pgTable" || name === "table";
|
|
511
|
+
}
|
|
512
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
513
|
+
const propName = expression.getName();
|
|
514
|
+
return propName === "table";
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
function extractTableName(callExpr) {
|
|
519
|
+
const args = callExpr.getArguments();
|
|
520
|
+
if (args.length === 0) return null;
|
|
521
|
+
const firstArg = args[0];
|
|
522
|
+
if (Node.isStringLiteral(firstArg)) {
|
|
523
|
+
return firstArg.getLiteralValue();
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
function resolveSchemaFromDeclaration(sourceFile, schemaVarName) {
|
|
528
|
+
for (const decl of sourceFile.getVariableDeclarations()) {
|
|
529
|
+
if (decl.getName() !== schemaVarName) continue;
|
|
530
|
+
const init = decl.getInitializer();
|
|
531
|
+
if (!init || !Node.isCallExpression(init)) continue;
|
|
532
|
+
const callee = init.getExpression();
|
|
533
|
+
if (!Node.isIdentifier(callee) || callee.getText() !== "pgSchema") continue;
|
|
534
|
+
const schemaArgs = init.getArguments();
|
|
535
|
+
if (schemaArgs.length > 0 && Node.isStringLiteral(schemaArgs[0])) {
|
|
536
|
+
return schemaArgs[0].getLiteralValue();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function extractSchemaName(callExpr) {
|
|
542
|
+
const expression = callExpr.getExpression();
|
|
543
|
+
if (!Node.isPropertyAccessExpression(expression)) {
|
|
544
|
+
return "public";
|
|
545
|
+
}
|
|
546
|
+
const object = expression.getExpression();
|
|
547
|
+
if (!Node.isIdentifier(object)) {
|
|
548
|
+
return "public";
|
|
549
|
+
}
|
|
550
|
+
const schemaVarName = object.getText();
|
|
551
|
+
const sourceFile = callExpr.getSourceFile();
|
|
552
|
+
return resolveSchemaFromDeclaration(sourceFile, schemaVarName) ?? "public";
|
|
553
|
+
}
|
|
554
|
+
function isTenantColumnMatch(propName, initText) {
|
|
555
|
+
for (const tenantPattern of TENANT_COLUMN_PATTERNS) {
|
|
556
|
+
const matchesByName = propName === tenantPattern || propName.endsWith("Id");
|
|
557
|
+
const matchesByText = initText.includes(`'${tenantPattern}'`) || initText.includes(`"${tenantPattern}"`);
|
|
558
|
+
if (!matchesByName && !matchesByText) continue;
|
|
559
|
+
if (initText.includes("uuid(") && initText.includes(tenantPattern)) {
|
|
560
|
+
return propName;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
function extractTenantColumnFromTable(callExpr) {
|
|
566
|
+
const args = callExpr.getArguments();
|
|
567
|
+
if (args.length < 2) return { hasTenantColumn: false };
|
|
568
|
+
const columnsArg = args[1];
|
|
569
|
+
if (!Node.isObjectLiteralExpression(columnsArg)) return { hasTenantColumn: false };
|
|
570
|
+
for (const prop of columnsArg.getProperties()) {
|
|
571
|
+
if (!Node.isPropertyAssignment(prop)) continue;
|
|
572
|
+
const propName = prop.getName();
|
|
573
|
+
const initializer = prop.getInitializer();
|
|
574
|
+
if (!initializer) continue;
|
|
575
|
+
const matchedName = isTenantColumnMatch(propName, initializer.getText());
|
|
576
|
+
if (matchedName) {
|
|
577
|
+
return { hasTenantColumn: true, tenantColumnName: matchedName };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { hasTenantColumn: false };
|
|
581
|
+
}
|
|
582
|
+
function parseSchemaFileAST(sourceFile, filePath) {
|
|
583
|
+
const tables = [];
|
|
584
|
+
for (const varDecl of sourceFile.getVariableDeclarations()) {
|
|
585
|
+
const parent = varDecl.getParent()?.getParent();
|
|
586
|
+
if (!parent) continue;
|
|
587
|
+
const isExported = Node.isVariableStatement(parent) && parent.getModifiers()?.some((m) => m.getKind() === SyntaxKind.ExportKeyword);
|
|
588
|
+
if (!isExported) continue;
|
|
589
|
+
const initializer = varDecl.getInitializer();
|
|
590
|
+
if (!initializer || !Node.isCallExpression(initializer)) continue;
|
|
591
|
+
if (!isPgTableCall(initializer)) continue;
|
|
592
|
+
const tableName = extractTableName(initializer);
|
|
593
|
+
if (!tableName) continue;
|
|
594
|
+
const schemaName = extractSchemaName(initializer);
|
|
595
|
+
const { hasTenantColumn, tenantColumnName } = extractTenantColumnFromTable(initializer);
|
|
596
|
+
tables.push({
|
|
597
|
+
name: tableName,
|
|
598
|
+
schema: schemaName,
|
|
599
|
+
file: filePath,
|
|
600
|
+
line: varDecl.getStartLineNumber(),
|
|
601
|
+
hasRLS: false,
|
|
602
|
+
hasTenantColumn,
|
|
603
|
+
tenantColumnName
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return tables;
|
|
607
|
+
}
|
|
608
|
+
async function parseSchemaFiles(rootDir) {
|
|
609
|
+
const schemaFiles = await glob(["**/schema/*.ts", "**/schema/**/*.ts"], {
|
|
610
|
+
cwd: rootDir,
|
|
611
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/*.spec.ts"],
|
|
612
|
+
absolute: true
|
|
613
|
+
});
|
|
614
|
+
const project = new Project({
|
|
615
|
+
skipAddingFilesFromTsConfig: true,
|
|
616
|
+
skipFileDependencyResolution: true
|
|
617
|
+
});
|
|
618
|
+
const tables = [];
|
|
619
|
+
for (const filePath of schemaFiles) {
|
|
620
|
+
try {
|
|
621
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
622
|
+
const fileTables = parseSchemaFileAST(sourceFile, filePath);
|
|
623
|
+
tables.push(...fileTables);
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return tables;
|
|
628
|
+
}
|
|
629
|
+
function getLineNumber(content, matchIndex) {
|
|
630
|
+
return content.substring(0, matchIndex).split("\n").length;
|
|
631
|
+
}
|
|
632
|
+
function findPolicyMatches(content) {
|
|
633
|
+
const policyRegex = /CREATE\s+POLICY\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?\s+ON\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?\s+(?:FOR\s+(SELECT|INSERT|UPDATE|DELETE|ALL)\s+)?/gi;
|
|
634
|
+
const matches = [];
|
|
635
|
+
let match = policyRegex.exec(content);
|
|
636
|
+
while (match !== null) {
|
|
637
|
+
matches.push(match);
|
|
638
|
+
match = policyRegex.exec(content);
|
|
639
|
+
}
|
|
640
|
+
return matches;
|
|
641
|
+
}
|
|
642
|
+
function extractPolicyClauses(content, matchIndex) {
|
|
643
|
+
const remainder = content.substring(matchIndex);
|
|
644
|
+
const usingMatch = remainder.match(/USING\s*\(([\s\S]*?)(?:\)\s*(?:WITH\s+CHECK|;)|$)/i);
|
|
645
|
+
const using = usingMatch ? usingMatch[1].trim() : void 0;
|
|
646
|
+
const withCheckMatch = remainder.match(/WITH\s+CHECK\s*\(([\s\S]*?)\)\s*;/i);
|
|
647
|
+
const withCheck = withCheckMatch ? withCheckMatch[1].trim() : void 0;
|
|
648
|
+
return { using, withCheck };
|
|
649
|
+
}
|
|
650
|
+
function createPolicyFromMatch(match, content, filePath) {
|
|
651
|
+
const { using, withCheck } = extractPolicyClauses(content, match.index);
|
|
652
|
+
return {
|
|
653
|
+
name: match[1],
|
|
654
|
+
table: `${match[2] || "public"}.${match[3]}`,
|
|
655
|
+
operation: match[4]?.toUpperCase() || "ALL",
|
|
656
|
+
using,
|
|
657
|
+
withCheck,
|
|
658
|
+
file: filePath,
|
|
659
|
+
line: getLineNumber(content, match.index)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function parseSqlFileForPolicies(filePath) {
|
|
663
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
664
|
+
const matches = findPolicyMatches(content);
|
|
665
|
+
return matches.map((match) => createPolicyFromMatch(match, content, filePath));
|
|
666
|
+
}
|
|
667
|
+
async function parseRLSPolicies(rootDir) {
|
|
668
|
+
const sqlFiles = await glob(["**/sql/**/*.sql", "**/rls/**/*.sql", "**/schemas/**/*.sql"], {
|
|
669
|
+
cwd: rootDir,
|
|
670
|
+
ignore: ["**/node_modules/**", "**/dist/**"],
|
|
671
|
+
absolute: true
|
|
672
|
+
});
|
|
673
|
+
const policies = [];
|
|
674
|
+
for (const filePath of sqlFiles) {
|
|
675
|
+
try {
|
|
676
|
+
const filePolicies = await parseSqlFileForPolicies(filePath);
|
|
677
|
+
policies.push(...filePolicies);
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return policies;
|
|
682
|
+
}
|
|
683
|
+
function normalizeClause(clause) {
|
|
684
|
+
return clause.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\s+/g, " ").trim();
|
|
685
|
+
}
|
|
686
|
+
function hasTenantIsolation(policy) {
|
|
687
|
+
const usingClause = normalizeClause(policy.using || "");
|
|
688
|
+
const withCheckClause = normalizeClause(policy.withCheck || "");
|
|
689
|
+
const hasUsingIsolation = TENANT_ISOLATION_PATTERNS.some((pattern) => pattern.test(usingClause));
|
|
690
|
+
if (policy.operation === "SELECT") {
|
|
691
|
+
return hasUsingIsolation;
|
|
692
|
+
}
|
|
693
|
+
if (policy.operation === "INSERT") {
|
|
694
|
+
const hasWithCheckIsolation = TENANT_ISOLATION_PATTERNS.some(
|
|
695
|
+
(pattern) => pattern.test(withCheckClause)
|
|
696
|
+
);
|
|
697
|
+
return hasWithCheckIsolation || hasUsingIsolation;
|
|
698
|
+
}
|
|
699
|
+
return hasUsingIsolation;
|
|
700
|
+
}
|
|
701
|
+
function isSystemTable(tableName) {
|
|
702
|
+
return SYSTEM_TABLE_PATTERNS.some(
|
|
703
|
+
(pattern) => pattern === "_" ? tableName.startsWith("_") : tableName === pattern
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
function createMissingRLSFinding(table, fullTableName) {
|
|
707
|
+
return {
|
|
708
|
+
ruleId: "rls/missing-policy",
|
|
709
|
+
severity: "critical",
|
|
710
|
+
title: "Missing RLS Policy",
|
|
711
|
+
description: `Table "${fullTableName}" does not have Row Level Security enabled. All data is accessible without restrictions.`,
|
|
712
|
+
location: { file: table.file, line: table.line, column: 1 },
|
|
713
|
+
fix: {
|
|
714
|
+
description: `Enable RLS and create policies for table "${fullTableName}"`,
|
|
715
|
+
replacement: `ALTER TABLE ${fullTableName} ENABLE ROW LEVEL SECURITY;
|
|
716
|
+
CREATE POLICY tenant_isolation ON ${fullTableName} USING (tenant_id = auth.jwt() ->> 'tenant_id');`
|
|
717
|
+
},
|
|
718
|
+
cweId: CWE.IMPROPER_ACCESS_CONTROL,
|
|
719
|
+
owaspCategory: OWASP_2021.BROKEN_ACCESS_CONTROL,
|
|
720
|
+
confidence: 0.9
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function createWeakTenantFinding(table, fullTableName) {
|
|
724
|
+
return {
|
|
725
|
+
ruleId: "rls/weak-tenant",
|
|
726
|
+
severity: "high",
|
|
727
|
+
title: "Weak Tenant Isolation",
|
|
728
|
+
description: `Table "${fullTableName}" has a tenant column (${table.tenantColumnName}) but RLS policies don't enforce tenant isolation.`,
|
|
729
|
+
location: { file: table.file, line: table.line, column: 1 },
|
|
730
|
+
fix: {
|
|
731
|
+
description: "Add tenant isolation to RLS policy USING clause",
|
|
732
|
+
replacement: `USING (${table.tenantColumnName} = (auth.jwt() ->> 'tenant_id')::uuid)`
|
|
733
|
+
},
|
|
734
|
+
cweId: CWE.IMPROPER_ACCESS_CONTROL,
|
|
735
|
+
owaspCategory: OWASP_2021.BROKEN_ACCESS_CONTROL,
|
|
736
|
+
confidence: 0.8
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function createPermissiveFinding(policy) {
|
|
740
|
+
return {
|
|
741
|
+
ruleId: "rls/permissive",
|
|
742
|
+
severity: "high",
|
|
743
|
+
title: "Overly Permissive RLS Policy",
|
|
744
|
+
description: `Policy "${policy.name}" on "${policy.table}" allows unrestricted access (USING true or empty).`,
|
|
745
|
+
location: { file: policy.file, line: policy.line, column: 1 },
|
|
746
|
+
fix: { description: "Add proper restrictions to the USING clause" },
|
|
747
|
+
cweId: CWE.IMPROPER_ACCESS_CONTROL,
|
|
748
|
+
owaspCategory: OWASP_2021.BROKEN_ACCESS_CONTROL,
|
|
749
|
+
confidence: 0.95
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function isPermissivePolicy(policy) {
|
|
753
|
+
return policy.using === "true" || policy.using === "1=1" || policy.using === "";
|
|
754
|
+
}
|
|
755
|
+
function checkTableForIssues(table, tablesWithPolicies, policies) {
|
|
756
|
+
const findings = [];
|
|
757
|
+
const fullTableName = `${table.schema}.${table.name}`;
|
|
758
|
+
if (isSystemTable(table.name)) return findings;
|
|
759
|
+
if (!tablesWithPolicies.has(fullTableName)) {
|
|
760
|
+
if (isStorageTable(fullTableName)) {
|
|
761
|
+
findings.push({
|
|
762
|
+
...createMissingRLSFinding(table, fullTableName),
|
|
763
|
+
severity: "medium",
|
|
764
|
+
description: `Storage table "${fullTableName}" \u2014 Supabase provides default storage policies. Custom RLS may still be needed.`,
|
|
765
|
+
confidence: 0.4
|
|
766
|
+
});
|
|
767
|
+
return findings;
|
|
768
|
+
}
|
|
769
|
+
findings.push(createMissingRLSFinding(table, fullTableName));
|
|
770
|
+
return findings;
|
|
771
|
+
}
|
|
772
|
+
if (table.hasTenantColumn) {
|
|
773
|
+
const tablePolicies = policies.filter((p) => p.table === fullTableName);
|
|
774
|
+
const hasIsolation = tablePolicies.some(hasTenantIsolation);
|
|
775
|
+
if (!hasIsolation && tablePolicies.length > 0) {
|
|
776
|
+
findings.push(createWeakTenantFinding(table, fullTableName));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return findings;
|
|
780
|
+
}
|
|
781
|
+
var RLSAnalyzer = class {
|
|
782
|
+
name = "RLSAnalyzer";
|
|
783
|
+
categories = ["rls"];
|
|
784
|
+
async analyze(options) {
|
|
785
|
+
const tables = await parseSchemaFiles(options.rootDir);
|
|
786
|
+
const policies = await parseRLSPolicies(options.rootDir);
|
|
787
|
+
const tablesWithPolicies = new Set(policies.map((p) => p.table));
|
|
788
|
+
const tableFindings = tables.flatMap(
|
|
789
|
+
(table) => checkTableForIssues(table, tablesWithPolicies, policies)
|
|
790
|
+
);
|
|
791
|
+
const policyFindings = policies.filter(isPermissivePolicy).map((policy) => {
|
|
792
|
+
if (isStorageTable(policy.table) && /SELECT/i.test(policy.operation)) {
|
|
793
|
+
return {
|
|
794
|
+
...createPermissiveFinding(policy),
|
|
795
|
+
severity: "info",
|
|
796
|
+
description: `Policy "${policy.name}" on "${policy.table}" allows public SELECT. This is common for public file downloads. Verify this is intentional.`,
|
|
797
|
+
confidence: 0.3
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return createPermissiveFinding(policy);
|
|
801
|
+
});
|
|
802
|
+
return [...tableFindings, ...policyFindings];
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// src/internal/vuln-checker/analyzers/secret-analyzer.ts
|
|
807
|
+
init_esm_shims();
|
|
808
|
+
var MAX_LINE_LENGTH = 2e3;
|
|
809
|
+
var MAX_FILE_SIZE = 1024 * 1024;
|
|
810
|
+
var SECRET_SCAN_SKIPPED_RULE_ID = "secret/scan-skipped";
|
|
811
|
+
var SECRET_PATTERNS = [
|
|
812
|
+
// AWS
|
|
813
|
+
{
|
|
814
|
+
id: "aws-access-key",
|
|
815
|
+
name: "AWS Access Key",
|
|
816
|
+
// AWS Access Key IDs always start with AKIA (user) or ASIA (temporary)
|
|
817
|
+
pattern: /(?:AKIA|ASIA)[0-9A-Z]{16}/g,
|
|
818
|
+
severity: "critical",
|
|
819
|
+
description: "AWS Access Key ID detected",
|
|
820
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
id: "aws-secret-key",
|
|
824
|
+
name: "AWS Secret Key",
|
|
825
|
+
// AWS Secret Keys are 40 chars, but we require context clues to reduce false positives
|
|
826
|
+
// Look for assignment context: aws_secret, secret_key, AWS_SECRET, etc.
|
|
827
|
+
// SECURITY (Issue #463): Simplified pattern to avoid nested quantifiers
|
|
828
|
+
// Changed from nested optional groups to explicit alternation
|
|
829
|
+
pattern: /(?:aws_secret|aws-secret|awssecret|secret_key|secret-key|secretkey|secret_access_key|AWS_SECRET)\s{0,5}[:=]\s{0,5}['"]?([A-Za-z0-9/+=]{40})['"]?/gi,
|
|
830
|
+
severity: "critical",
|
|
831
|
+
description: "AWS Secret Access Key detected",
|
|
832
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
833
|
+
},
|
|
834
|
+
// GitHub
|
|
835
|
+
{
|
|
836
|
+
id: "github-token",
|
|
837
|
+
name: "GitHub Token",
|
|
838
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
839
|
+
pattern: /gh[pousr]_[A-Za-z0-9_]{36,100}/g,
|
|
840
|
+
severity: "critical",
|
|
841
|
+
description: "GitHub Personal Access Token detected",
|
|
842
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
id: "github-oauth",
|
|
846
|
+
name: "GitHub OAuth",
|
|
847
|
+
pattern: /gho_[A-Za-z0-9]{36}/g,
|
|
848
|
+
severity: "critical",
|
|
849
|
+
description: "GitHub OAuth Token detected",
|
|
850
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
851
|
+
},
|
|
852
|
+
// Google / Gemini
|
|
853
|
+
{
|
|
854
|
+
id: "google-api-key",
|
|
855
|
+
name: "Google / Gemini API Key",
|
|
856
|
+
pattern: /AIza[0-9A-Za-z\-_]{35}/g,
|
|
857
|
+
severity: "critical",
|
|
858
|
+
description: "Google API Key detected (used by Gemini, Vertex AI, Maps, etc.)",
|
|
859
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
id: "google-oauth",
|
|
863
|
+
name: "Google OAuth",
|
|
864
|
+
pattern: /[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com/g,
|
|
865
|
+
severity: "high",
|
|
866
|
+
description: "Google OAuth Client ID detected",
|
|
867
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
868
|
+
},
|
|
869
|
+
// Stripe
|
|
870
|
+
{
|
|
871
|
+
id: "stripe-secret",
|
|
872
|
+
name: "Stripe Secret Key",
|
|
873
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
874
|
+
pattern: /sk_live_[0-9a-zA-Z]{24,100}/g,
|
|
875
|
+
severity: "critical",
|
|
876
|
+
description: "Stripe Live Secret Key detected",
|
|
877
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
id: "stripe-test",
|
|
881
|
+
name: "Stripe Test Key",
|
|
882
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
883
|
+
pattern: /sk_test_[0-9a-zA-Z]{24,100}/g,
|
|
884
|
+
severity: "medium",
|
|
885
|
+
description: "Stripe Test Secret Key detected",
|
|
886
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
887
|
+
},
|
|
888
|
+
// Supabase
|
|
889
|
+
{
|
|
890
|
+
id: "supabase-service-role",
|
|
891
|
+
name: "Supabase Service Role Key",
|
|
892
|
+
// Supabase JWT: header.payload.signature
|
|
893
|
+
// Header: {"alg":"HS256","typ":"JWT"} (fixed)
|
|
894
|
+
// Payload prefix: {"iss":"supabase","ref": (24 bytes, 3-byte aligned = stable base64url)
|
|
895
|
+
// Service role marker: role":"service_role" = cm9sZSI6InNlcnZpY2Vfcm9sZSI (stable)
|
|
896
|
+
// Variable content between prefix and marker covers the encoded project ref
|
|
897
|
+
pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6[A-Za-z0-9_-]+cm9sZSI6InNlcnZpY2Vfcm9sZSI[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+/g,
|
|
898
|
+
severity: "critical",
|
|
899
|
+
description: "Supabase Service Role Key detected",
|
|
900
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
901
|
+
},
|
|
902
|
+
// Private Keys
|
|
903
|
+
// NOTE: Patterns are split to avoid triggering publish-safety checks when bundled
|
|
904
|
+
{
|
|
905
|
+
id: "private-key-rsa",
|
|
906
|
+
name: "RSA Private Key",
|
|
907
|
+
pattern: new RegExp("-----BEGIN RSA PRIVATE KEY-----", "g"),
|
|
908
|
+
severity: "critical",
|
|
909
|
+
description: "RSA Private Key detected",
|
|
910
|
+
cweId: CWE.HARDCODED_CRYPTO_KEY
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
id: "private-key-openssh",
|
|
914
|
+
name: "OpenSSH Private Key",
|
|
915
|
+
pattern: new RegExp("-----BEGIN OPENSSH PRIVATE KEY-----", "g"),
|
|
916
|
+
severity: "critical",
|
|
917
|
+
description: "OpenSSH Private Key detected",
|
|
918
|
+
cweId: CWE.HARDCODED_CRYPTO_KEY
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: "private-key-ec",
|
|
922
|
+
name: "EC Private Key",
|
|
923
|
+
pattern: new RegExp("-----BEGIN EC PRIVATE KEY-----", "g"),
|
|
924
|
+
severity: "critical",
|
|
925
|
+
description: "EC Private Key detected",
|
|
926
|
+
cweId: CWE.HARDCODED_CRYPTO_KEY
|
|
927
|
+
},
|
|
928
|
+
// Generic patterns
|
|
929
|
+
// SECURITY (Issue #463): All generic patterns use bounded quantifiers to prevent ReDoS
|
|
930
|
+
{
|
|
931
|
+
id: "password-assignment",
|
|
932
|
+
name: "Password Assignment",
|
|
933
|
+
// Limited whitespace to max 5 chars, limited value length to max 200 chars
|
|
934
|
+
pattern: /(?:password|passwd|pwd)\s{0,5}[:=]\s{0,5}['"][^'"]{8,200}['"]/gi,
|
|
935
|
+
severity: "high",
|
|
936
|
+
description: "Hardcoded password detected",
|
|
937
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
id: "api-key-assignment",
|
|
941
|
+
name: "API Key Assignment",
|
|
942
|
+
// Explicit alternation instead of nested optional groups
|
|
943
|
+
pattern: /(?:api_key|api-key|apikey)\s{0,5}[:=]\s{0,5}['"][^'"]{16,200}['"]/gi,
|
|
944
|
+
severity: "high",
|
|
945
|
+
description: "Hardcoded API key detected",
|
|
946
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
id: "secret-assignment",
|
|
950
|
+
name: "Secret Assignment",
|
|
951
|
+
pattern: /(?:secret|token)\s{0,5}[:=]\s{0,5}['"][^'"]{16,200}['"]/gi,
|
|
952
|
+
severity: "high",
|
|
953
|
+
description: "Hardcoded secret/token detected",
|
|
954
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
955
|
+
},
|
|
956
|
+
// Connection strings
|
|
957
|
+
{
|
|
958
|
+
id: "database-url",
|
|
959
|
+
name: "Database Connection String",
|
|
960
|
+
// Require actual credentials (not user:password placeholder patterns)
|
|
961
|
+
// Exclude common placeholder usernames: user, username, admin, root with simple passwords
|
|
962
|
+
// SECURITY (Issue #463): Bounded character classes to prevent ReDoS
|
|
963
|
+
// Limited each segment to max 200 chars
|
|
964
|
+
pattern: /(?:postgresql|mysql|mongodb|redis):\/\/(?!(?:user|username|admin|root):(?:password|pass|secret)@)[^\s'"]{1,200}:[^\s'"]{1,200}@[^\s'"]{1,200}/gi,
|
|
965
|
+
severity: "critical",
|
|
966
|
+
description: "Database connection string with credentials detected",
|
|
967
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
968
|
+
},
|
|
969
|
+
// JWT - Lower severity because JWTs in code are often:
|
|
970
|
+
// 1. Test tokens with known secrets
|
|
971
|
+
// 2. Example tokens from documentation
|
|
972
|
+
// 3. Expired tokens used for testing
|
|
973
|
+
//
|
|
974
|
+
// Pattern notes:
|
|
975
|
+
// - Header (eyJ...): Base64-encoded {"alg":...,"typ":"JWT"}
|
|
976
|
+
// - Payload (eyJ...): Base64-encoded claims
|
|
977
|
+
// - Signature: Variable length (32-512+ chars depending on algorithm)
|
|
978
|
+
// HS256: ~43 chars, RS256: ~342 chars, ES256: ~86 chars
|
|
979
|
+
{
|
|
980
|
+
id: "jwt-token",
|
|
981
|
+
name: "JWT Token",
|
|
982
|
+
// Improved: Require minimum signature length of 20 chars to reduce false positives
|
|
983
|
+
// from partial JWT-like strings in code
|
|
984
|
+
// SECURITY (Issue #463): Bounded quantifiers to prevent ReDoS
|
|
985
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,500}\.eyJ[A-Za-z0-9_-]{10,1000}\.[A-Za-z0-9_-]{20,600}/g,
|
|
986
|
+
severity: "low",
|
|
987
|
+
// Changed from medium - often false positive
|
|
988
|
+
description: "JWT token detected (verify if production token)",
|
|
989
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
990
|
+
},
|
|
991
|
+
// Slack
|
|
992
|
+
{
|
|
993
|
+
id: "slack-token",
|
|
994
|
+
name: "Slack Token",
|
|
995
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
996
|
+
pattern: /xox[baprs]-[0-9A-Za-z-]{10,100}/g,
|
|
997
|
+
severity: "high",
|
|
998
|
+
description: "Slack token detected",
|
|
999
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1000
|
+
},
|
|
1001
|
+
// Discord
|
|
1002
|
+
{
|
|
1003
|
+
id: "discord-token",
|
|
1004
|
+
name: "Discord Token",
|
|
1005
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
1006
|
+
pattern: /[MN][A-Za-z\d]{23,100}\.[\w-]{6}\.[\w-]{27}/g,
|
|
1007
|
+
severity: "high",
|
|
1008
|
+
description: "Discord bot token detected",
|
|
1009
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1010
|
+
},
|
|
1011
|
+
// SendGrid
|
|
1012
|
+
{
|
|
1013
|
+
id: "sendgrid-api-key",
|
|
1014
|
+
name: "SendGrid API Key",
|
|
1015
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g,
|
|
1016
|
+
severity: "high",
|
|
1017
|
+
description: "SendGrid API key detected",
|
|
1018
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1019
|
+
},
|
|
1020
|
+
// Twilio
|
|
1021
|
+
{
|
|
1022
|
+
id: "twilio-api-key",
|
|
1023
|
+
name: "Twilio API Key",
|
|
1024
|
+
pattern: /SK[a-f0-9]{32}/g,
|
|
1025
|
+
severity: "high",
|
|
1026
|
+
description: "Twilio API Key detected",
|
|
1027
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1028
|
+
},
|
|
1029
|
+
// Mailchimp
|
|
1030
|
+
// Format: {32-char-hex}-us{datacenter}
|
|
1031
|
+
// Datacenter can be 1-3 digits (us1 through us999 for future-proofing)
|
|
1032
|
+
{
|
|
1033
|
+
id: "mailchimp-api-key",
|
|
1034
|
+
name: "Mailchimp API Key",
|
|
1035
|
+
pattern: /[a-f0-9]{32}-us[0-9]{1,3}/g,
|
|
1036
|
+
severity: "high",
|
|
1037
|
+
description: "Mailchimp API key detected",
|
|
1038
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1039
|
+
},
|
|
1040
|
+
// npm
|
|
1041
|
+
{
|
|
1042
|
+
id: "npm-token",
|
|
1043
|
+
name: "npm Token",
|
|
1044
|
+
pattern: /npm_[A-Za-z0-9]{36}/g,
|
|
1045
|
+
severity: "high",
|
|
1046
|
+
description: "npm access token detected",
|
|
1047
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1048
|
+
},
|
|
1049
|
+
// Vercel
|
|
1050
|
+
{
|
|
1051
|
+
id: "vercel-token",
|
|
1052
|
+
name: "Vercel Token",
|
|
1053
|
+
pattern: /vercel_[A-Za-z0-9]{24}/g,
|
|
1054
|
+
severity: "high",
|
|
1055
|
+
description: "Vercel token detected",
|
|
1056
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1057
|
+
},
|
|
1058
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1059
|
+
// AI Service API Keys (2024-2026 patterns)
|
|
1060
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1061
|
+
{
|
|
1062
|
+
id: "openai-api-key",
|
|
1063
|
+
name: "OpenAI API Key",
|
|
1064
|
+
// Format: sk-{48+ alphanumeric chars} (real keys are typically 51 chars)
|
|
1065
|
+
// SECURITY: Bounded quantifier to prevent ReDoS
|
|
1066
|
+
pattern: /sk-[a-zA-Z0-9]{48,200}/g,
|
|
1067
|
+
severity: "critical",
|
|
1068
|
+
description: "OpenAI API key detected",
|
|
1069
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
id: "openai-project-key",
|
|
1073
|
+
name: "OpenAI Project Key",
|
|
1074
|
+
// Format: sk-proj-{48+ alphanumeric/dash/underscore chars}
|
|
1075
|
+
// SECURITY: Bounded quantifier to prevent ReDoS
|
|
1076
|
+
pattern: /sk-proj-[a-zA-Z0-9_-]{48,200}/g,
|
|
1077
|
+
severity: "critical",
|
|
1078
|
+
description: "OpenAI Project API key detected",
|
|
1079
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
id: "anthropic-api-key",
|
|
1083
|
+
name: "Anthropic API Key",
|
|
1084
|
+
// Format: sk-ant-api03-{base64-like string}
|
|
1085
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
1086
|
+
pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{40,200}/g,
|
|
1087
|
+
severity: "critical",
|
|
1088
|
+
description: "Anthropic (Claude) API key detected",
|
|
1089
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
id: "cohere-api-key",
|
|
1093
|
+
name: "Cohere API Key",
|
|
1094
|
+
// Format: varies, but often starts with identifiable prefix
|
|
1095
|
+
// SECURITY (Issue #463): Explicit alternation instead of nested optional groups
|
|
1096
|
+
pattern: /(?:cohere_api_key|cohere-api-key|cohere_key|cohere-key|coherekey|COHERE_API_KEY|COHERE_KEY)\s{0,5}[:=]\s{0,5}['"][A-Za-z0-9]{40,200}['"]/g,
|
|
1097
|
+
severity: "high",
|
|
1098
|
+
description: "Cohere API key detected",
|
|
1099
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
id: "mistral-api-key",
|
|
1103
|
+
name: "Mistral API Key",
|
|
1104
|
+
// Mistral keys have specific format
|
|
1105
|
+
// SECURITY (Issue #463): Explicit alternation instead of nested optional groups
|
|
1106
|
+
pattern: /(?:mistral_api_key|mistral-api-key|mistral_key|mistral-key|mistralkey|MISTRAL_API_KEY|MISTRAL_KEY)\s{0,5}[:=]\s{0,5}['"][A-Za-z0-9]{32,200}['"]/g,
|
|
1107
|
+
severity: "high",
|
|
1108
|
+
description: "Mistral AI API key detected",
|
|
1109
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
id: "openrouter-api-key",
|
|
1113
|
+
name: "OpenRouter API Key",
|
|
1114
|
+
// Format: sk-or-v1-{64 chars}
|
|
1115
|
+
pattern: /sk-or-v1-[A-Za-z0-9]{64}/g,
|
|
1116
|
+
severity: "critical",
|
|
1117
|
+
description: "OpenRouter API key detected",
|
|
1118
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
id: "replicate-api-key",
|
|
1122
|
+
name: "Replicate API Key",
|
|
1123
|
+
// Format: r8_...
|
|
1124
|
+
pattern: /r8_[A-Za-z0-9]{40}/g,
|
|
1125
|
+
severity: "high",
|
|
1126
|
+
description: "Replicate API key detected",
|
|
1127
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
id: "huggingface-token",
|
|
1131
|
+
name: "HuggingFace Token",
|
|
1132
|
+
// Format: hf_...
|
|
1133
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
1134
|
+
pattern: /hf_[A-Za-z0-9]{34,100}/g,
|
|
1135
|
+
severity: "high",
|
|
1136
|
+
description: "HuggingFace API token detected",
|
|
1137
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
id: "groq-api-key",
|
|
1141
|
+
name: "Groq API Key",
|
|
1142
|
+
// Format: gsk_...
|
|
1143
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
1144
|
+
pattern: /gsk_[A-Za-z0-9]{52,100}/g,
|
|
1145
|
+
severity: "critical",
|
|
1146
|
+
description: "Groq API key detected",
|
|
1147
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
id: "together-api-key",
|
|
1151
|
+
name: "Together AI API Key",
|
|
1152
|
+
// Context-based detection
|
|
1153
|
+
// SECURITY (Issue #463): Explicit alternation instead of nested optional groups
|
|
1154
|
+
pattern: /(?:together_api_key|together-api-key|together_key|together-key|togetherkey|TOGETHER_API_KEY|TOGETHER_KEY)\s{0,5}[:=]\s{0,5}['"][A-Za-z0-9]{64,200}['"]/g,
|
|
1155
|
+
severity: "high",
|
|
1156
|
+
description: "Together AI API key detected",
|
|
1157
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
id: "fireworks-api-key",
|
|
1161
|
+
name: "Fireworks AI API Key",
|
|
1162
|
+
// Format: fw_...
|
|
1163
|
+
// SECURITY (Issue #463): Bounded quantifier to prevent ReDoS
|
|
1164
|
+
pattern: /fw_[A-Za-z0-9]{40,100}/g,
|
|
1165
|
+
severity: "high",
|
|
1166
|
+
description: "Fireworks AI API key detected",
|
|
1167
|
+
cweId: CWE.HARDCODED_CREDENTIALS
|
|
1168
|
+
}
|
|
1169
|
+
];
|
|
1170
|
+
var BASE64_SECRET_INDICATORS = [
|
|
1171
|
+
"sk-",
|
|
1172
|
+
// OpenAI, Stripe
|
|
1173
|
+
"ghp_",
|
|
1174
|
+
// GitHub PAT
|
|
1175
|
+
"gho_",
|
|
1176
|
+
// GitHub OAuth
|
|
1177
|
+
"AKIA",
|
|
1178
|
+
// AWS Access Key
|
|
1179
|
+
"eyJ",
|
|
1180
|
+
// JWT (already base64, double-encoded)
|
|
1181
|
+
"vercel_",
|
|
1182
|
+
// Vercel
|
|
1183
|
+
"sbp_"
|
|
1184
|
+
// Supabase
|
|
1185
|
+
];
|
|
1186
|
+
function checkBase64ForSecrets(base64String) {
|
|
1187
|
+
try {
|
|
1188
|
+
if (!/^[A-Za-z0-9+/]+=*$/.test(base64String)) {
|
|
1189
|
+
return { found: false };
|
|
1190
|
+
}
|
|
1191
|
+
if (base64String.length < 20) {
|
|
1192
|
+
return { found: false };
|
|
1193
|
+
}
|
|
1194
|
+
const decoded = Buffer.from(base64String, "base64").toString("utf-8");
|
|
1195
|
+
for (const indicator of BASE64_SECRET_INDICATORS) {
|
|
1196
|
+
if (decoded.startsWith(indicator)) {
|
|
1197
|
+
const secretType = getSecretTypeFromIndicator(indicator);
|
|
1198
|
+
return { found: true, secretType };
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return { found: false };
|
|
1202
|
+
} catch {
|
|
1203
|
+
return { found: false };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function getSecretTypeFromIndicator(indicator) {
|
|
1207
|
+
const indicatorMap = {
|
|
1208
|
+
"sk-": "OpenAI/Stripe API key",
|
|
1209
|
+
ghp_: "GitHub Personal Access Token",
|
|
1210
|
+
gho_: "GitHub OAuth Token",
|
|
1211
|
+
AKIA: "AWS Access Key",
|
|
1212
|
+
eyJ: "JWT Token",
|
|
1213
|
+
vercel_: "Vercel Token",
|
|
1214
|
+
sbp_: "Supabase Token"
|
|
1215
|
+
};
|
|
1216
|
+
return indicatorMap[indicator] ?? "Unknown secret type";
|
|
1217
|
+
}
|
|
1218
|
+
var EXCLUDED_PATTERNS = [
|
|
1219
|
+
/\.env\.example$/,
|
|
1220
|
+
/\.env\.sample$/,
|
|
1221
|
+
/\.env\.template$/,
|
|
1222
|
+
/node_modules/,
|
|
1223
|
+
/\.git\//,
|
|
1224
|
+
/dist\//,
|
|
1225
|
+
/\.next\//,
|
|
1226
|
+
/coverage\//,
|
|
1227
|
+
/\.lock$/,
|
|
1228
|
+
/lock\.json$/,
|
|
1229
|
+
/\.test\./,
|
|
1230
|
+
/\.spec\./,
|
|
1231
|
+
/__tests__/,
|
|
1232
|
+
/fixtures/,
|
|
1233
|
+
/mocks/
|
|
1234
|
+
];
|
|
1235
|
+
function isCommentLine(line) {
|
|
1236
|
+
const trimmed = line.trim();
|
|
1237
|
+
return trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.startsWith("<!--") || trimmed.startsWith("--");
|
|
1238
|
+
}
|
|
1239
|
+
function isInDocumentationContext(line) {
|
|
1240
|
+
const lineLower = line.toLowerCase();
|
|
1241
|
+
const docPatterns = [
|
|
1242
|
+
"format:",
|
|
1243
|
+
"example:",
|
|
1244
|
+
"e.g.",
|
|
1245
|
+
"e.g:",
|
|
1246
|
+
"i.e.",
|
|
1247
|
+
"sample:",
|
|
1248
|
+
"template:",
|
|
1249
|
+
"pattern:",
|
|
1250
|
+
"@example",
|
|
1251
|
+
"@param",
|
|
1252
|
+
"@returns",
|
|
1253
|
+
"@see",
|
|
1254
|
+
"usage:",
|
|
1255
|
+
"syntax:",
|
|
1256
|
+
"like:",
|
|
1257
|
+
"such as",
|
|
1258
|
+
"documentation",
|
|
1259
|
+
"readme",
|
|
1260
|
+
"docs/"
|
|
1261
|
+
];
|
|
1262
|
+
return docPatterns.some((p) => lineLower.includes(p));
|
|
1263
|
+
}
|
|
1264
|
+
function isInCodeExample(line) {
|
|
1265
|
+
const trimmed = line.trim();
|
|
1266
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
const exampleIndicators = [
|
|
1270
|
+
"const example",
|
|
1271
|
+
"let example",
|
|
1272
|
+
"var example",
|
|
1273
|
+
"// example",
|
|
1274
|
+
"# example",
|
|
1275
|
+
"example =",
|
|
1276
|
+
"example:"
|
|
1277
|
+
];
|
|
1278
|
+
return exampleIndicators.some((p) => line.toLowerCase().includes(p));
|
|
1279
|
+
}
|
|
1280
|
+
function calculateEntropy(str) {
|
|
1281
|
+
if (str.length === 0) return 0;
|
|
1282
|
+
const freq = /* @__PURE__ */ new Map();
|
|
1283
|
+
for (const char of str) {
|
|
1284
|
+
freq.set(char, (freq.get(char) || 0) + 1);
|
|
1285
|
+
}
|
|
1286
|
+
let entropy = 0;
|
|
1287
|
+
const counts = Array.from(freq.values());
|
|
1288
|
+
for (const count of counts) {
|
|
1289
|
+
const p = count / str.length;
|
|
1290
|
+
entropy -= p * Math.log2(p);
|
|
1291
|
+
}
|
|
1292
|
+
return entropy;
|
|
1293
|
+
}
|
|
1294
|
+
function looksLikePlaceholder(match) {
|
|
1295
|
+
if (match.length > 500) {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
const matchLower = match.toLowerCase();
|
|
1299
|
+
const placeholders = [
|
|
1300
|
+
"your_",
|
|
1301
|
+
"your-",
|
|
1302
|
+
"yourkeyhere",
|
|
1303
|
+
"your_key",
|
|
1304
|
+
"replace_",
|
|
1305
|
+
"replace-",
|
|
1306
|
+
"replaceme",
|
|
1307
|
+
"xxx",
|
|
1308
|
+
"yyy",
|
|
1309
|
+
"zzz",
|
|
1310
|
+
"placeholder",
|
|
1311
|
+
"example",
|
|
1312
|
+
"sample",
|
|
1313
|
+
"test",
|
|
1314
|
+
"demo",
|
|
1315
|
+
"fake",
|
|
1316
|
+
"mock",
|
|
1317
|
+
"dummy",
|
|
1318
|
+
"todo",
|
|
1319
|
+
"fixme",
|
|
1320
|
+
"changeme",
|
|
1321
|
+
"<your",
|
|
1322
|
+
"[your",
|
|
1323
|
+
"{your",
|
|
1324
|
+
"****",
|
|
1325
|
+
"....",
|
|
1326
|
+
"----"
|
|
1327
|
+
];
|
|
1328
|
+
if (placeholders.some((p) => matchLower.includes(p))) {
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
if (match.length >= 8) {
|
|
1332
|
+
const uniqueChars = new Set(match).size;
|
|
1333
|
+
const uniqueRatio = uniqueChars / match.length;
|
|
1334
|
+
if (uniqueRatio < 0.15) {
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (/^[a-z]+$/.test(match) || /^[A-Z]+$/.test(match) || /^[0-9]+$/.test(match)) {
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
function isCodeDefinitionContext(lineLower) {
|
|
1344
|
+
const codePatterns = [
|
|
1345
|
+
"type ",
|
|
1346
|
+
"interface ",
|
|
1347
|
+
": string",
|
|
1348
|
+
"z.string()",
|
|
1349
|
+
"zod.",
|
|
1350
|
+
".parse(",
|
|
1351
|
+
"regexp",
|
|
1352
|
+
"regex",
|
|
1353
|
+
"/g,",
|
|
1354
|
+
"/gi,"
|
|
1355
|
+
];
|
|
1356
|
+
return codePatterns.some((p) => lineLower.includes(p));
|
|
1357
|
+
}
|
|
1358
|
+
function isTestContext(lineLower) {
|
|
1359
|
+
const testPatterns = ["expect(", "assert", "should."];
|
|
1360
|
+
return testPatterns.some((p) => lineLower.includes(p));
|
|
1361
|
+
}
|
|
1362
|
+
function isEnvReference(lineLower) {
|
|
1363
|
+
const envPatterns = ["process.env", "env.", "getenv"];
|
|
1364
|
+
return envPatterns.some((p) => lineLower.includes(p));
|
|
1365
|
+
}
|
|
1366
|
+
function isLikelyFalsePositive(line, match, patternId) {
|
|
1367
|
+
const lineLower = line.toLowerCase();
|
|
1368
|
+
if (isCommentLine(line)) return true;
|
|
1369
|
+
if (isInDocumentationContext(line)) return true;
|
|
1370
|
+
if (isInCodeExample(line)) return true;
|
|
1371
|
+
if (isEnvReference(lineLower)) return true;
|
|
1372
|
+
if (looksLikePlaceholder(match)) return true;
|
|
1373
|
+
if (isCodeDefinitionContext(lineLower)) return true;
|
|
1374
|
+
if (isTestContext(lineLower)) return true;
|
|
1375
|
+
if (patternId === "aws-secret-key" && calculateEntropy(match) < 3.5) {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
function maskSecret(secret) {
|
|
1381
|
+
if (secret.length > 10) {
|
|
1382
|
+
return `${secret.substring(0, 5)}...${secret.substring(secret.length - 3)}`;
|
|
1383
|
+
}
|
|
1384
|
+
return "***";
|
|
1385
|
+
}
|
|
1386
|
+
function calculateConfidence(pattern) {
|
|
1387
|
+
const highConfidencePatterns = [
|
|
1388
|
+
"aws-access-key",
|
|
1389
|
+
// AKIA/ASIA prefix is unique to AWS
|
|
1390
|
+
"github-token",
|
|
1391
|
+
// ghp_/gho_/etc prefix is unique
|
|
1392
|
+
"github-oauth",
|
|
1393
|
+
"openai-api-key",
|
|
1394
|
+
// sk- prefix with length constraint
|
|
1395
|
+
"openai-project-key",
|
|
1396
|
+
// sk-proj- prefix is unique
|
|
1397
|
+
"anthropic-api-key",
|
|
1398
|
+
// sk-ant-api prefix is unique
|
|
1399
|
+
"google-api-key",
|
|
1400
|
+
// AIza prefix is unique to Google (Gemini, Vertex AI, etc.)
|
|
1401
|
+
"stripe-secret",
|
|
1402
|
+
// sk_live_ prefix is unique
|
|
1403
|
+
"stripe-test",
|
|
1404
|
+
// sk_test_ prefix is unique
|
|
1405
|
+
"sendgrid-api-key",
|
|
1406
|
+
// SG. prefix is unique
|
|
1407
|
+
"npm-token",
|
|
1408
|
+
// npm_ prefix is unique
|
|
1409
|
+
"vercel-token",
|
|
1410
|
+
// vercel_ prefix is unique
|
|
1411
|
+
"private-key-rsa",
|
|
1412
|
+
// BEGIN RSA PRIVATE KEY is unique
|
|
1413
|
+
"private-key-openssh",
|
|
1414
|
+
"private-key-ec",
|
|
1415
|
+
"slack-token"
|
|
1416
|
+
// xox prefix is unique
|
|
1417
|
+
];
|
|
1418
|
+
if (highConfidencePatterns.includes(pattern.id)) {
|
|
1419
|
+
return 0.95;
|
|
1420
|
+
}
|
|
1421
|
+
const mediumConfidencePatterns = [
|
|
1422
|
+
"google-oauth",
|
|
1423
|
+
"database-url",
|
|
1424
|
+
// Has structure but could be example
|
|
1425
|
+
"aws-secret-key"
|
|
1426
|
+
// Requires context clues
|
|
1427
|
+
];
|
|
1428
|
+
if (mediumConfidencePatterns.includes(pattern.id)) {
|
|
1429
|
+
return 0.7;
|
|
1430
|
+
}
|
|
1431
|
+
return 0.5;
|
|
1432
|
+
}
|
|
1433
|
+
function createSecretFinding(pattern, filePath, lineNumber, line, match) {
|
|
1434
|
+
const matchText = match[0];
|
|
1435
|
+
const maskedSecret = maskSecret(matchText);
|
|
1436
|
+
const confidence = calculateConfidence(pattern);
|
|
1437
|
+
return {
|
|
1438
|
+
ruleId: `secret/${pattern.id}`,
|
|
1439
|
+
severity: pattern.severity,
|
|
1440
|
+
title: pattern.name,
|
|
1441
|
+
description: `${pattern.description}. Value: ${maskedSecret}`,
|
|
1442
|
+
location: {
|
|
1443
|
+
file: filePath,
|
|
1444
|
+
line: lineNumber,
|
|
1445
|
+
column: match.index
|
|
1446
|
+
},
|
|
1447
|
+
snippet: {
|
|
1448
|
+
text: line.length > 100 ? `${line.substring(0, 100)}...` : line,
|
|
1449
|
+
highlightStart: match.index,
|
|
1450
|
+
highlightEnd: match.index + matchText.length
|
|
1451
|
+
},
|
|
1452
|
+
fix: {
|
|
1453
|
+
description: "Move secret to environment variable",
|
|
1454
|
+
replacement: `process.env.${pattern.id.toUpperCase().replace(/-/g, "_")}`
|
|
1455
|
+
},
|
|
1456
|
+
cweId: pattern.cweId,
|
|
1457
|
+
owaspCategory: OWASP_2021.CRYPTO_FAILURES,
|
|
1458
|
+
confidence
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function findPatternMatches(pattern, line) {
|
|
1462
|
+
pattern.pattern.lastIndex = 0;
|
|
1463
|
+
const matches = [];
|
|
1464
|
+
let match = pattern.pattern.exec(line);
|
|
1465
|
+
while (match !== null) {
|
|
1466
|
+
matches.push(match);
|
|
1467
|
+
match = pattern.pattern.exec(line);
|
|
1468
|
+
}
|
|
1469
|
+
return matches;
|
|
1470
|
+
}
|
|
1471
|
+
function collectPatternSecretFindings(line, lineNumber, filePath) {
|
|
1472
|
+
const findings = [];
|
|
1473
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
1474
|
+
const matches = findPatternMatches(pattern, line);
|
|
1475
|
+
for (const match of matches) {
|
|
1476
|
+
if (isLikelyFalsePositive(line, match[0], pattern.id)) continue;
|
|
1477
|
+
findings.push(createSecretFinding(pattern, filePath, lineNumber, line, match));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return findings;
|
|
1481
|
+
}
|
|
1482
|
+
var BASE64_SECRET_PATTERN = /['"`]([A-Za-z0-9+/]{40,500}=*)['"`]/g;
|
|
1483
|
+
function createBase64SecretFinding(line, lineNumber, filePath, base64Match, secretType) {
|
|
1484
|
+
const start = base64Match.index ?? 0;
|
|
1485
|
+
const fullMatch = base64Match[0] ?? "";
|
|
1486
|
+
return {
|
|
1487
|
+
ruleId: "secret/base64-encoded",
|
|
1488
|
+
severity: "high",
|
|
1489
|
+
title: "Base64-Encoded Secret",
|
|
1490
|
+
description: `Base64-encoded secret detected. Type: ${secretType}`,
|
|
1491
|
+
location: {
|
|
1492
|
+
file: filePath,
|
|
1493
|
+
line: lineNumber,
|
|
1494
|
+
column: start
|
|
1495
|
+
},
|
|
1496
|
+
snippet: {
|
|
1497
|
+
text: line.length > 100 ? `${line.substring(0, 100)}...` : line,
|
|
1498
|
+
highlightStart: start,
|
|
1499
|
+
highlightEnd: start + fullMatch.length
|
|
1500
|
+
},
|
|
1501
|
+
fix: {
|
|
1502
|
+
description: "Remove encoded secret and use environment variable",
|
|
1503
|
+
replacement: "process.env.SECRET_KEY"
|
|
1504
|
+
},
|
|
1505
|
+
cweId: CWE.HARDCODED_CREDENTIALS,
|
|
1506
|
+
owaspCategory: OWASP_2021.CRYPTO_FAILURES,
|
|
1507
|
+
confidence: 0.85
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
function createSkippedLineScanFinding(filePath, firstLineNumber, skippedLineCount) {
|
|
1511
|
+
return {
|
|
1512
|
+
ruleId: SECRET_SCAN_SKIPPED_RULE_ID,
|
|
1513
|
+
severity: "low",
|
|
1514
|
+
title: "Secret Scan Partially Skipped",
|
|
1515
|
+
description: `Secret scan skipped ${skippedLineCount} overlong line(s) exceeding ${MAX_LINE_LENGTH} characters.`,
|
|
1516
|
+
location: {
|
|
1517
|
+
file: filePath,
|
|
1518
|
+
line: firstLineNumber,
|
|
1519
|
+
column: 0
|
|
1520
|
+
},
|
|
1521
|
+
fix: {
|
|
1522
|
+
description: "Split minified or generated content so the scanner can inspect each line safely"
|
|
1523
|
+
},
|
|
1524
|
+
metadata: {
|
|
1525
|
+
skippedLineCount,
|
|
1526
|
+
maxLineLength: MAX_LINE_LENGTH
|
|
1527
|
+
},
|
|
1528
|
+
cweId: CWE.HARDCODED_CREDENTIALS,
|
|
1529
|
+
owaspCategory: OWASP_2021.CRYPTO_FAILURES,
|
|
1530
|
+
confidence: 1
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function createSkippedFileScanFinding(filePath, sizeBytes) {
|
|
1534
|
+
return {
|
|
1535
|
+
ruleId: SECRET_SCAN_SKIPPED_RULE_ID,
|
|
1536
|
+
severity: "low",
|
|
1537
|
+
title: "Secret Scan Skipped Large File",
|
|
1538
|
+
description: `Secret scan skipped a file larger than ${MAX_FILE_SIZE} bytes.`,
|
|
1539
|
+
location: {
|
|
1540
|
+
file: filePath,
|
|
1541
|
+
line: 1,
|
|
1542
|
+
column: 0
|
|
1543
|
+
},
|
|
1544
|
+
fix: {
|
|
1545
|
+
description: "Reduce file size or split generated artifacts so secret scanning can inspect the content"
|
|
1546
|
+
},
|
|
1547
|
+
metadata: {
|
|
1548
|
+
sizeBytes,
|
|
1549
|
+
maxFileSize: MAX_FILE_SIZE
|
|
1550
|
+
},
|
|
1551
|
+
cweId: CWE.HARDCODED_CREDENTIALS,
|
|
1552
|
+
owaspCategory: OWASP_2021.CRYPTO_FAILURES,
|
|
1553
|
+
confidence: 1
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
function collectBase64SecretFindings(line, lineNumber, filePath) {
|
|
1557
|
+
const findings = [];
|
|
1558
|
+
for (const base64Match of line.matchAll(BASE64_SECRET_PATTERN)) {
|
|
1559
|
+
const base64String = base64Match[1];
|
|
1560
|
+
if (!base64String) continue;
|
|
1561
|
+
const result = checkBase64ForSecrets(base64String);
|
|
1562
|
+
if (!result.found) continue;
|
|
1563
|
+
if (isLikelyFalsePositive(line, base64String, "base64-secret")) continue;
|
|
1564
|
+
const secretType = result.secretType ?? "Unknown secret";
|
|
1565
|
+
findings.push(createBase64SecretFinding(line, lineNumber, filePath, base64Match, secretType));
|
|
1566
|
+
}
|
|
1567
|
+
return findings;
|
|
1568
|
+
}
|
|
1569
|
+
function scanLineForSecrets(line, lineNumber, filePath) {
|
|
1570
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
1571
|
+
return [];
|
|
1572
|
+
}
|
|
1573
|
+
return [
|
|
1574
|
+
...collectPatternSecretFindings(line, lineNumber, filePath),
|
|
1575
|
+
...collectBase64SecretFindings(line, lineNumber, filePath)
|
|
1576
|
+
];
|
|
1577
|
+
}
|
|
1578
|
+
async function scanFileForSecrets(filePath) {
|
|
1579
|
+
const stats = await fs.stat(filePath);
|
|
1580
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
1581
|
+
return [createSkippedFileScanFinding(filePath, stats.size)];
|
|
1582
|
+
}
|
|
1583
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1584
|
+
const lines = content.split("\n");
|
|
1585
|
+
const findings = [];
|
|
1586
|
+
let skippedLineCount = 0;
|
|
1587
|
+
let firstSkippedLineNumber = null;
|
|
1588
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1589
|
+
const lineNumber = i + 1;
|
|
1590
|
+
if (lines[i].length > MAX_LINE_LENGTH) {
|
|
1591
|
+
skippedLineCount += 1;
|
|
1592
|
+
firstSkippedLineNumber ??= lineNumber;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const lineFindings = scanLineForSecrets(lines[i], lineNumber, filePath);
|
|
1596
|
+
findings.push(...lineFindings);
|
|
1597
|
+
}
|
|
1598
|
+
if (firstSkippedLineNumber !== null) {
|
|
1599
|
+
findings.push(createSkippedLineScanFinding(filePath, firstSkippedLineNumber, skippedLineCount));
|
|
1600
|
+
}
|
|
1601
|
+
const multiLineFindings = scanMultiLineSecrets(content, filePath);
|
|
1602
|
+
findings.push(...multiLineFindings);
|
|
1603
|
+
return findings;
|
|
1604
|
+
}
|
|
1605
|
+
var MULTILINE_PATTERNS = [
|
|
1606
|
+
{
|
|
1607
|
+
id: "private-key-block",
|
|
1608
|
+
name: "Private Key Block",
|
|
1609
|
+
startPattern: new RegExp(
|
|
1610
|
+
"-----BEGIN\\s+(?:RSA\\s+|EC\\s+|DSA\\s+|OPENSSH\\s+)?PRIVATE\\s+KEY-----"
|
|
1611
|
+
),
|
|
1612
|
+
endPattern: new RegExp(
|
|
1613
|
+
"-----END\\s+(?:RSA\\s+|EC\\s+|DSA\\s+|OPENSSH\\s+)?PRIVATE\\s+KEY-----"
|
|
1614
|
+
),
|
|
1615
|
+
severity: "critical",
|
|
1616
|
+
description: "Private key block detected"
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
id: "pgp-private-key-block",
|
|
1620
|
+
name: "PGP Private Key Block",
|
|
1621
|
+
startPattern: new RegExp("-----BEGIN\\s+PGP\\s+PRIVATE\\s+KEY\\s+BLOCK-----"),
|
|
1622
|
+
endPattern: new RegExp("-----END\\s+PGP\\s+PRIVATE\\s+KEY\\s+BLOCK-----"),
|
|
1623
|
+
severity: "critical",
|
|
1624
|
+
description: "PGP private key block detected"
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
id: "certificate-block",
|
|
1628
|
+
name: "Certificate Block",
|
|
1629
|
+
startPattern: /-----BEGIN\s+CERTIFICATE-----/,
|
|
1630
|
+
endPattern: /-----END\s+CERTIFICATE-----/,
|
|
1631
|
+
severity: "medium",
|
|
1632
|
+
description: "Certificate block detected (verify if private)"
|
|
1633
|
+
}
|
|
1634
|
+
];
|
|
1635
|
+
function scanMultiLineSecrets(content, filePath) {
|
|
1636
|
+
const findings = [];
|
|
1637
|
+
const lines = content.split("\n");
|
|
1638
|
+
for (const pattern of MULTILINE_PATTERNS) {
|
|
1639
|
+
let startLine = -1;
|
|
1640
|
+
let endLine = -1;
|
|
1641
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1642
|
+
if (startLine === -1 && pattern.startPattern.test(lines[i])) {
|
|
1643
|
+
startLine = i;
|
|
1644
|
+
} else if (startLine !== -1 && pattern.endPattern.test(lines[i])) {
|
|
1645
|
+
endLine = i;
|
|
1646
|
+
const blockContent = lines.slice(startLine, endLine + 1).join("\n");
|
|
1647
|
+
if (isMultiLineFalsePositive(blockContent, lines, startLine)) {
|
|
1648
|
+
startLine = -1;
|
|
1649
|
+
endLine = -1;
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
findings.push({
|
|
1653
|
+
ruleId: `secret/${pattern.id}`,
|
|
1654
|
+
severity: pattern.severity,
|
|
1655
|
+
title: pattern.name,
|
|
1656
|
+
description: `${pattern.description} (${endLine - startLine + 1} lines)`,
|
|
1657
|
+
location: {
|
|
1658
|
+
file: filePath,
|
|
1659
|
+
line: startLine + 1,
|
|
1660
|
+
column: 0
|
|
1661
|
+
},
|
|
1662
|
+
snippet: {
|
|
1663
|
+
text: `${lines[startLine].substring(0, 60)}...`,
|
|
1664
|
+
highlightStart: 0,
|
|
1665
|
+
highlightEnd: lines[startLine].length
|
|
1666
|
+
},
|
|
1667
|
+
fix: {
|
|
1668
|
+
description: "Remove private key and use environment variable or secret manager",
|
|
1669
|
+
replacement: "process.env.PRIVATE_KEY"
|
|
1670
|
+
},
|
|
1671
|
+
cweId: CWE.HARDCODED_CRYPTO_KEY,
|
|
1672
|
+
owaspCategory: OWASP_2021.CRYPTO_FAILURES,
|
|
1673
|
+
confidence: 0.9
|
|
1674
|
+
});
|
|
1675
|
+
startLine = -1;
|
|
1676
|
+
endLine = -1;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return findings;
|
|
1681
|
+
}
|
|
1682
|
+
function isMultiLineFalsePositive(blockContent, lines, startLine) {
|
|
1683
|
+
const contentLower = blockContent.toLowerCase();
|
|
1684
|
+
if (/placeholder|example|sample|test|demo|fake|mock|dummy/.test(contentLower)) {
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
const blockLines = blockContent.split("\n");
|
|
1688
|
+
if (blockLines.length <= 3) {
|
|
1689
|
+
return true;
|
|
1690
|
+
}
|
|
1691
|
+
if (startLine > 0) {
|
|
1692
|
+
const prevLine = lines[startLine - 1].toLowerCase();
|
|
1693
|
+
if (prevLine.includes("example") || prevLine.includes("template") || prevLine.includes("placeholder") || prevLine.includes("format:") || prevLine.trim().startsWith("//")) {
|
|
1694
|
+
return true;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
var SecretAnalyzer = class {
|
|
1700
|
+
name = "SecretAnalyzer";
|
|
1701
|
+
categories = ["secret"];
|
|
1702
|
+
async analyze(options) {
|
|
1703
|
+
const files = await this.getFilesToScan(options);
|
|
1704
|
+
const findings = [];
|
|
1705
|
+
for (const filePath of files) {
|
|
1706
|
+
if (this.isExcluded(filePath)) continue;
|
|
1707
|
+
try {
|
|
1708
|
+
const fileFindings = await scanFileForSecrets(filePath);
|
|
1709
|
+
findings.push(...fileFindings);
|
|
1710
|
+
} catch {
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return findings;
|
|
1714
|
+
}
|
|
1715
|
+
async getFilesToScan(options) {
|
|
1716
|
+
return glob(
|
|
1717
|
+
["**/*.ts", "**/*.tsx", "**/*.js", "**/*.json", "**/*.yml", "**/*.yaml", "**/*.env*"],
|
|
1718
|
+
{
|
|
1719
|
+
cwd: options.rootDir,
|
|
1720
|
+
ignore: options.exclude || ["**/node_modules/**", "**/dist/**"],
|
|
1721
|
+
absolute: true,
|
|
1722
|
+
dot: true
|
|
1723
|
+
}
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
isExcluded(filePath) {
|
|
1727
|
+
return EXCLUDED_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
// src/internal/vuln-checker/analyzers/typescript-analyzer.ts
|
|
1732
|
+
init_esm_shims();
|
|
1733
|
+
function getLocation(node, filePath) {
|
|
1734
|
+
const startLine = node.getStartLineNumber();
|
|
1735
|
+
const startCol = node.getStartLinePos();
|
|
1736
|
+
const endLine = node.getEndLineNumber();
|
|
1737
|
+
return {
|
|
1738
|
+
file: filePath,
|
|
1739
|
+
line: startLine,
|
|
1740
|
+
column: startCol,
|
|
1741
|
+
endLine
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function getSnippet(node) {
|
|
1745
|
+
const text = node.getText();
|
|
1746
|
+
return {
|
|
1747
|
+
text: text.length > 200 ? `${text.substring(0, 200)}...` : text
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
var USER_INPUT_PATTERNS = [
|
|
1751
|
+
/\breq(?:uest)?\.(?:params|query|body|headers|cookies)/i,
|
|
1752
|
+
/\bparams\./i,
|
|
1753
|
+
/\bquery\./i,
|
|
1754
|
+
/\bbody\./i,
|
|
1755
|
+
/\buser(?:Input|Data|Id)?\b/i,
|
|
1756
|
+
/\binput\b/i,
|
|
1757
|
+
/\bargs?\b/i,
|
|
1758
|
+
/\bdata\./i,
|
|
1759
|
+
/\bctx\.(?:params|query|body)/i
|
|
1760
|
+
// Koa/Hono context
|
|
1761
|
+
];
|
|
1762
|
+
var SAFE_VARIABLE_PATTERNS = [
|
|
1763
|
+
/\.parse\s*\(/,
|
|
1764
|
+
// Zod .parse() result
|
|
1765
|
+
/\.safeParse\s*\(/,
|
|
1766
|
+
// Zod .safeParse() result
|
|
1767
|
+
/validated\w*/i,
|
|
1768
|
+
// validatedInput, validatedData
|
|
1769
|
+
/sanitized\w*/i,
|
|
1770
|
+
// sanitizedInput
|
|
1771
|
+
/parsed\w*/i,
|
|
1772
|
+
// parsedBody, parsedParams
|
|
1773
|
+
/\bconst\s+\w+\s*:\s*(string|number|boolean)\b/i
|
|
1774
|
+
// Typed constants
|
|
1775
|
+
];
|
|
1776
|
+
function hasUserInputInterpolation(text) {
|
|
1777
|
+
if (USER_INPUT_PATTERNS.some((p) => p.test(text))) return true;
|
|
1778
|
+
if (text.includes("${")) {
|
|
1779
|
+
const interpolations = text.match(/\$\{([^}]+)\}/g);
|
|
1780
|
+
if (!interpolations) return false;
|
|
1781
|
+
for (const expr of interpolations) {
|
|
1782
|
+
const innerExpr = expr.slice(2, -1).trim();
|
|
1783
|
+
if (/^\d+$/.test(innerExpr)) continue;
|
|
1784
|
+
if (SAFE_VARIABLE_PATTERNS.some((p) => p.test(text))) continue;
|
|
1785
|
+
if (USER_INPUT_PATTERNS.some((p) => p.test(innerExpr))) {
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
function hasArrayBasedSqlBuilding(text) {
|
|
1794
|
+
const arrayBuildPatterns = [
|
|
1795
|
+
/\.push\s*\(\s*`[^`]*\$\{/i,
|
|
1796
|
+
// conditions.push(`field = ${value}`)
|
|
1797
|
+
/\.push\s*\(\s*['"][^'"]*\s*\+/i,
|
|
1798
|
+
// conditions.push('field = ' + value)
|
|
1799
|
+
/\.join\s*\(\s*['"](?:\s*AND\s*|\s*OR\s*|\s*,\s*)['"]\s*\)/i
|
|
1800
|
+
// .join(' AND ')
|
|
1801
|
+
];
|
|
1802
|
+
return arrayBuildPatterns.some((p) => p.test(text));
|
|
1803
|
+
}
|
|
1804
|
+
function buildDefinitionPositionsFromNodes(nodes) {
|
|
1805
|
+
return new Set(nodes.map((node) => `${node.getSourceFile().getFilePath()}:${node.getPos()}`));
|
|
1806
|
+
}
|
|
1807
|
+
function hasDefinitionMatch(node, definitionPositions) {
|
|
1808
|
+
if (!Node.isIdentifier(node)) return false;
|
|
1809
|
+
const nodeDefinitions = node.getDefinitions().map((definition) => definition.getNode());
|
|
1810
|
+
return nodeDefinitions.some(
|
|
1811
|
+
(definitionNode) => definitionPositions.has(
|
|
1812
|
+
`${definitionNode.getSourceFile().getFilePath()}:${definitionNode.getPos()}`
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
function buildJoinedArrayBuilderRisk(node, sourceFile) {
|
|
1817
|
+
const joinTarget = getIdentifierFromJoinCall(node);
|
|
1818
|
+
if (!joinTarget) return null;
|
|
1819
|
+
const scopeNode = node.getFirstAncestor((ancestor) => Node.isBlock(ancestor) || Node.isSourceFile(ancestor)) ?? sourceFile;
|
|
1820
|
+
if (isDynamicArrayPushInScope(scopeNode.getText(), joinTarget.getText())) {
|
|
1821
|
+
return {
|
|
1822
|
+
confidence: 0.6,
|
|
1823
|
+
description: "SQL query variable may be built dynamically from array concatenation."
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
const arrayDefinitionPositions = getDefinitionPositions(joinTarget);
|
|
1827
|
+
if (arrayDefinitionPositions.size === 0) return null;
|
|
1828
|
+
if (hasDynamicPushArgumentFromDefinition(
|
|
1829
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression),
|
|
1830
|
+
arrayDefinitionPositions
|
|
1831
|
+
)) {
|
|
1832
|
+
return {
|
|
1833
|
+
confidence: 0.6,
|
|
1834
|
+
description: "SQL query variable may be built dynamically from array concatenation."
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
function getIdentifierFromJoinCall(node) {
|
|
1840
|
+
if (!Node.isCallExpression(node)) return null;
|
|
1841
|
+
const expression = node.getExpression();
|
|
1842
|
+
if (!Node.isPropertyAccessExpression(expression) || expression.getName() !== "join") return null;
|
|
1843
|
+
const joinTarget = expression.getExpression();
|
|
1844
|
+
return Node.isIdentifier(joinTarget) ? joinTarget : null;
|
|
1845
|
+
}
|
|
1846
|
+
function isDynamicArrayPushInScope(scopedText, arrayName) {
|
|
1847
|
+
return hasDynamicArrayPush(scopedText, arrayName);
|
|
1848
|
+
}
|
|
1849
|
+
function getDefinitionPositions(identifier) {
|
|
1850
|
+
return buildDefinitionPositionsFromNodes(
|
|
1851
|
+
identifier.getDefinitions().map((definition) => definition.getNode()).filter((node) => node !== void 0)
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
function hasDynamicPushArgumentFromDefinition(callExpressions, definitionPositions) {
|
|
1855
|
+
for (const call of callExpressions) {
|
|
1856
|
+
const pushExpression = call.getExpression();
|
|
1857
|
+
if (!Node.isPropertyAccessExpression(pushExpression) || pushExpression.getName() !== "push") {
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
const pushTarget = pushExpression.getExpression();
|
|
1861
|
+
if (!Node.isIdentifier(pushTarget) || !hasDefinitionMatch(pushTarget, definitionPositions)) {
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const pushArg = call.getArguments()[0];
|
|
1865
|
+
if (!pushArg) continue;
|
|
1866
|
+
if ((Node.isTemplateExpression(pushArg) || Node.isBinaryExpression(pushArg)) && hasUserInputInterpolation(pushArg.getText())) {
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
function hasDynamicArrayPush(scopedText, arrayName) {
|
|
1873
|
+
return new RegExp(`\\b${arrayName}\\.push\\s*\\(\\s*\`[^\\\`]*\\$\\{`, "m").test(scopedText) || new RegExp(`\\b${arrayName}\\.push\\s*\\(\\s*['"][^'"]*['"]\\s*\\+`, "m").test(scopedText);
|
|
1874
|
+
}
|
|
1875
|
+
function collectIdentifierDefinitionRisk(definitionNode, sourceFile) {
|
|
1876
|
+
if (!Node.isVariableDeclaration(definitionNode)) return null;
|
|
1877
|
+
const initializer = definitionNode.getInitializer();
|
|
1878
|
+
if (!initializer) return null;
|
|
1879
|
+
const risk = classifyDynamicSqlNode(initializer);
|
|
1880
|
+
if (risk) return risk;
|
|
1881
|
+
return buildJoinedArrayBuilderRisk(initializer, sourceFile);
|
|
1882
|
+
}
|
|
1883
|
+
function detectAssignmentRisk(sourceFile, identifierName, definitionPositions) {
|
|
1884
|
+
for (const expression of sourceFile.getDescendantsOfKind(SyntaxKind.BinaryExpression)) {
|
|
1885
|
+
if (expression.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) continue;
|
|
1886
|
+
const left = expression.getLeft();
|
|
1887
|
+
if (!Node.isIdentifier(left) || left.getText() !== identifierName) continue;
|
|
1888
|
+
if (!hasDefinitionMatch(left, definitionPositions)) continue;
|
|
1889
|
+
const risk = classifyDynamicSqlNode(expression.getRight());
|
|
1890
|
+
if (risk) return risk;
|
|
1891
|
+
const joinRisk = buildJoinedArrayBuilderRisk(expression.getRight(), sourceFile);
|
|
1892
|
+
if (joinRisk) return joinRisk;
|
|
1893
|
+
}
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
function detectIdentifierJoinPattern(firstArg, identifierName, sourceFile) {
|
|
1897
|
+
const scopeNode = firstArg.getFirstAncestor(
|
|
1898
|
+
(ancestor) => Node.isBlock(ancestor) || Node.isSourceFile(ancestor)
|
|
1899
|
+
) ?? sourceFile;
|
|
1900
|
+
const scopedText = scopeNode.getText();
|
|
1901
|
+
const identifierJoinPattern = new RegExp(
|
|
1902
|
+
`(?:const|let|var)\\s+${identifierName}\\s*=\\s*(\\w+)\\.join\\s*\\(`,
|
|
1903
|
+
"m"
|
|
1904
|
+
);
|
|
1905
|
+
const joinMatch = scopedText.match(identifierJoinPattern);
|
|
1906
|
+
if (!joinMatch?.[1]) return null;
|
|
1907
|
+
const arrayName = joinMatch[1];
|
|
1908
|
+
if (!hasDynamicArrayPush(scopedText, arrayName)) return null;
|
|
1909
|
+
return {
|
|
1910
|
+
confidence: 0.6,
|
|
1911
|
+
description: "SQL query variable may be built dynamically from array concatenation."
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function isDynamicQueryBuilder(text) {
|
|
1915
|
+
const builderPatterns = [
|
|
1916
|
+
/buildQuery\s*\(/i,
|
|
1917
|
+
/buildSql\s*\(/i,
|
|
1918
|
+
/createQuery\s*\(/i,
|
|
1919
|
+
/makeQuery\s*\(/i,
|
|
1920
|
+
/generateSql\s*\(/i,
|
|
1921
|
+
/formatQuery\s*\(/i
|
|
1922
|
+
];
|
|
1923
|
+
return builderPatterns.some((p) => p.test(text));
|
|
1924
|
+
}
|
|
1925
|
+
var SQL_EXECUTION_METHODS = ["query", "execute", "raw", "unsafe", "sql", "runQuery"];
|
|
1926
|
+
var DB_CALLER_PATTERN = /\b(?:db|client|pool|connection|knex|prisma|drizzle|supabase|pg|postgres|conn|database)\b/i;
|
|
1927
|
+
var SQL_SAFE_PATTERNS = [/^sql`/, /^sql\.raw\(sql`/, /\$\d+/, /\?/, /:[\w]+/];
|
|
1928
|
+
var CLIENT_FILE_PATTERNS = [/\/hooks\//i, /\/components\//i, /use[A-Z]\w*\.tsx?$/];
|
|
1929
|
+
var ROUTE_METHODS = [
|
|
1930
|
+
"get",
|
|
1931
|
+
"post",
|
|
1932
|
+
"put",
|
|
1933
|
+
"patch",
|
|
1934
|
+
"delete",
|
|
1935
|
+
"app.get",
|
|
1936
|
+
"app.post",
|
|
1937
|
+
"app.put",
|
|
1938
|
+
"app.patch",
|
|
1939
|
+
"app.delete",
|
|
1940
|
+
"router.get",
|
|
1941
|
+
"router.post"
|
|
1942
|
+
];
|
|
1943
|
+
var AUTH_MIDDLEWARE_PATTERNS = [
|
|
1944
|
+
/\bauth(?:enticate)?(?:Middleware)?\b/i,
|
|
1945
|
+
/\bisAuthenticated\b/i,
|
|
1946
|
+
/\brequireAuth\b/i,
|
|
1947
|
+
/\bprotect(?:ed)?(?:Route)?\b/i,
|
|
1948
|
+
/\bverify(?:Token|JWT|Session)\b/i,
|
|
1949
|
+
/\bcheck(?:Auth|Token|Session)\b/i,
|
|
1950
|
+
/\bguard\b/i,
|
|
1951
|
+
/\bmiddleware\(/i,
|
|
1952
|
+
/\buse\s*\(\s*auth/i
|
|
1953
|
+
];
|
|
1954
|
+
var PUBLIC_ROUTE_PATTERNS = [
|
|
1955
|
+
/\/public\//i,
|
|
1956
|
+
/\/health(?:check)?(?:\/|$)/i,
|
|
1957
|
+
/\/(?:login|logout|signin|signout)(?:\/|$)/i,
|
|
1958
|
+
/\/(?:signup|register)(?:\/|$)/i,
|
|
1959
|
+
/\/(?:forgot|reset)-?password/i,
|
|
1960
|
+
/\/callback(?:\/|$)/i,
|
|
1961
|
+
/\/webhook(?:s)?(?:\/|$)/i,
|
|
1962
|
+
/\/api\/v\d+\/public\//i,
|
|
1963
|
+
/\/\.well-known\//i,
|
|
1964
|
+
/\/robots\.txt/i,
|
|
1965
|
+
/\/favicon/i,
|
|
1966
|
+
/\/status(?:\/|$)/i,
|
|
1967
|
+
/\/ping(?:\/|$)/i,
|
|
1968
|
+
/\/version(?:\/|$)/i
|
|
1969
|
+
];
|
|
1970
|
+
function isSqlExecutionCall(expressionText) {
|
|
1971
|
+
const isSqlMethod = SQL_EXECUTION_METHODS.some(
|
|
1972
|
+
(method) => expressionText.endsWith(`.${method}`) || expressionText === method
|
|
1973
|
+
);
|
|
1974
|
+
const isDbExec = expressionText.endsWith(".exec") && DB_CALLER_PATTERN.test(expressionText);
|
|
1975
|
+
return isSqlMethod || isDbExec;
|
|
1976
|
+
}
|
|
1977
|
+
function isSafeSqlArgument(argText) {
|
|
1978
|
+
return SQL_SAFE_PATTERNS.some((pattern) => pattern.test(argText));
|
|
1979
|
+
}
|
|
1980
|
+
function classifyDynamicSqlNode(node) {
|
|
1981
|
+
const text = node.getText();
|
|
1982
|
+
if (Node.isTemplateExpression(node) && hasUserInputInterpolation(text)) {
|
|
1983
|
+
return {
|
|
1984
|
+
confidence: 0.9,
|
|
1985
|
+
description: "Template literal with user input in SQL query."
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
if (Node.isBinaryExpression(node) && hasUserInputInterpolation(text)) {
|
|
1989
|
+
return {
|
|
1990
|
+
confidence: 0.85,
|
|
1991
|
+
description: "String concatenation with user input in SQL query."
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
if (isDynamicQueryBuilder(text)) {
|
|
1995
|
+
return {
|
|
1996
|
+
confidence: 0.7,
|
|
1997
|
+
description: "SQL query built by function that may use string concatenation."
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
if (hasArrayBasedSqlBuilding(text)) {
|
|
2001
|
+
return {
|
|
2002
|
+
confidence: 0.6,
|
|
2003
|
+
description: "SQL query variable may be built dynamically from array concatenation."
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
function findIdentifierSqlConstruction(firstArg) {
|
|
2009
|
+
if (!Node.isIdentifier(firstArg)) return null;
|
|
2010
|
+
const identifierName = firstArg.getText();
|
|
2011
|
+
const definitionNodes = firstArg.getDefinitions().map((definition) => definition.getNode()).filter((node) => node !== void 0);
|
|
2012
|
+
const sourceFile = firstArg.getSourceFile();
|
|
2013
|
+
const definitionPositions = buildDefinitionPositionsFromNodes(definitionNodes);
|
|
2014
|
+
for (const definitionNode of definitionNodes) {
|
|
2015
|
+
const risk = collectIdentifierDefinitionRisk(definitionNode, sourceFile);
|
|
2016
|
+
if (risk) return risk;
|
|
2017
|
+
}
|
|
2018
|
+
const assignmentRisk = detectAssignmentRisk(sourceFile, identifierName, definitionPositions);
|
|
2019
|
+
if (assignmentRisk) return assignmentRisk;
|
|
2020
|
+
const directMatchRisk = detectIdentifierJoinPattern(firstArg, identifierName, sourceFile);
|
|
2021
|
+
if (directMatchRisk) {
|
|
2022
|
+
return directMatchRisk;
|
|
2023
|
+
}
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
function evaluateSqlInjectionRisk(firstArg) {
|
|
2027
|
+
const argText = firstArg.getText();
|
|
2028
|
+
if (isSafeSqlArgument(argText)) {
|
|
2029
|
+
return { isDangerous: false, confidence: 0, description: "" };
|
|
2030
|
+
}
|
|
2031
|
+
const directRisk = classifyDynamicSqlNode(firstArg);
|
|
2032
|
+
if (directRisk) {
|
|
2033
|
+
return {
|
|
2034
|
+
isDangerous: true,
|
|
2035
|
+
confidence: directRisk.confidence,
|
|
2036
|
+
description: directRisk.description
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
const identifierRisk = findIdentifierSqlConstruction(firstArg);
|
|
2040
|
+
if (identifierRisk) {
|
|
2041
|
+
return {
|
|
2042
|
+
isDangerous: true,
|
|
2043
|
+
confidence: identifierRisk.confidence,
|
|
2044
|
+
description: identifierRisk.description
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
return { isDangerous: false, confidence: 0, description: "" };
|
|
2048
|
+
}
|
|
2049
|
+
function isClientSideRouteFile(filePath) {
|
|
2050
|
+
return CLIENT_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
2051
|
+
}
|
|
2052
|
+
function isRouteMethodCall(expressionText) {
|
|
2053
|
+
return ROUTE_METHODS.some(
|
|
2054
|
+
(method) => expressionText === method || expressionText.endsWith(`.${method.split(".").pop()}`)
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
function hasHookOrEventWrapper(node) {
|
|
2058
|
+
let current = node.getParent();
|
|
2059
|
+
let depth = 0;
|
|
2060
|
+
while (current && depth < 10) {
|
|
2061
|
+
if (Node.isCallExpression(current)) {
|
|
2062
|
+
const callee = current.getExpression().getText();
|
|
2063
|
+
if (/^use[A-Z]/.test(callee)) return true;
|
|
2064
|
+
}
|
|
2065
|
+
if (Node.isPropertyAssignment(current)) {
|
|
2066
|
+
const name = current.getName();
|
|
2067
|
+
if (/^on[A-Z]/.test(name)) return true;
|
|
2068
|
+
}
|
|
2069
|
+
current = current.getParent();
|
|
2070
|
+
depth += 1;
|
|
2071
|
+
}
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
function hasAuthMiddlewareArg(args) {
|
|
2075
|
+
const middlewareArgs = args.slice(1, -1);
|
|
2076
|
+
return middlewareArgs.some((arg) => {
|
|
2077
|
+
const argText = arg.getText();
|
|
2078
|
+
return AUTH_MIDDLEWARE_PATTERNS.some((pattern) => pattern.test(argText));
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
function hasInlineAuthCheck(lastArg, fullText) {
|
|
2082
|
+
const handlerText = lastArg.getText();
|
|
2083
|
+
const handlerPatterns = [
|
|
2084
|
+
/headers\.authorization/i,
|
|
2085
|
+
/getToken\s*\(/i,
|
|
2086
|
+
/session\./i,
|
|
2087
|
+
/req\.user\b/i,
|
|
2088
|
+
/ctx\.(?:user|auth|session)\b/i,
|
|
2089
|
+
/throw.*(?:Unauthorized|401)/i
|
|
2090
|
+
];
|
|
2091
|
+
return handlerPatterns.some((pattern) => pattern.test(handlerText)) || AUTH_MIDDLEWARE_PATTERNS.some((pattern) => pattern.test(fullText));
|
|
2092
|
+
}
|
|
2093
|
+
function isPublicRoute(fullText) {
|
|
2094
|
+
return PUBLIC_ROUTE_PATTERNS.some((pattern) => pattern.test(fullText));
|
|
2095
|
+
}
|
|
2096
|
+
function buildAuthConfidence(expressionText, fullText) {
|
|
2097
|
+
let confidence = 0.4;
|
|
2098
|
+
if (/\/(?:api|admin|user|account|setting|profile)/i.test(fullText)) confidence = 0.6;
|
|
2099
|
+
if (/\.(post|put|patch|delete)\s*\(/i.test(expressionText)) confidence += 0.1;
|
|
2100
|
+
return confidence;
|
|
2101
|
+
}
|
|
2102
|
+
var TS_RULES = [
|
|
2103
|
+
// SQL Injection Detection
|
|
2104
|
+
{
|
|
2105
|
+
id: "injection/sql",
|
|
2106
|
+
name: "SQL Injection",
|
|
2107
|
+
detect: (node, context) => {
|
|
2108
|
+
if (!Node.isCallExpression(node)) return null;
|
|
2109
|
+
const expression = node.getExpression();
|
|
2110
|
+
const expressionText = expression.getText();
|
|
2111
|
+
if (!isSqlExecutionCall(expressionText)) return null;
|
|
2112
|
+
const args = node.getArguments();
|
|
2113
|
+
if (args.length === 0) return null;
|
|
2114
|
+
const firstArg = args[0];
|
|
2115
|
+
const risk = evaluateSqlInjectionRisk(firstArg);
|
|
2116
|
+
if (!risk.isDangerous) return null;
|
|
2117
|
+
return {
|
|
2118
|
+
ruleId: "injection/sql",
|
|
2119
|
+
severity: "critical",
|
|
2120
|
+
title: "Potential SQL Injection",
|
|
2121
|
+
description: `${risk.description} Use parameterized queries to prevent SQL injection.`,
|
|
2122
|
+
location: getLocation(node, context.filePath),
|
|
2123
|
+
snippet: getSnippet(node),
|
|
2124
|
+
fix: {
|
|
2125
|
+
description: "Use parameterized queries or Drizzle ORM",
|
|
2126
|
+
replacement: "Use db.select().from(table).where(eq(column, value))"
|
|
2127
|
+
},
|
|
2128
|
+
cweId: CWE.SQL_INJECTION,
|
|
2129
|
+
owaspCategory: OWASP_2021.INJECTION,
|
|
2130
|
+
confidence: risk.confidence
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
},
|
|
2134
|
+
// XSS Detection
|
|
2135
|
+
{
|
|
2136
|
+
id: "injection/xss",
|
|
2137
|
+
name: "Cross-Site Scripting (XSS)",
|
|
2138
|
+
detect: (node, context) => {
|
|
2139
|
+
if (!Node.isJsxAttribute(node)) return null;
|
|
2140
|
+
const nameNode = node.getNameNode();
|
|
2141
|
+
const name = nameNode.getText();
|
|
2142
|
+
if (name !== "dangerouslySetInnerHTML") return null;
|
|
2143
|
+
const initializer = node.getInitializer();
|
|
2144
|
+
if (initializer) {
|
|
2145
|
+
const text = initializer.getText();
|
|
2146
|
+
if (text.includes("DOMPurify") || text.includes("sanitize") || text.includes("escape")) {
|
|
2147
|
+
return null;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return {
|
|
2151
|
+
ruleId: "injection/xss",
|
|
2152
|
+
severity: "high",
|
|
2153
|
+
title: "Potential XSS Vulnerability",
|
|
2154
|
+
description: "dangerouslySetInnerHTML used without sanitization. Use DOMPurify or similar library.",
|
|
2155
|
+
location: getLocation(node, context.filePath),
|
|
2156
|
+
snippet: getSnippet(node),
|
|
2157
|
+
fix: {
|
|
2158
|
+
description: "Sanitize HTML before rendering",
|
|
2159
|
+
replacement: "dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}"
|
|
2160
|
+
},
|
|
2161
|
+
cweId: CWE.XSS,
|
|
2162
|
+
owaspCategory: OWASP_2021.INJECTION,
|
|
2163
|
+
confidence: 0.9
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
},
|
|
2167
|
+
// Command Injection Detection
|
|
2168
|
+
{
|
|
2169
|
+
id: "injection/command",
|
|
2170
|
+
name: "Command Injection",
|
|
2171
|
+
detect: (node, context) => {
|
|
2172
|
+
const NON_RUNTIME_PATH_PATTERNS = [
|
|
2173
|
+
/\/scripts\//i,
|
|
2174
|
+
/\/tools\//i,
|
|
2175
|
+
/\/bin\//i,
|
|
2176
|
+
/\.config\./i,
|
|
2177
|
+
/\/__tests__\//i,
|
|
2178
|
+
/\.test\./i,
|
|
2179
|
+
/\.spec\./i,
|
|
2180
|
+
/\/migrations?\//i,
|
|
2181
|
+
/\/seeds?\//i
|
|
2182
|
+
];
|
|
2183
|
+
if (!Node.isCallExpression(node)) return null;
|
|
2184
|
+
const expression = node.getExpression();
|
|
2185
|
+
const expressionText = expression.getText();
|
|
2186
|
+
const dangerousMethods = [
|
|
2187
|
+
"exec",
|
|
2188
|
+
"execSync",
|
|
2189
|
+
"spawn",
|
|
2190
|
+
"spawnSync",
|
|
2191
|
+
"execFile",
|
|
2192
|
+
"execFileSync"
|
|
2193
|
+
];
|
|
2194
|
+
const isDangerous = dangerousMethods.some(
|
|
2195
|
+
(m) => expressionText === m || expressionText.endsWith(`.${m}`)
|
|
2196
|
+
);
|
|
2197
|
+
if (!isDangerous) return null;
|
|
2198
|
+
const args = node.getArguments();
|
|
2199
|
+
if (args.length === 0) return null;
|
|
2200
|
+
const firstArg = args[0];
|
|
2201
|
+
if (Node.isTemplateExpression(firstArg) || Node.isBinaryExpression(firstArg)) {
|
|
2202
|
+
const isNonRuntime = NON_RUNTIME_PATH_PATTERNS.some((p) => p.test(context.filePath));
|
|
2203
|
+
return {
|
|
2204
|
+
ruleId: "injection/command",
|
|
2205
|
+
severity: isNonRuntime ? "medium" : "critical",
|
|
2206
|
+
title: "Potential Command Injection",
|
|
2207
|
+
description: "User input in command execution detected. Use parameterized commands or input validation.",
|
|
2208
|
+
location: getLocation(node, context.filePath),
|
|
2209
|
+
snippet: getSnippet(node),
|
|
2210
|
+
fix: {
|
|
2211
|
+
description: "Use spawn with array arguments instead of exec with string"
|
|
2212
|
+
},
|
|
2213
|
+
cweId: CWE.COMMAND_INJECTION,
|
|
2214
|
+
owaspCategory: OWASP_2021.INJECTION,
|
|
2215
|
+
confidence: isNonRuntime ? 0.5 : 0.7
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
// Insecure Eval Detection
|
|
2222
|
+
{
|
|
2223
|
+
id: "injection/eval",
|
|
2224
|
+
name: "Insecure Eval Usage",
|
|
2225
|
+
detect: (node, context) => {
|
|
2226
|
+
if (!Node.isCallExpression(node)) return null;
|
|
2227
|
+
const expression = node.getExpression();
|
|
2228
|
+
const expressionText = expression.getText();
|
|
2229
|
+
const evalFunctions = ["eval", "Function", "setTimeout", "setInterval"];
|
|
2230
|
+
if (!evalFunctions.includes(expressionText)) return null;
|
|
2231
|
+
if (expressionText === "setTimeout" || expressionText === "setInterval") {
|
|
2232
|
+
const args = node.getArguments();
|
|
2233
|
+
if (args.length > 0 && !Node.isStringLiteral(args[0])) {
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return {
|
|
2238
|
+
ruleId: "injection/eval",
|
|
2239
|
+
severity: "high",
|
|
2240
|
+
title: "Insecure Eval Usage",
|
|
2241
|
+
description: `${expressionText} can execute arbitrary code. Avoid using eval-like functions.`,
|
|
2242
|
+
location: getLocation(node, context.filePath),
|
|
2243
|
+
snippet: getSnippet(node),
|
|
2244
|
+
fix: {
|
|
2245
|
+
description: "Use safer alternatives like JSON.parse or explicit logic"
|
|
2246
|
+
},
|
|
2247
|
+
cweId: CWE.EVAL_INJECTION,
|
|
2248
|
+
owaspCategory: OWASP_2021.INJECTION,
|
|
2249
|
+
confidence: 0.85
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
},
|
|
2253
|
+
// Missing Auth Check Detection
|
|
2254
|
+
{
|
|
2255
|
+
id: "auth/missing-check",
|
|
2256
|
+
name: "Missing Authentication Check",
|
|
2257
|
+
detect: (node, context) => {
|
|
2258
|
+
if (!Node.isCallExpression(node)) return null;
|
|
2259
|
+
if (isClientSideRouteFile(context.filePath)) return null;
|
|
2260
|
+
const expression = node.getExpression();
|
|
2261
|
+
const expressionText = expression.getText();
|
|
2262
|
+
if (!isRouteMethodCall(expressionText)) return null;
|
|
2263
|
+
if (hasHookOrEventWrapper(node)) return null;
|
|
2264
|
+
const args = node.getArguments();
|
|
2265
|
+
if (args.length < 2) return null;
|
|
2266
|
+
const fullText = node.getText();
|
|
2267
|
+
const hasAuthMiddleware = hasAuthMiddlewareArg(args);
|
|
2268
|
+
const lastArg = args[args.length - 1];
|
|
2269
|
+
const inlineAuthDetected = hasInlineAuthCheck(lastArg, fullText);
|
|
2270
|
+
const hasAuth = hasAuthMiddleware || inlineAuthDetected;
|
|
2271
|
+
const isPublic = isPublicRoute(fullText);
|
|
2272
|
+
if (!hasAuth && !isPublic) {
|
|
2273
|
+
const confidence = buildAuthConfidence(expressionText, fullText);
|
|
2274
|
+
return {
|
|
2275
|
+
ruleId: "auth/missing-check",
|
|
2276
|
+
severity: "medium",
|
|
2277
|
+
title: "Potentially Missing Authentication",
|
|
2278
|
+
description: "Route handler may be missing authentication middleware. Verify if this endpoint should be protected.",
|
|
2279
|
+
location: getLocation(node, context.filePath),
|
|
2280
|
+
snippet: getSnippet(node),
|
|
2281
|
+
fix: {
|
|
2282
|
+
description: "Add authentication middleware to protected routes"
|
|
2283
|
+
},
|
|
2284
|
+
cweId: CWE.MISSING_AUTH,
|
|
2285
|
+
owaspCategory: OWASP_2021.AUTH_FAILURES,
|
|
2286
|
+
confidence
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
return null;
|
|
2290
|
+
}
|
|
2291
|
+
},
|
|
2292
|
+
// Sensitive Data Logging Detection
|
|
2293
|
+
{
|
|
2294
|
+
id: "secret/logging",
|
|
2295
|
+
name: "Sensitive Data Logging",
|
|
2296
|
+
detect: (node, context) => {
|
|
2297
|
+
if (!Node.isCallExpression(node)) return null;
|
|
2298
|
+
const expression = node.getExpression();
|
|
2299
|
+
const expressionText = expression.getText();
|
|
2300
|
+
const logMethods = [
|
|
2301
|
+
"console.log",
|
|
2302
|
+
"console.info",
|
|
2303
|
+
"console.debug",
|
|
2304
|
+
"console.warn",
|
|
2305
|
+
"console.error",
|
|
2306
|
+
"logger.log",
|
|
2307
|
+
"logger.info",
|
|
2308
|
+
"logger.debug"
|
|
2309
|
+
];
|
|
2310
|
+
if (!logMethods.includes(expressionText)) return null;
|
|
2311
|
+
const args = node.getArguments();
|
|
2312
|
+
for (const arg of args) {
|
|
2313
|
+
const text = arg.getText().toLowerCase();
|
|
2314
|
+
const sensitivePatterns = [
|
|
2315
|
+
"password",
|
|
2316
|
+
"secret",
|
|
2317
|
+
"token",
|
|
2318
|
+
"apikey",
|
|
2319
|
+
"api_key",
|
|
2320
|
+
"authorization",
|
|
2321
|
+
"bearer",
|
|
2322
|
+
"credential",
|
|
2323
|
+
"private"
|
|
2324
|
+
];
|
|
2325
|
+
if (sensitivePatterns.some((p) => text.includes(p))) {
|
|
2326
|
+
return {
|
|
2327
|
+
ruleId: "secret/logging",
|
|
2328
|
+
severity: "high",
|
|
2329
|
+
title: "Sensitive Data in Logs",
|
|
2330
|
+
description: "Logging statement may contain sensitive data.",
|
|
2331
|
+
location: getLocation(node, context.filePath),
|
|
2332
|
+
snippet: getSnippet(node),
|
|
2333
|
+
fix: {
|
|
2334
|
+
description: "Remove sensitive data from log statements or redact"
|
|
2335
|
+
},
|
|
2336
|
+
cweId: CWE.SENSITIVE_LOG,
|
|
2337
|
+
owaspCategory: OWASP_2021.LOGGING_FAILURES,
|
|
2338
|
+
confidence: 0.7
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
2345
|
+
// Insecure Randomness Detection
|
|
2346
|
+
{
|
|
2347
|
+
id: "crypto/weak-random",
|
|
2348
|
+
name: "Insecure Randomness",
|
|
2349
|
+
detect: (node, context) => {
|
|
2350
|
+
if (!Node.isCallExpression(node)) return null;
|
|
2351
|
+
const expression = node.getExpression();
|
|
2352
|
+
const expressionText = expression.getText();
|
|
2353
|
+
if (expressionText !== "Math.random") return null;
|
|
2354
|
+
const parent = node.getParent();
|
|
2355
|
+
const grandparent = parent?.getParent();
|
|
2356
|
+
const contextText = (grandparent?.getText() || parent?.getText() || "").toLowerCase();
|
|
2357
|
+
const securityContexts = [
|
|
2358
|
+
"token",
|
|
2359
|
+
"secret",
|
|
2360
|
+
"password",
|
|
2361
|
+
"session",
|
|
2362
|
+
"id",
|
|
2363
|
+
"key",
|
|
2364
|
+
"nonce",
|
|
2365
|
+
"salt"
|
|
2366
|
+
];
|
|
2367
|
+
if (securityContexts.some((c) => contextText.includes(c))) {
|
|
2368
|
+
return {
|
|
2369
|
+
ruleId: "crypto/weak-random",
|
|
2370
|
+
severity: "medium",
|
|
2371
|
+
title: "Insecure Randomness for Security Context",
|
|
2372
|
+
description: "Math.random() is not cryptographically secure. Use crypto.randomBytes() or crypto.randomUUID().",
|
|
2373
|
+
location: getLocation(node, context.filePath),
|
|
2374
|
+
snippet: getSnippet(node),
|
|
2375
|
+
fix: {
|
|
2376
|
+
description: "Use crypto.randomBytes() or crypto.randomUUID()",
|
|
2377
|
+
replacement: "crypto.randomUUID() or crypto.randomBytes(32).toString('hex')"
|
|
2378
|
+
},
|
|
2379
|
+
cweId: CWE.WEAK_RANDOM,
|
|
2380
|
+
owaspCategory: OWASP_2021.CRYPTO_FAILURES,
|
|
2381
|
+
confidence: 0.6
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
];
|
|
2388
|
+
var TypeScriptAnalyzer = class {
|
|
2389
|
+
name = "TypeScriptAnalyzer";
|
|
2390
|
+
categories = ["injection", "auth", "crypto", "secret"];
|
|
2391
|
+
async analyze(options) {
|
|
2392
|
+
const findings = [];
|
|
2393
|
+
const files = await glob(["**/*.ts", "**/*.tsx"], {
|
|
2394
|
+
cwd: options.rootDir,
|
|
2395
|
+
ignore: options.exclude || ["**/node_modules/**", "**/dist/**"],
|
|
2396
|
+
absolute: true
|
|
2397
|
+
});
|
|
2398
|
+
const project = new Project({
|
|
2399
|
+
skipAddingFilesFromTsConfig: true,
|
|
2400
|
+
compilerOptions: {
|
|
2401
|
+
allowJs: true,
|
|
2402
|
+
checkJs: false,
|
|
2403
|
+
noEmit: true,
|
|
2404
|
+
skipLibCheck: true
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
for (const filePath of files) {
|
|
2408
|
+
try {
|
|
2409
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
2410
|
+
const context = { filePath, project };
|
|
2411
|
+
sourceFile.forEachDescendant((node) => {
|
|
2412
|
+
for (const rule of TS_RULES) {
|
|
2413
|
+
const finding = rule.detect(node, context);
|
|
2414
|
+
if (finding) {
|
|
2415
|
+
findings.push(finding);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
project.removeSourceFile(sourceFile);
|
|
2420
|
+
} catch {
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return findings;
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
|
|
2427
|
+
// src/internal/vuln-checker/config/loader.ts
|
|
2428
|
+
init_esm_shims();
|
|
2429
|
+
|
|
2430
|
+
// src/internal/vuln-checker/security/path-validation.ts
|
|
2431
|
+
init_esm_shims();
|
|
2432
|
+
function containsPathTraversal(inputPath) {
|
|
2433
|
+
const normalized = path2.normalize(inputPath);
|
|
2434
|
+
if (normalized.includes("..")) return true;
|
|
2435
|
+
if (inputPath.includes("\0")) return true;
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
function isPathWithinBoundary(filePath, boundaryDir) {
|
|
2439
|
+
try {
|
|
2440
|
+
const resolvedFile = path2.resolve(filePath);
|
|
2441
|
+
const resolvedBoundary = path2.resolve(boundaryDir);
|
|
2442
|
+
const relative = path2.relative(resolvedBoundary, resolvedFile);
|
|
2443
|
+
return !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
2444
|
+
} catch {
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
function resolvePathWithinBoundary(inputPath, boundaryDir) {
|
|
2449
|
+
if (containsPathTraversal(inputPath)) {
|
|
2450
|
+
throw new Error(`Path contains traversal patterns: ${inputPath}`);
|
|
2451
|
+
}
|
|
2452
|
+
const resolvedPath = path2.isAbsolute(inputPath) ? inputPath : path2.resolve(boundaryDir, inputPath);
|
|
2453
|
+
if (!isPathWithinBoundary(resolvedPath, boundaryDir)) {
|
|
2454
|
+
throw new Error(`Path is outside allowed boundary: ${inputPath}`);
|
|
2455
|
+
}
|
|
2456
|
+
return resolvedPath;
|
|
2457
|
+
}
|
|
2458
|
+
function filterPathsWithinBoundary(files, boundaryDir) {
|
|
2459
|
+
const resolvedBoundary = path2.resolve(boundaryDir);
|
|
2460
|
+
return files.filter((file) => isPathWithinBoundary(file, resolvedBoundary));
|
|
2461
|
+
}
|
|
2462
|
+
function validateGlobPatterns(patterns) {
|
|
2463
|
+
for (const pattern of patterns) {
|
|
2464
|
+
if (containsPathTraversal(pattern)) {
|
|
2465
|
+
throw new Error(`Glob pattern contains path traversal: ${pattern}`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
// src/internal/vuln-checker/config/loader.ts
|
|
2471
|
+
async function loadConfig(configPath, rootDir) {
|
|
2472
|
+
const fullPath = resolvePathWithinBoundary(configPath, rootDir);
|
|
2473
|
+
try {
|
|
2474
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
2475
|
+
if (fullPath.endsWith(".yml") || fullPath.endsWith(".yaml")) {
|
|
2476
|
+
return yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
2477
|
+
}
|
|
2478
|
+
return JSON.parse(content);
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
if (error.code === "ENOENT") {
|
|
2481
|
+
return { version: 1 };
|
|
2482
|
+
}
|
|
2483
|
+
throw error;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
async function loadIgnores(ignorePath, rootDir) {
|
|
2487
|
+
const fullPath = resolvePathWithinBoundary(ignorePath, rootDir);
|
|
2488
|
+
try {
|
|
2489
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
2490
|
+
let parsed;
|
|
2491
|
+
if (fullPath.endsWith(".yml") || fullPath.endsWith(".yaml")) {
|
|
2492
|
+
parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
2493
|
+
} else {
|
|
2494
|
+
parsed = JSON.parse(content);
|
|
2495
|
+
}
|
|
2496
|
+
const ignores = parsed?.ignores || [];
|
|
2497
|
+
const now = /* @__PURE__ */ new Date();
|
|
2498
|
+
return ignores.filter((ignore) => {
|
|
2499
|
+
if (!ignore.expires) return true;
|
|
2500
|
+
const expiresDate = new Date(ignore.expires);
|
|
2501
|
+
return expiresDate > now;
|
|
2502
|
+
});
|
|
2503
|
+
} catch (error) {
|
|
2504
|
+
if (error.code === "ENOENT") {
|
|
2505
|
+
return [];
|
|
2506
|
+
}
|
|
2507
|
+
throw error;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/internal/vuln-checker/ignore/matcher.ts
|
|
2512
|
+
init_esm_shims();
|
|
2513
|
+
function normalizePath(filePath) {
|
|
2514
|
+
const normalized = path2.normalize(filePath);
|
|
2515
|
+
return normalized.replace(/\\/g, "/");
|
|
2516
|
+
}
|
|
2517
|
+
function normalizeRuleId(ruleId) {
|
|
2518
|
+
return ruleId.toLowerCase().replace(/_/g, "-");
|
|
2519
|
+
}
|
|
2520
|
+
function matchIgnoreRule(rule, finding) {
|
|
2521
|
+
if (rule.rule !== "*") {
|
|
2522
|
+
const normalizedRule = normalizeRuleId(rule.rule);
|
|
2523
|
+
const normalizedFinding = normalizeRuleId(finding.ruleId);
|
|
2524
|
+
if (normalizedRule !== normalizedFinding) {
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (rule.path) {
|
|
2529
|
+
const normalizedPath = normalizePath(finding.location.file);
|
|
2530
|
+
const normalizedPattern = normalizePath(rule.path);
|
|
2531
|
+
if (!minimatch(normalizedPath, normalizedPattern, { nocase: true })) {
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
var RULE_ID_PATTERN = "[a-zA-Z0-9\\-_\\/\\*]+";
|
|
2538
|
+
function createAnnotationPatterns() {
|
|
2539
|
+
const commentPrefixes = [
|
|
2540
|
+
"\\/\\/",
|
|
2541
|
+
// JS single-line: //
|
|
2542
|
+
"\\/\\*\\s*",
|
|
2543
|
+
// JS block: /*
|
|
2544
|
+
"#",
|
|
2545
|
+
// Shell/YAML/SQL: #
|
|
2546
|
+
"--"
|
|
2547
|
+
// SQL: --
|
|
2548
|
+
].join("|");
|
|
2549
|
+
const basePattern = (type) => new RegExp(
|
|
2550
|
+
`(?:${commentPrefixes})\\s*@vuln-ignore${type}:(${RULE_ID_PATTERN})(?:\\s*-\\s*(.+?))?(?:\\s*\\*\\/)?$`,
|
|
2551
|
+
"i"
|
|
2552
|
+
);
|
|
2553
|
+
return {
|
|
2554
|
+
line: basePattern(""),
|
|
2555
|
+
nextLine: basePattern("-next-line"),
|
|
2556
|
+
file: basePattern("-file")
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
var ANNOTATION_PATTERNS = createAnnotationPatterns();
|
|
2560
|
+
async function parseInlineAnnotations(filePath) {
|
|
2561
|
+
const annotations = [];
|
|
2562
|
+
try {
|
|
2563
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
2564
|
+
const lines = content.split("\n");
|
|
2565
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2566
|
+
const line = lines[i];
|
|
2567
|
+
const lineNumber = i + 1;
|
|
2568
|
+
const fileMatch = line.match(ANNOTATION_PATTERNS.file);
|
|
2569
|
+
if (fileMatch) {
|
|
2570
|
+
annotations.push({
|
|
2571
|
+
// Normalize rule ID for consistent matching
|
|
2572
|
+
ruleId: normalizeRuleId(fileMatch[1]),
|
|
2573
|
+
reason: fileMatch[2]?.trim(),
|
|
2574
|
+
scope: "file",
|
|
2575
|
+
line: lineNumber
|
|
2576
|
+
});
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
const nextLineMatch = line.match(ANNOTATION_PATTERNS.nextLine);
|
|
2580
|
+
if (nextLineMatch) {
|
|
2581
|
+
annotations.push({
|
|
2582
|
+
ruleId: normalizeRuleId(nextLineMatch[1]),
|
|
2583
|
+
reason: nextLineMatch[2]?.trim(),
|
|
2584
|
+
scope: "next-line",
|
|
2585
|
+
line: lineNumber
|
|
2586
|
+
});
|
|
2587
|
+
continue;
|
|
2588
|
+
}
|
|
2589
|
+
const lineMatch = line.match(ANNOTATION_PATTERNS.line);
|
|
2590
|
+
if (lineMatch) {
|
|
2591
|
+
annotations.push({
|
|
2592
|
+
ruleId: normalizeRuleId(lineMatch[1]),
|
|
2593
|
+
reason: lineMatch[2]?.trim(),
|
|
2594
|
+
scope: "line",
|
|
2595
|
+
line: lineNumber
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
return annotations;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// src/internal/vuln-checker/reporters/console-reporter.ts
|
|
2605
|
+
init_esm_shims();
|
|
2606
|
+
var COLORS = {
|
|
2607
|
+
reset: "\x1B[0m",
|
|
2608
|
+
bold: "\x1B[1m",
|
|
2609
|
+
dim: "\x1B[2m",
|
|
2610
|
+
red: "\x1B[31m",
|
|
2611
|
+
green: "\x1B[32m",
|
|
2612
|
+
yellow: "\x1B[33m",
|
|
2613
|
+
blue: "\x1B[34m",
|
|
2614
|
+
magenta: "\x1B[35m",
|
|
2615
|
+
cyan: "\x1B[36m",
|
|
2616
|
+
white: "\x1B[37m",
|
|
2617
|
+
bgRed: "\x1B[41m"};
|
|
2618
|
+
function getSeverityColor(severity) {
|
|
2619
|
+
switch (severity) {
|
|
2620
|
+
case "critical":
|
|
2621
|
+
return COLORS.bgRed + COLORS.white;
|
|
2622
|
+
case "high":
|
|
2623
|
+
return COLORS.red;
|
|
2624
|
+
case "medium":
|
|
2625
|
+
return COLORS.yellow;
|
|
2626
|
+
case "low":
|
|
2627
|
+
return COLORS.cyan;
|
|
2628
|
+
case "info":
|
|
2629
|
+
return COLORS.blue;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
function formatSeverity(severity) {
|
|
2633
|
+
const color = getSeverityColor(severity);
|
|
2634
|
+
const label = severity.toUpperCase().padEnd(8);
|
|
2635
|
+
return `${color}${label}${COLORS.reset}`;
|
|
2636
|
+
}
|
|
2637
|
+
function formatFinding(finding, index) {
|
|
2638
|
+
const lines = [];
|
|
2639
|
+
lines.push(`
|
|
2640
|
+
${COLORS.bold}${index}. ${finding.title}${COLORS.reset}`);
|
|
2641
|
+
lines.push(
|
|
2642
|
+
` ${formatSeverity(finding.severity)} ${COLORS.dim}${finding.ruleId}${COLORS.reset}`
|
|
2643
|
+
);
|
|
2644
|
+
const shortPath = finding.location.file.split("/").slice(-3).join("/");
|
|
2645
|
+
lines.push(
|
|
2646
|
+
` ${COLORS.cyan}${shortPath}:${finding.location.line}:${finding.location.column}${COLORS.reset}`
|
|
2647
|
+
);
|
|
2648
|
+
lines.push(` ${finding.description}`);
|
|
2649
|
+
if (finding.snippet) {
|
|
2650
|
+
lines.push(` ${COLORS.dim}\u2502${COLORS.reset}`);
|
|
2651
|
+
const snippetLines = finding.snippet.text.split("\n");
|
|
2652
|
+
for (const snippetLine of snippetLines.slice(0, 3)) {
|
|
2653
|
+
lines.push(` ${COLORS.dim}\u2502${COLORS.reset} ${snippetLine}`);
|
|
2654
|
+
}
|
|
2655
|
+
if (snippetLines.length > 3) {
|
|
2656
|
+
lines.push(` ${COLORS.dim}\u2502 ... (${snippetLines.length - 3} more lines)${COLORS.reset}`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
if (finding.fix) {
|
|
2660
|
+
lines.push(` ${COLORS.green}Fix:${COLORS.reset} ${finding.fix.description}`);
|
|
2661
|
+
}
|
|
2662
|
+
return lines.join("\n");
|
|
2663
|
+
}
|
|
2664
|
+
var ConsoleReporter = class {
|
|
2665
|
+
name = "console";
|
|
2666
|
+
format(result) {
|
|
2667
|
+
const lines = [];
|
|
2668
|
+
lines.push(
|
|
2669
|
+
`
|
|
2670
|
+
${COLORS.bold}${COLORS.magenta}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${COLORS.reset}`
|
|
2671
|
+
);
|
|
2672
|
+
lines.push(
|
|
2673
|
+
`${COLORS.bold}${COLORS.magenta} VULNERABILITY SCAN RESULTS ${COLORS.reset}`
|
|
2674
|
+
);
|
|
2675
|
+
lines.push(
|
|
2676
|
+
`${COLORS.bold}${COLORS.magenta}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${COLORS.reset}
|
|
2677
|
+
`
|
|
2678
|
+
);
|
|
2679
|
+
lines.push(`${COLORS.bold}Summary${COLORS.reset}`);
|
|
2680
|
+
lines.push(`${"\u2500".repeat(40)}`);
|
|
2681
|
+
if (result.summary.critical > 0) {
|
|
2682
|
+
lines.push(` ${formatSeverity("critical")} ${result.summary.critical}`);
|
|
2683
|
+
}
|
|
2684
|
+
if (result.summary.high > 0) {
|
|
2685
|
+
lines.push(` ${formatSeverity("high")} ${result.summary.high}`);
|
|
2686
|
+
}
|
|
2687
|
+
if (result.summary.medium > 0) {
|
|
2688
|
+
lines.push(` ${formatSeverity("medium")} ${result.summary.medium}`);
|
|
2689
|
+
}
|
|
2690
|
+
if (result.summary.low > 0) {
|
|
2691
|
+
lines.push(` ${formatSeverity("low")} ${result.summary.low}`);
|
|
2692
|
+
}
|
|
2693
|
+
if (result.summary.info > 0) {
|
|
2694
|
+
lines.push(` ${formatSeverity("info")} ${result.summary.info}`);
|
|
2695
|
+
}
|
|
2696
|
+
lines.push(`${"\u2500".repeat(40)}`);
|
|
2697
|
+
lines.push(` ${COLORS.bold}Total:${COLORS.reset} ${result.summary.total} findings`);
|
|
2698
|
+
if (result.summary.ignored > 0) {
|
|
2699
|
+
lines.push(` ${COLORS.dim}Ignored: ${result.summary.ignored}${COLORS.reset}`);
|
|
2700
|
+
}
|
|
2701
|
+
lines.push(
|
|
2702
|
+
` ${COLORS.dim}Scanned ${result.metadata.filesScanned} files in ${result.metadata.duration}ms${COLORS.reset}`
|
|
2703
|
+
);
|
|
2704
|
+
if (result.findings.length > 0) {
|
|
2705
|
+
lines.push(`
|
|
2706
|
+
${COLORS.bold}Findings${COLORS.reset}`);
|
|
2707
|
+
lines.push(`${"\u2500".repeat(40)}`);
|
|
2708
|
+
const sorted = [...result.findings].sort((a, b) => {
|
|
2709
|
+
const order = ["critical", "high", "medium", "low", "info"];
|
|
2710
|
+
return order.indexOf(a.severity) - order.indexOf(b.severity);
|
|
2711
|
+
});
|
|
2712
|
+
let index = 1;
|
|
2713
|
+
for (const finding of sorted) {
|
|
2714
|
+
lines.push(formatFinding(finding, index));
|
|
2715
|
+
index++;
|
|
2716
|
+
}
|
|
2717
|
+
} else {
|
|
2718
|
+
lines.push(`
|
|
2719
|
+
${COLORS.green}${COLORS.bold}\u2713 No vulnerabilities found!${COLORS.reset}`);
|
|
2720
|
+
}
|
|
2721
|
+
lines.push(`
|
|
2722
|
+
${COLORS.dim}${"\u2500".repeat(40)}${COLORS.reset}`);
|
|
2723
|
+
lines.push(`${COLORS.dim}${TOOL.NAME} v${TOOL.VERSION}${COLORS.reset}
|
|
2724
|
+
`);
|
|
2725
|
+
return lines.join("\n");
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2728
|
+
|
|
2729
|
+
// src/internal/vuln-checker/reporters/json-reporter.ts
|
|
2730
|
+
init_esm_shims();
|
|
2731
|
+
var JsonReporter = class {
|
|
2732
|
+
name = "json";
|
|
2733
|
+
format(result) {
|
|
2734
|
+
return JSON.stringify(result, null, 2);
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
|
|
2738
|
+
// src/internal/vuln-checker/reporters/markdown-reporter.ts
|
|
2739
|
+
init_esm_shims();
|
|
2740
|
+
function escapeMarkdown(text) {
|
|
2741
|
+
let escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2742
|
+
escaped = escaped.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\*/g, "\\*").replace(/_/g, "\\_").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/#/g, "\\#").replace(/\+/g, "\\+").replace(/-/g, "\\-").replace(/\./g, "\\.").replace(/!/g, "\\!").replace(/\|/g, "\\|");
|
|
2743
|
+
return escaped;
|
|
2744
|
+
}
|
|
2745
|
+
function sanitizeCodeBlock(text) {
|
|
2746
|
+
return text.replace(/```/g, "\\`\\`\\`");
|
|
2747
|
+
}
|
|
2748
|
+
var SEVERITY_EMOJI = {
|
|
2749
|
+
critical: "\u{1F534}",
|
|
2750
|
+
high: "\u{1F7E0}",
|
|
2751
|
+
medium: "\u{1F7E1}",
|
|
2752
|
+
low: "\u{1F7E2}",
|
|
2753
|
+
info: "\u{1F535}"
|
|
2754
|
+
};
|
|
2755
|
+
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
2756
|
+
function groupByCategory(findings) {
|
|
2757
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
2758
|
+
for (const finding of findings) {
|
|
2759
|
+
const category = finding.ruleId.split("/")[0];
|
|
2760
|
+
const existing = byCategory.get(category) || [];
|
|
2761
|
+
existing.push(finding);
|
|
2762
|
+
byCategory.set(category, existing);
|
|
2763
|
+
}
|
|
2764
|
+
return byCategory;
|
|
2765
|
+
}
|
|
2766
|
+
function sortBySeverity(findings) {
|
|
2767
|
+
return [...findings].sort(
|
|
2768
|
+
(a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity)
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
function formatFilePath(filePath) {
|
|
2772
|
+
return filePath.split("/").slice(-3).join("/");
|
|
2773
|
+
}
|
|
2774
|
+
function formatMetadata(finding) {
|
|
2775
|
+
const lines = [];
|
|
2776
|
+
if (finding.cweId) {
|
|
2777
|
+
lines.push(`- **CWE**: ${finding.cweId}`);
|
|
2778
|
+
}
|
|
2779
|
+
if (finding.owaspCategory) {
|
|
2780
|
+
lines.push(`- **OWASP**: ${finding.owaspCategory}`);
|
|
2781
|
+
}
|
|
2782
|
+
return lines;
|
|
2783
|
+
}
|
|
2784
|
+
function formatSnippet(finding) {
|
|
2785
|
+
if (!finding.snippet) return [];
|
|
2786
|
+
return ["```", sanitizeCodeBlock(finding.snippet.text), "```\n"];
|
|
2787
|
+
}
|
|
2788
|
+
function formatFix(finding) {
|
|
2789
|
+
if (!finding.fix) return [];
|
|
2790
|
+
const lines = [`**Fix**: ${escapeMarkdown(finding.fix.description)}
|
|
2791
|
+
`];
|
|
2792
|
+
if (finding.fix.replacement) {
|
|
2793
|
+
lines.push("```", sanitizeCodeBlock(finding.fix.replacement), "```\n");
|
|
2794
|
+
}
|
|
2795
|
+
return lines;
|
|
2796
|
+
}
|
|
2797
|
+
function formatFinding2(finding) {
|
|
2798
|
+
const emoji = SEVERITY_EMOJI[finding.severity];
|
|
2799
|
+
const location = `${formatFilePath(finding.location.file)}:${finding.location.line}`;
|
|
2800
|
+
const safeTitle = escapeMarkdown(finding.title);
|
|
2801
|
+
const safeDescription = escapeMarkdown(finding.description);
|
|
2802
|
+
return [
|
|
2803
|
+
`#### ${emoji} ${safeTitle}
|
|
2804
|
+
`,
|
|
2805
|
+
`- **Severity**: ${finding.severity}`,
|
|
2806
|
+
`- **Location**: \`${location}\``,
|
|
2807
|
+
`- **Rule**: \`${finding.ruleId}\``,
|
|
2808
|
+
...formatMetadata(finding),
|
|
2809
|
+
`
|
|
2810
|
+
${safeDescription}
|
|
2811
|
+
`,
|
|
2812
|
+
...formatSnippet(finding),
|
|
2813
|
+
...formatFix(finding),
|
|
2814
|
+
"---\n"
|
|
2815
|
+
];
|
|
2816
|
+
}
|
|
2817
|
+
function formatCategorySection(category, findings) {
|
|
2818
|
+
const title = category.charAt(0).toUpperCase() + category.slice(1);
|
|
2819
|
+
const sorted = sortBySeverity(findings);
|
|
2820
|
+
return [`
|
|
2821
|
+
### ${title}
|
|
2822
|
+
`, ...sorted.flatMap(formatFinding2)];
|
|
2823
|
+
}
|
|
2824
|
+
function formatFindingsByCategory(findings) {
|
|
2825
|
+
const byCategory = groupByCategory(findings);
|
|
2826
|
+
const sections = Array.from(byCategory.entries()).flatMap(
|
|
2827
|
+
([category, categoryFindings]) => formatCategorySection(category, categoryFindings)
|
|
2828
|
+
);
|
|
2829
|
+
return sections.join("\n");
|
|
2830
|
+
}
|
|
2831
|
+
var MarkdownReporter = class {
|
|
2832
|
+
name = "markdown";
|
|
2833
|
+
format(result) {
|
|
2834
|
+
const lines = [];
|
|
2835
|
+
lines.push("# Security Vulnerability Report\n");
|
|
2836
|
+
lines.push(`Generated: ${result.timestamp}
|
|
2837
|
+
`);
|
|
2838
|
+
lines.push("## Summary\n");
|
|
2839
|
+
lines.push("| Severity | Count |");
|
|
2840
|
+
lines.push("|----------|-------|");
|
|
2841
|
+
lines.push(`| \u{1F534} Critical | ${result.summary.critical} |`);
|
|
2842
|
+
lines.push(`| \u{1F7E0} High | ${result.summary.high} |`);
|
|
2843
|
+
lines.push(`| \u{1F7E1} Medium | ${result.summary.medium} |`);
|
|
2844
|
+
lines.push(`| \u{1F7E2} Low | ${result.summary.low} |`);
|
|
2845
|
+
lines.push(`| \u{1F535} Info | ${result.summary.info} |`);
|
|
2846
|
+
lines.push(`| **Total** | **${result.summary.total}** |`);
|
|
2847
|
+
lines.push(`| Ignored | ${result.summary.ignored} |
|
|
2848
|
+
`);
|
|
2849
|
+
lines.push("## Scan Details\n");
|
|
2850
|
+
lines.push(`- **Duration**: ${result.metadata.duration}ms`);
|
|
2851
|
+
lines.push(`- **Files Scanned**: ${result.metadata.filesScanned}`);
|
|
2852
|
+
lines.push(`- **Root Directory**: ${result.metadata.rootDir}
|
|
2853
|
+
`);
|
|
2854
|
+
if (result.findings.length > 0) {
|
|
2855
|
+
lines.push("## Findings\n");
|
|
2856
|
+
lines.push(formatFindingsByCategory(result.findings));
|
|
2857
|
+
} else {
|
|
2858
|
+
lines.push("## Findings\n");
|
|
2859
|
+
lines.push("No vulnerabilities found! \u{1F389}\n");
|
|
2860
|
+
}
|
|
2861
|
+
if (result.ignoredFindings && result.ignoredFindings.length > 0) {
|
|
2862
|
+
lines.push("## Ignored Findings\n");
|
|
2863
|
+
lines.push("The following findings were ignored based on configuration:\n");
|
|
2864
|
+
for (const finding of result.ignoredFindings) {
|
|
2865
|
+
const emoji = SEVERITY_EMOJI[finding.severity];
|
|
2866
|
+
const safeTitle = escapeMarkdown(finding.title);
|
|
2867
|
+
lines.push(
|
|
2868
|
+
`- ${emoji} **${safeTitle}** at \`${finding.location.file}:${finding.location.line}\``
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
lines.push("");
|
|
2872
|
+
}
|
|
2873
|
+
lines.push("---");
|
|
2874
|
+
lines.push(`*Generated by [${TOOL.NAME}](${TOOL.REPO_URL})*`);
|
|
2875
|
+
return lines.join("\n");
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
|
|
2879
|
+
// src/internal/vuln-checker/reporters/sarif-reporter.ts
|
|
2880
|
+
init_esm_shims();
|
|
2881
|
+
function mapToSarifLevel(severity) {
|
|
2882
|
+
switch (severity) {
|
|
2883
|
+
case "critical":
|
|
2884
|
+
case "high":
|
|
2885
|
+
return "error";
|
|
2886
|
+
case "medium":
|
|
2887
|
+
return "warning";
|
|
2888
|
+
case "low":
|
|
2889
|
+
return "note";
|
|
2890
|
+
case "info":
|
|
2891
|
+
return "none";
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function buildRule(finding) {
|
|
2895
|
+
return {
|
|
2896
|
+
id: finding.ruleId,
|
|
2897
|
+
name: finding.title,
|
|
2898
|
+
shortDescription: {
|
|
2899
|
+
text: finding.title
|
|
2900
|
+
},
|
|
2901
|
+
fullDescription: {
|
|
2902
|
+
text: finding.description
|
|
2903
|
+
},
|
|
2904
|
+
help: {
|
|
2905
|
+
text: finding.fix?.description || "No fix available",
|
|
2906
|
+
markdown: finding.fix?.description || "No fix available"
|
|
2907
|
+
},
|
|
2908
|
+
properties: {
|
|
2909
|
+
tags: [finding.cweId, finding.owaspCategory].filter(Boolean),
|
|
2910
|
+
precision: "medium",
|
|
2911
|
+
"security-severity": severityToScore(finding.severity).toString()
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function severityToScore(severity) {
|
|
2916
|
+
return SEVERITY_SCORE[severity] ?? 1;
|
|
2917
|
+
}
|
|
2918
|
+
function buildResult(finding) {
|
|
2919
|
+
return {
|
|
2920
|
+
ruleId: finding.ruleId,
|
|
2921
|
+
level: mapToSarifLevel(finding.severity),
|
|
2922
|
+
message: {
|
|
2923
|
+
text: finding.description
|
|
2924
|
+
},
|
|
2925
|
+
locations: [
|
|
2926
|
+
{
|
|
2927
|
+
physicalLocation: {
|
|
2928
|
+
artifactLocation: {
|
|
2929
|
+
uri: finding.location.file,
|
|
2930
|
+
uriBaseId: "%SRCROOT%"
|
|
2931
|
+
},
|
|
2932
|
+
region: {
|
|
2933
|
+
startLine: finding.location.line,
|
|
2934
|
+
startColumn: finding.location.column,
|
|
2935
|
+
endLine: finding.location.endLine || finding.location.line,
|
|
2936
|
+
snippet: finding.snippet ? {
|
|
2937
|
+
text: finding.snippet.text
|
|
2938
|
+
} : void 0
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
],
|
|
2943
|
+
fixes: finding.fix ? [
|
|
2944
|
+
{
|
|
2945
|
+
description: {
|
|
2946
|
+
text: finding.fix.description
|
|
2947
|
+
},
|
|
2948
|
+
artifactChanges: finding.fix.replacement ? [
|
|
2949
|
+
{
|
|
2950
|
+
artifactLocation: {
|
|
2951
|
+
uri: finding.location.file
|
|
2952
|
+
},
|
|
2953
|
+
replacements: [
|
|
2954
|
+
{
|
|
2955
|
+
deletedRegion: {
|
|
2956
|
+
startLine: finding.location.line,
|
|
2957
|
+
startColumn: finding.location.column
|
|
2958
|
+
},
|
|
2959
|
+
insertedContent: {
|
|
2960
|
+
text: finding.fix.replacement
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
]
|
|
2964
|
+
}
|
|
2965
|
+
] : []
|
|
2966
|
+
}
|
|
2967
|
+
] : void 0,
|
|
2968
|
+
properties: {
|
|
2969
|
+
confidence: finding.confidence,
|
|
2970
|
+
cweId: finding.cweId,
|
|
2971
|
+
owaspCategory: finding.owaspCategory
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
var SarifReporter = class {
|
|
2976
|
+
name = "sarif";
|
|
2977
|
+
format(result) {
|
|
2978
|
+
const rulesMap = /* @__PURE__ */ new Map();
|
|
2979
|
+
for (const finding of result.findings) {
|
|
2980
|
+
if (!rulesMap.has(finding.ruleId)) {
|
|
2981
|
+
rulesMap.set(finding.ruleId, buildRule(finding));
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const sarif = {
|
|
2985
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
2986
|
+
version: "2.1.0",
|
|
2987
|
+
runs: [
|
|
2988
|
+
{
|
|
2989
|
+
tool: {
|
|
2990
|
+
driver: {
|
|
2991
|
+
name: TOOL.NAME,
|
|
2992
|
+
version: TOOL.VERSION,
|
|
2993
|
+
informationUri: TOOL.REPO_URL,
|
|
2994
|
+
rules: Array.from(rulesMap.values())
|
|
2995
|
+
}
|
|
2996
|
+
},
|
|
2997
|
+
results: result.findings.map(buildResult),
|
|
2998
|
+
invocations: [
|
|
2999
|
+
{
|
|
3000
|
+
executionSuccessful: true,
|
|
3001
|
+
endTimeUtc: result.timestamp
|
|
3002
|
+
}
|
|
3003
|
+
]
|
|
3004
|
+
}
|
|
3005
|
+
]
|
|
3006
|
+
};
|
|
3007
|
+
return JSON.stringify(sarif, null, 2);
|
|
3008
|
+
}
|
|
3009
|
+
};
|
|
3010
|
+
|
|
3011
|
+
// src/internal/vuln-checker/types.ts
|
|
3012
|
+
init_esm_shims();
|
|
3013
|
+
|
|
3014
|
+
// src/internal/vuln-checker/index.ts
|
|
3015
|
+
var SEVERITY_ORDER2 = {
|
|
3016
|
+
critical: 5,
|
|
3017
|
+
high: 4,
|
|
3018
|
+
medium: 3,
|
|
3019
|
+
low: 2,
|
|
3020
|
+
info: 1
|
|
3021
|
+
};
|
|
3022
|
+
async function buildInlineAnnotationsMap(files) {
|
|
3023
|
+
const inlineAnnotations = /* @__PURE__ */ new Map();
|
|
3024
|
+
for (const file of files) {
|
|
3025
|
+
const annotations = await parseInlineAnnotations(file);
|
|
3026
|
+
if (annotations.length === 0) continue;
|
|
3027
|
+
const lineRules = buildLineRulesSet(annotations);
|
|
3028
|
+
inlineAnnotations.set(file, lineRules);
|
|
3029
|
+
}
|
|
3030
|
+
return inlineAnnotations;
|
|
3031
|
+
}
|
|
3032
|
+
function buildLineRulesSet(annotations) {
|
|
3033
|
+
const lineRules = /* @__PURE__ */ new Set();
|
|
3034
|
+
for (const ann of annotations) {
|
|
3035
|
+
const key = getAnnotationKey(ann);
|
|
3036
|
+
if (key) lineRules.add(key);
|
|
3037
|
+
}
|
|
3038
|
+
return lineRules;
|
|
3039
|
+
}
|
|
3040
|
+
function getAnnotationKey(ann) {
|
|
3041
|
+
if (ann.scope === "line" || ann.scope === "next-line") {
|
|
3042
|
+
const targetLine = ann.scope === "next-line" ? ann.line + 1 : ann.line;
|
|
3043
|
+
return `${targetLine}:${ann.ruleId}`;
|
|
3044
|
+
}
|
|
3045
|
+
if (ann.scope === "file") {
|
|
3046
|
+
return `file:${ann.ruleId}`;
|
|
3047
|
+
}
|
|
3048
|
+
return null;
|
|
3049
|
+
}
|
|
3050
|
+
function isIgnoredByAnnotation(finding, fileAnnotations) {
|
|
3051
|
+
if (!fileAnnotations) return false;
|
|
3052
|
+
return fileAnnotations.has(`${finding.location.line}:${finding.ruleId}`) || fileAnnotations.has(`${finding.location.line}:*`) || fileAnnotations.has(`file:${finding.ruleId}`) || fileAnnotations.has("file:*");
|
|
3053
|
+
}
|
|
3054
|
+
function buildSummary(findings, ignoredCount) {
|
|
3055
|
+
return {
|
|
3056
|
+
total: findings.length,
|
|
3057
|
+
critical: findings.filter((f) => f.severity === "critical").length,
|
|
3058
|
+
high: findings.filter((f) => f.severity === "high").length,
|
|
3059
|
+
medium: findings.filter((f) => f.severity === "medium").length,
|
|
3060
|
+
low: findings.filter((f) => f.severity === "low").length,
|
|
3061
|
+
info: findings.filter((f) => f.severity === "info").length,
|
|
3062
|
+
ignored: ignoredCount
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
var VulnChecker = class {
|
|
3066
|
+
options;
|
|
3067
|
+
config;
|
|
3068
|
+
ignores;
|
|
3069
|
+
analyzers;
|
|
3070
|
+
reporters;
|
|
3071
|
+
errors = [];
|
|
3072
|
+
constructor(options) {
|
|
3073
|
+
this.options = {
|
|
3074
|
+
rootDir: options.rootDir,
|
|
3075
|
+
configFile: options.configFile || ".vuln-checker.yml",
|
|
3076
|
+
ignoreFile: options.ignoreFile || ".vuln-ignore.yml",
|
|
3077
|
+
categories: options.categories || [
|
|
3078
|
+
"injection",
|
|
3079
|
+
"auth",
|
|
3080
|
+
"crypto",
|
|
3081
|
+
"secret",
|
|
3082
|
+
"config",
|
|
3083
|
+
"rls",
|
|
3084
|
+
"dependency",
|
|
3085
|
+
"design"
|
|
3086
|
+
],
|
|
3087
|
+
minSeverity: options.minSeverity || "low",
|
|
3088
|
+
format: options.format || "console",
|
|
3089
|
+
showIgnored: options.showIgnored || false,
|
|
3090
|
+
include: options.include || ["**/*.ts", "**/*.tsx", "**/*.sql"],
|
|
3091
|
+
exclude: options.exclude || [
|
|
3092
|
+
"**/node_modules/**",
|
|
3093
|
+
"**/dist/**",
|
|
3094
|
+
"**/.next/**",
|
|
3095
|
+
"**/coverage/**"
|
|
3096
|
+
]
|
|
3097
|
+
};
|
|
3098
|
+
this.config = { version: 1 };
|
|
3099
|
+
this.ignores = [];
|
|
3100
|
+
this.analyzers = [
|
|
3101
|
+
new TypeScriptAnalyzer(),
|
|
3102
|
+
new SecretAnalyzer(),
|
|
3103
|
+
new DependencyAnalyzer(),
|
|
3104
|
+
new RLSAnalyzer()
|
|
3105
|
+
];
|
|
3106
|
+
this.reporters = /* @__PURE__ */ new Map([
|
|
3107
|
+
["json", new JsonReporter()],
|
|
3108
|
+
["sarif", new SarifReporter()],
|
|
3109
|
+
["markdown", new MarkdownReporter()],
|
|
3110
|
+
["console", new ConsoleReporter()]
|
|
3111
|
+
]);
|
|
3112
|
+
}
|
|
3113
|
+
async init() {
|
|
3114
|
+
this.config = await loadConfig(this.options.configFile, this.options.rootDir);
|
|
3115
|
+
this.ignores = await loadIgnores(this.options.ignoreFile, this.options.rootDir);
|
|
3116
|
+
if (this.config.ignores) {
|
|
3117
|
+
this.ignores.push(...this.config.ignores);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
async scan() {
|
|
3121
|
+
await this.init();
|
|
3122
|
+
const startTime = Date.now();
|
|
3123
|
+
const files = await this.getFiles();
|
|
3124
|
+
const inlineAnnotations = await buildInlineAnnotationsMap(files);
|
|
3125
|
+
const { findings, ignoredFindings } = await this.runAnalyzers(inlineAnnotations);
|
|
3126
|
+
findings.sort((a, b) => SEVERITY_ORDER2[b.severity] - SEVERITY_ORDER2[a.severity]);
|
|
3127
|
+
const result = {
|
|
3128
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3129
|
+
summary: buildSummary(findings, ignoredFindings.length),
|
|
3130
|
+
findings,
|
|
3131
|
+
metadata: {
|
|
3132
|
+
duration: Date.now() - startTime,
|
|
3133
|
+
filesScanned: files.length,
|
|
3134
|
+
rulesEvaluated: this.analyzers.reduce((acc, a) => acc + a.categories.length * 10, 0),
|
|
3135
|
+
rootDir: this.options.rootDir
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
if (this.options.showIgnored) {
|
|
3139
|
+
result.ignoredFindings = ignoredFindings;
|
|
3140
|
+
}
|
|
3141
|
+
return result;
|
|
3142
|
+
}
|
|
3143
|
+
async getFiles() {
|
|
3144
|
+
validateGlobPatterns(this.options.include);
|
|
3145
|
+
if (this.options.exclude) {
|
|
3146
|
+
validateGlobPatterns(this.options.exclude);
|
|
3147
|
+
}
|
|
3148
|
+
const files = await glob(this.options.include, {
|
|
3149
|
+
cwd: this.options.rootDir,
|
|
3150
|
+
ignore: this.options.exclude,
|
|
3151
|
+
absolute: true
|
|
3152
|
+
});
|
|
3153
|
+
return filterPathsWithinBoundary(files, this.options.rootDir);
|
|
3154
|
+
}
|
|
3155
|
+
async runAnalyzers(inlineAnnotations) {
|
|
3156
|
+
const findings = [];
|
|
3157
|
+
const ignoredFindings = [];
|
|
3158
|
+
const analyzerOptions = {
|
|
3159
|
+
rootDir: this.options.rootDir,
|
|
3160
|
+
include: this.options.include,
|
|
3161
|
+
exclude: this.options.exclude,
|
|
3162
|
+
ignores: this.ignores
|
|
3163
|
+
};
|
|
3164
|
+
for (const analyzer of this.analyzers) {
|
|
3165
|
+
if (!this.shouldRunAnalyzer(analyzer)) continue;
|
|
3166
|
+
try {
|
|
3167
|
+
const result = await analyzer.analyze(analyzerOptions);
|
|
3168
|
+
this.classifyFindings(result, inlineAnnotations, findings, ignoredFindings);
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
this.errors.push({ analyzer: analyzer.name, error });
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return { findings, ignoredFindings };
|
|
3174
|
+
}
|
|
3175
|
+
shouldRunAnalyzer(analyzer) {
|
|
3176
|
+
return analyzer.categories.some((cat) => this.options.categories.includes(cat));
|
|
3177
|
+
}
|
|
3178
|
+
classifyFindings(analyzerFindings, inlineAnnotations, findings, ignoredFindings) {
|
|
3179
|
+
for (const finding of analyzerFindings) {
|
|
3180
|
+
if (!this.meetsMinSeverity(finding)) continue;
|
|
3181
|
+
const isIgnored = this.isFindingIgnored(finding, inlineAnnotations);
|
|
3182
|
+
if (isIgnored) {
|
|
3183
|
+
ignoredFindings.push(finding);
|
|
3184
|
+
} else {
|
|
3185
|
+
findings.push(finding);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
meetsMinSeverity(finding) {
|
|
3190
|
+
return SEVERITY_ORDER2[finding.severity] >= SEVERITY_ORDER2[this.options.minSeverity];
|
|
3191
|
+
}
|
|
3192
|
+
isFindingIgnored(finding, inlineAnnotations) {
|
|
3193
|
+
const isGloballyIgnored = this.ignores.some((ignore) => matchIgnoreRule(ignore, finding));
|
|
3194
|
+
const fileAnnotations = inlineAnnotations.get(finding.location.file);
|
|
3195
|
+
const isInlineIgnored = isIgnoredByAnnotation(finding, fileAnnotations);
|
|
3196
|
+
return isGloballyIgnored || isInlineIgnored;
|
|
3197
|
+
}
|
|
3198
|
+
format(result) {
|
|
3199
|
+
const reporter = this.reporters.get(this.options.format);
|
|
3200
|
+
if (!reporter) {
|
|
3201
|
+
throw new Error(`Unknown format: ${this.options.format}`);
|
|
3202
|
+
}
|
|
3203
|
+
return reporter.format(result);
|
|
3204
|
+
}
|
|
3205
|
+
async run() {
|
|
3206
|
+
const result = await this.scan();
|
|
3207
|
+
return this.format(result);
|
|
3208
|
+
}
|
|
3209
|
+
getExitCode(result, failOn = "high") {
|
|
3210
|
+
const threshold = SEVERITY_ORDER2[failOn];
|
|
3211
|
+
if (result.summary.critical > 0 && SEVERITY_ORDER2.critical >= threshold) return 1;
|
|
3212
|
+
if (result.summary.high > 0 && SEVERITY_ORDER2.high >= threshold) return 1;
|
|
3213
|
+
if (result.summary.medium > 0 && SEVERITY_ORDER2.medium >= threshold) return 1;
|
|
3214
|
+
if (result.summary.low > 0 && SEVERITY_ORDER2.low >= threshold) return 1;
|
|
3215
|
+
return 0;
|
|
3216
|
+
}
|
|
3217
|
+
/** Get errors that occurred during scanning */
|
|
3218
|
+
getErrors() {
|
|
3219
|
+
return this.errors;
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
|
|
3223
|
+
export { VulnChecker };
|