vibecheck-ai 2.0.1 → 5.0.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/bin/.generated +25 -0
- package/bin/_deprecations.js +463 -0
- package/bin/_router.js +46 -0
- package/bin/cli-hygiene.js +241 -0
- package/bin/dev/run-v2-torture.js +30 -0
- package/bin/registry.js +656 -0
- package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -0
- package/bin/runners/REPORT_AUDIT.md +64 -0
- package/bin/runners/cli-utils.js +1070 -0
- package/bin/runners/context/ai-task-decomposer.js +337 -0
- package/bin/runners/context/analyzer.js +513 -0
- package/bin/runners/context/api-contracts.js +427 -0
- package/bin/runners/context/context-diff.js +342 -0
- package/bin/runners/context/context-pruner.js +291 -0
- package/bin/runners/context/dependency-graph.js +414 -0
- package/bin/runners/context/generators/claude.js +107 -0
- package/bin/runners/context/generators/codex.js +108 -0
- package/bin/runners/context/generators/copilot.js +119 -0
- package/bin/runners/context/generators/cursor-enhanced.js +2525 -0
- package/bin/runners/context/generators/cursor.js +514 -0
- package/bin/runners/context/generators/mcp.js +169 -0
- package/bin/runners/context/generators/windsurf.js +180 -0
- package/bin/runners/context/git-context.js +304 -0
- package/bin/runners/context/index.js +1110 -0
- package/bin/runners/context/insights.js +173 -0
- package/bin/runners/context/mcp-server/generate-rules.js +337 -0
- package/bin/runners/context/mcp-server/index.js +1176 -0
- package/bin/runners/context/mcp-server/package.json +24 -0
- package/bin/runners/context/memory.js +200 -0
- package/bin/runners/context/monorepo.js +215 -0
- package/bin/runners/context/multi-repo-federation.js +404 -0
- package/bin/runners/context/patterns.js +253 -0
- package/bin/runners/context/proof-context.js +1264 -0
- package/bin/runners/context/security-scanner.js +541 -0
- package/bin/runners/context/semantic-search.js +350 -0
- package/bin/runners/context/shared.js +264 -0
- package/bin/runners/context/team-conventions.js +336 -0
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +308 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +79 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +227 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +191 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/ai-bridge.js +416 -0
- package/bin/runners/lib/analysis-core.js +309 -0
- package/bin/runners/lib/analyzers.js +2500 -0
- package/bin/runners/lib/api-client.js +269 -0
- package/bin/runners/lib/approve-output.js +235 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
- package/bin/runners/lib/audit-bridge.js +391 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/auth-truth.js +193 -0
- package/bin/runners/lib/auth.js +215 -0
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/backup.js +62 -0
- package/bin/runners/lib/billing.js +107 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/claims.js +118 -0
- package/bin/runners/lib/classify-output.js +204 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/cli-output.js +400 -0
- package/bin/runners/lib/cli-ui.js +540 -0
- package/bin/runners/lib/compliance-bridge-new.js +0 -0
- package/bin/runners/lib/compliance-bridge.js +165 -0
- package/bin/runners/lib/contracts/auth-contract.js +202 -0
- package/bin/runners/lib/contracts/env-contract.js +181 -0
- package/bin/runners/lib/contracts/external-contract.js +206 -0
- package/bin/runners/lib/contracts/guard.js +168 -0
- package/bin/runners/lib/contracts/index.js +89 -0
- package/bin/runners/lib/contracts/plan-validator.js +311 -0
- package/bin/runners/lib/contracts/route-contract.js +199 -0
- package/bin/runners/lib/contracts.js +804 -0
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/detect.js +89 -0
- package/bin/runners/lib/detectors-v2.js +622 -0
- package/bin/runners/lib/doctor/autofix.js +254 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/index.js +37 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -0
- package/bin/runners/lib/doctor/modules/index.js +105 -0
- package/bin/runners/lib/doctor/modules/network.js +250 -0
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/project.js +312 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/modules/runtime.js +224 -0
- package/bin/runners/lib/doctor/modules/security.js +350 -0
- package/bin/runners/lib/doctor/modules/system.js +213 -0
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -0
- package/bin/runners/lib/doctor/reporter.js +262 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/doctor/service.js +262 -0
- package/bin/runners/lib/doctor/types.js +113 -0
- package/bin/runners/lib/doctor/ui.js +263 -0
- package/bin/runners/lib/doctor-enhanced.js +233 -0
- package/bin/runners/lib/doctor-output.js +226 -0
- package/bin/runners/lib/doctor-v2.js +608 -0
- package/bin/runners/lib/drift.js +425 -0
- package/bin/runners/lib/enforcement.js +72 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -0
- package/bin/runners/lib/enterprise-init.js +942 -0
- package/bin/runners/lib/entitlements-v2.js +265 -0
- package/bin/runners/lib/entitlements.generated.js +0 -0
- package/bin/runners/lib/entitlements.js +340 -0
- package/bin/runners/lib/env-resolver.js +417 -0
- package/bin/runners/lib/env-template.js +66 -0
- package/bin/runners/lib/env.js +189 -0
- package/bin/runners/lib/error-handler.js +368 -0
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +684 -0
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/extractors/client-calls.js +990 -0
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
- package/bin/runners/lib/extractors/fastify-routes.js +426 -0
- package/bin/runners/lib/extractors/index.js +363 -0
- package/bin/runners/lib/extractors/next-routes.js +524 -0
- package/bin/runners/lib/extractors/proof-graph.js +431 -0
- package/bin/runners/lib/extractors/route-matcher.js +451 -0
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
- package/bin/runners/lib/extractors/ui-bindings.js +547 -0
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/findings-schema.js +281 -0
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/firewall-prompt.js +50 -0
- package/bin/runners/lib/fix-output.js +228 -0
- package/bin/runners/lib/global-flags.js +250 -0
- package/bin/runners/lib/graph/graph-builder.js +265 -0
- package/bin/runners/lib/graph/html-renderer.js +413 -0
- package/bin/runners/lib/graph/index.js +32 -0
- package/bin/runners/lib/graph/runtime-collector.js +215 -0
- package/bin/runners/lib/graph/static-extractor.js +518 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/html-proof-report.js +913 -0
- package/bin/runners/lib/html-report.js +650 -0
- package/bin/runners/lib/init-wizard.js +601 -0
- package/bin/runners/lib/interactive-menu.js +1496 -0
- package/bin/runners/lib/json-output.js +76 -0
- package/bin/runners/lib/llm.js +75 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/meter.js +61 -0
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/evidence.js +126 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +648 -0
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/missions/templates.js +317 -0
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/patch.js +40 -0
- package/bin/runners/lib/permissions/auth-model.js +213 -0
- package/bin/runners/lib/permissions/idor-prover.js +205 -0
- package/bin/runners/lib/permissions/index.js +45 -0
- package/bin/runners/lib/permissions/matrix-builder.js +198 -0
- package/bin/runners/lib/pkgjson.js +28 -0
- package/bin/runners/lib/policy.js +295 -0
- package/bin/runners/lib/polish/accessibility.js +62 -0
- package/bin/runners/lib/polish/analyzer.js +93 -0
- package/bin/runners/lib/polish/backend.js +87 -0
- package/bin/runners/lib/polish/configuration.js +83 -0
- package/bin/runners/lib/polish/documentation.js +83 -0
- package/bin/runners/lib/polish/frontend.js +817 -0
- package/bin/runners/lib/polish/index.js +27 -0
- package/bin/runners/lib/polish/infrastructure.js +80 -0
- package/bin/runners/lib/polish/internationalization.js +85 -0
- package/bin/runners/lib/polish/libraries.js +180 -0
- package/bin/runners/lib/polish/observability.js +75 -0
- package/bin/runners/lib/polish/performance.js +64 -0
- package/bin/runners/lib/polish/privacy.js +110 -0
- package/bin/runners/lib/polish/resilience.js +92 -0
- package/bin/runners/lib/polish/security.js +78 -0
- package/bin/runners/lib/polish/seo.js +71 -0
- package/bin/runners/lib/polish/styles.js +62 -0
- package/bin/runners/lib/polish/utils.js +104 -0
- package/bin/runners/lib/preflight.js +142 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/prove-output.js +220 -0
- package/bin/runners/lib/reality/correlation-detectors.js +359 -0
- package/bin/runners/lib/reality/index.js +318 -0
- package/bin/runners/lib/reality/request-hashing.js +416 -0
- package/bin/runners/lib/reality/request-mapper.js +453 -0
- package/bin/runners/lib/reality/safety-rails.js +463 -0
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
- package/bin/runners/lib/reality/toast-detector.js +393 -0
- package/bin/runners/lib/reality-findings.js +84 -0
- package/bin/runners/lib/reality-output.js +231 -0
- package/bin/runners/lib/receipts.js +179 -0
- package/bin/runners/lib/redact.js +29 -0
- package/bin/runners/lib/replay/capsule-manager.js +154 -0
- package/bin/runners/lib/replay/index.js +263 -0
- package/bin/runners/lib/replay/player.js +348 -0
- package/bin/runners/lib/replay/recorder.js +331 -0
- package/bin/runners/lib/report-engine.js +626 -0
- package/bin/runners/lib/report-html.js +1233 -0
- package/bin/runners/lib/report-output.js +366 -0
- package/bin/runners/lib/report-templates.js +967 -0
- package/bin/runners/lib/report.js +135 -0
- package/bin/runners/lib/route-detection.js +1209 -0
- package/bin/runners/lib/route-truth.js +1322 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/sandbox/index.js +59 -0
- package/bin/runners/lib/sandbox/proof-chain.js +399 -0
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -0
- package/bin/runners/lib/sandbox/worktree.js +174 -0
- package/bin/runners/lib/scan-cache.js +330 -0
- package/bin/runners/lib/scan-output-schema.js +344 -0
- package/bin/runners/lib/scan-output.js +631 -0
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schema-validator.js +350 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/contracts.schema.json +160 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/finding.schema.json +100 -0
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
- package/bin/runners/lib/schemas/validator.js +465 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/score-history.js +282 -0
- package/bin/runners/lib/security-bridge.js +249 -0
- package/bin/runners/lib/server-usage.js +513 -0
- package/bin/runners/lib/share-pack.js +239 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/ship-output.js +1128 -0
- package/bin/runners/lib/snippets.js +67 -0
- package/bin/runners/lib/status-output.js +340 -0
- package/bin/runners/lib/terminal-ui.js +356 -0
- package/bin/runners/lib/truth.js +1691 -0
- package/bin/runners/lib/ui.js +562 -0
- package/bin/runners/lib/unified-cli-output.js +947 -0
- package/bin/runners/lib/unified-output.js +197 -0
- package/bin/runners/lib/upsell.js +410 -0
- package/bin/runners/lib/usage.js +153 -0
- package/bin/runners/lib/validate-patch.js +156 -0
- package/bin/runners/lib/verdict-engine.js +628 -0
- package/bin/runners/lib/verification.js +345 -0
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/reality/engine.js +917 -0
- package/bin/runners/reality/flows.js +122 -0
- package/bin/runners/reality/report.js +378 -0
- package/bin/runners/reality/session.js +193 -0
- package/bin/runners/runAIAgent.js +229 -0
- package/bin/runners/runAgent.d.ts +5 -0
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runAllowlist.js +418 -0
- package/bin/runners/runApprove.js +320 -0
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +731 -0
- package/bin/runners/runCI.js +353 -0
- package/bin/runners/runCheckpoint.js +530 -0
- package/bin/runners/runClassify.js +928 -0
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runContext.d.ts +4 -0
- package/bin/runners/runContext.js +175 -0
- package/bin/runners/runDoctor.js +877 -0
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.d.ts +5 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.d.ts +5 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runFix.js +1355 -0
- package/bin/runners/runForge.js +451 -0
- package/bin/runners/runGuard.js +262 -0
- package/bin/runners/runInit.js +1927 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1875 -0
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.d.ts +4 -0
- package/bin/runners/runPolish.js +390 -0
- package/bin/runners/runPromptFirewall.js +211 -0
- package/bin/runners/runProve.js +1411 -0
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +2260 -0
- package/bin/runners/runReport.js +726 -0
- package/bin/runners/runRuntime.js +110 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +688 -0
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +1660 -0
- package/bin/runners/runTruth.d.ts +5 -0
- package/bin/runners/runTruth.js +101 -0
- package/bin/runners/runValidate.js +179 -0
- package/bin/runners/runWatch.js +478 -0
- package/bin/runners/utils.js +360 -0
- package/bin/scan.js +617 -0
- package/bin/vibecheck.js +1617 -0
- package/dist/guardrail/index.d.ts +2405 -0
- package/dist/guardrail/index.js +9747 -0
- package/dist/guardrail/index.js.map +1 -0
- package/dist/scanner/index.d.ts +282 -0
- package/dist/scanner/index.js +3395 -0
- package/dist/scanner/index.js.map +1 -0
- package/package.json +123 -104
- package/README.md +0 -491
- package/dist/index.js +0 -99711
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,3395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/scanner/index.ts
|
|
21
|
+
var scanner_exports = {};
|
|
22
|
+
__export(scanner_exports, {
|
|
23
|
+
ALL_ENGINES: () => ALL_ENGINES,
|
|
24
|
+
RULE_CATALOG: () => RULE_CATALOG,
|
|
25
|
+
applyFixes: () => applyFixes,
|
|
26
|
+
classifyPath: () => classifyPath,
|
|
27
|
+
fix: () => fix,
|
|
28
|
+
getRuleOrDefault: () => getRuleOrDefault,
|
|
29
|
+
scan: () => scan
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(scanner_exports);
|
|
32
|
+
var import_fs3 = require("fs");
|
|
33
|
+
var import_path4 = require("path");
|
|
34
|
+
var import_crypto = require("crypto");
|
|
35
|
+
|
|
36
|
+
// src/scanner/classifiers/path-classifier.ts
|
|
37
|
+
var import_path = require("path");
|
|
38
|
+
var CATEGORY_PATTERNS = [
|
|
39
|
+
{
|
|
40
|
+
category: "third_party",
|
|
41
|
+
excludeByDefault: true,
|
|
42
|
+
patterns: [
|
|
43
|
+
/node_modules\//,
|
|
44
|
+
/vendor\//,
|
|
45
|
+
/\.yarn\//,
|
|
46
|
+
/\.pnpm\//,
|
|
47
|
+
/bower_components\//,
|
|
48
|
+
/packages\/.*\/node_modules/
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
category: "build_output",
|
|
53
|
+
excludeByDefault: true,
|
|
54
|
+
patterns: [
|
|
55
|
+
/\/dist\//,
|
|
56
|
+
/\/build\//,
|
|
57
|
+
/\/.next\//,
|
|
58
|
+
/\/.nuxt\//,
|
|
59
|
+
/\/\.output\//,
|
|
60
|
+
/\/out\//,
|
|
61
|
+
/\/coverage\//,
|
|
62
|
+
/\/__generated__\//,
|
|
63
|
+
/\.min\.(js|css)$/,
|
|
64
|
+
/\.bundle\.(js|css)$/,
|
|
65
|
+
/\.chunk\.(js|css)$/
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
category: "generated",
|
|
70
|
+
excludeByDefault: true,
|
|
71
|
+
patterns: [
|
|
72
|
+
/\.d\.ts$/,
|
|
73
|
+
/generated\//,
|
|
74
|
+
/\.gen\.(ts|js)$/,
|
|
75
|
+
/prisma\/generated\//,
|
|
76
|
+
/graphql\/generated\//,
|
|
77
|
+
/swagger-output\./,
|
|
78
|
+
/openapi-generated\//,
|
|
79
|
+
/\.lock$/,
|
|
80
|
+
/lock\.json$/
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
category: "test",
|
|
85
|
+
excludeByDefault: false,
|
|
86
|
+
// We scan tests separately
|
|
87
|
+
patterns: [
|
|
88
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
89
|
+
/__tests__\//,
|
|
90
|
+
/__mocks__\//,
|
|
91
|
+
/\.stories\.(ts|tsx|js|jsx)$/,
|
|
92
|
+
/\.storybook\//,
|
|
93
|
+
/test-utils?\.(ts|tsx|js|jsx)$/,
|
|
94
|
+
/fixtures?\//,
|
|
95
|
+
/cypress\//,
|
|
96
|
+
/playwright\//,
|
|
97
|
+
/e2e\//,
|
|
98
|
+
/setupTests\.(ts|js)$/,
|
|
99
|
+
/jest\.config\./,
|
|
100
|
+
/vitest\.config\./
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
category: "config",
|
|
105
|
+
excludeByDefault: false,
|
|
106
|
+
patterns: [
|
|
107
|
+
/\.(config|rc)\.(ts|js|mjs|cjs|json|yaml|yml)$/,
|
|
108
|
+
/\.eslintrc/,
|
|
109
|
+
/\.prettierrc/,
|
|
110
|
+
/tsconfig.*\.json$/,
|
|
111
|
+
/\.babelrc/,
|
|
112
|
+
/tailwind\.config/,
|
|
113
|
+
/next\.config/,
|
|
114
|
+
/vite\.config/,
|
|
115
|
+
/webpack\.config/
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
category: "documentation",
|
|
120
|
+
excludeByDefault: true,
|
|
121
|
+
patterns: [
|
|
122
|
+
/\.(md|mdx|rst|txt)$/,
|
|
123
|
+
/\/docs?\//,
|
|
124
|
+
/\/documentation\//,
|
|
125
|
+
/README/i,
|
|
126
|
+
/CHANGELOG/i,
|
|
127
|
+
/LICENSE/i,
|
|
128
|
+
/CONTRIBUTING/i
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
var CRITICAL_PATH_PATTERNS = [
|
|
133
|
+
/\/api\//,
|
|
134
|
+
/\/auth\//,
|
|
135
|
+
/\/payment/,
|
|
136
|
+
/\/billing/,
|
|
137
|
+
/\/admin/,
|
|
138
|
+
/\/checkout/,
|
|
139
|
+
/\/users\//,
|
|
140
|
+
/\/account/,
|
|
141
|
+
/\/webhook/,
|
|
142
|
+
/\/security/,
|
|
143
|
+
/middleware\.(ts|js)/,
|
|
144
|
+
/\/lib\/auth/,
|
|
145
|
+
/\/lib\/db/,
|
|
146
|
+
/\/lib\/stripe/
|
|
147
|
+
];
|
|
148
|
+
var ALWAYS_EXCLUDE = [
|
|
149
|
+
/\.git\//,
|
|
150
|
+
/\.DS_Store$/,
|
|
151
|
+
/thumbs\.db$/i,
|
|
152
|
+
/\.(png|jpg|jpeg|gif|webp|svg|ico|bmp|tiff?)$/i,
|
|
153
|
+
/\.(woff2?|ttf|eot|otf)$/i,
|
|
154
|
+
/\.(mp3|mp4|wav|ogg|webm|avi|mov)$/i,
|
|
155
|
+
/\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$/i,
|
|
156
|
+
/\.(zip|tar|gz|bz2|7z|rar)$/i,
|
|
157
|
+
/\.(exe|dll|so|dylib|bin)$/i,
|
|
158
|
+
/\.(sqlite|db)$/i
|
|
159
|
+
];
|
|
160
|
+
function classifyPath(relativePath) {
|
|
161
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
162
|
+
for (const pattern of ALWAYS_EXCLUDE) {
|
|
163
|
+
if (pattern.test(normalizedPath)) {
|
|
164
|
+
return {
|
|
165
|
+
category: "build_output",
|
|
166
|
+
reason: `Binary/irrelevant file: ${pattern.source}`,
|
|
167
|
+
excludeByDefault: true,
|
|
168
|
+
isCriticalPath: false
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const def of CATEGORY_PATTERNS) {
|
|
173
|
+
for (const pattern of def.patterns) {
|
|
174
|
+
if (pattern.test(normalizedPath)) {
|
|
175
|
+
return {
|
|
176
|
+
category: def.category,
|
|
177
|
+
reason: `Matched pattern: ${pattern.source}`,
|
|
178
|
+
excludeByDefault: def.excludeByDefault,
|
|
179
|
+
isCriticalPath: false
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const isCritical = CRITICAL_PATH_PATTERNS.some((p) => p.test(normalizedPath));
|
|
185
|
+
return {
|
|
186
|
+
category: "user_code",
|
|
187
|
+
reason: "Application source code",
|
|
188
|
+
excludeByDefault: false,
|
|
189
|
+
isCriticalPath: isCritical
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function isCodeFile(filePath) {
|
|
193
|
+
const ext = (0, import_path.extname)(filePath).toLowerCase();
|
|
194
|
+
return [
|
|
195
|
+
".ts",
|
|
196
|
+
".tsx",
|
|
197
|
+
".js",
|
|
198
|
+
".jsx",
|
|
199
|
+
".mjs",
|
|
200
|
+
".cjs",
|
|
201
|
+
".py",
|
|
202
|
+
".java",
|
|
203
|
+
".go",
|
|
204
|
+
".rs",
|
|
205
|
+
".rb",
|
|
206
|
+
".php",
|
|
207
|
+
".vue",
|
|
208
|
+
".svelte"
|
|
209
|
+
].includes(ext);
|
|
210
|
+
}
|
|
211
|
+
function isUIFile(filePath) {
|
|
212
|
+
const ext = (0, import_path.extname)(filePath).toLowerCase();
|
|
213
|
+
return [".tsx", ".jsx", ".vue", ".svelte"].includes(ext);
|
|
214
|
+
}
|
|
215
|
+
function escalateSeverity(severity, isCriticalPath) {
|
|
216
|
+
if (!isCriticalPath) return severity;
|
|
217
|
+
const escalation = {
|
|
218
|
+
low: "medium",
|
|
219
|
+
medium: "high",
|
|
220
|
+
high: "critical",
|
|
221
|
+
critical: "critical"
|
|
222
|
+
};
|
|
223
|
+
return escalation[severity];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/scanner/utils/dedup.ts
|
|
227
|
+
var SEVERITY_ORDER = {
|
|
228
|
+
critical: 4,
|
|
229
|
+
high: 3,
|
|
230
|
+
medium: 2,
|
|
231
|
+
low: 1
|
|
232
|
+
};
|
|
233
|
+
function deduplicateFindings(findings) {
|
|
234
|
+
const groups = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const finding of findings) {
|
|
236
|
+
const key = `${finding.file}:${finding.line}`;
|
|
237
|
+
if (!groups.has(key)) {
|
|
238
|
+
groups.set(key, []);
|
|
239
|
+
}
|
|
240
|
+
groups.get(key).push(finding);
|
|
241
|
+
}
|
|
242
|
+
const deduplicated = [];
|
|
243
|
+
let suppressedCount = 0;
|
|
244
|
+
for (const [, group] of groups) {
|
|
245
|
+
if (group.length === 1) {
|
|
246
|
+
deduplicated.push(group[0]);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const subGroups = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const f of group) {
|
|
251
|
+
const subKey = f.category;
|
|
252
|
+
if (!subGroups.has(subKey)) subGroups.set(subKey, []);
|
|
253
|
+
subGroups.get(subKey).push(f);
|
|
254
|
+
}
|
|
255
|
+
for (const [, subGroup] of subGroups) {
|
|
256
|
+
if (subGroup.length === 1) {
|
|
257
|
+
deduplicated.push(subGroup[0]);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
subGroup.sort((a, b) => {
|
|
261
|
+
const confDiff = b.confidenceScore - a.confidenceScore;
|
|
262
|
+
if (confDiff !== 0) return confDiff;
|
|
263
|
+
return (SEVERITY_ORDER[b.severity] ?? 0) - (SEVERITY_ORDER[a.severity] ?? 0);
|
|
264
|
+
});
|
|
265
|
+
const best = { ...subGroup[0] };
|
|
266
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
267
|
+
for (const f of subGroup) {
|
|
268
|
+
for (const tag of f.tags) allTags.add(tag);
|
|
269
|
+
}
|
|
270
|
+
best.tags = Array.from(allTags);
|
|
271
|
+
const uniqueEngines = new Set(subGroup.map((f) => f.engine));
|
|
272
|
+
if (uniqueEngines.size >= 2) {
|
|
273
|
+
best.verified = true;
|
|
274
|
+
if (best.severity !== "critical") {
|
|
275
|
+
const maxSeverity = subGroup.reduce(
|
|
276
|
+
(max, f) => SEVERITY_ORDER[f.severity] > SEVERITY_ORDER[max] ? f.severity : max,
|
|
277
|
+
best.severity
|
|
278
|
+
);
|
|
279
|
+
best.severity = maxSeverity;
|
|
280
|
+
}
|
|
281
|
+
best.confidenceScore = Math.min(99, best.confidenceScore + 5);
|
|
282
|
+
}
|
|
283
|
+
deduplicated.push(best);
|
|
284
|
+
suppressedCount += subGroup.length - 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
deduplicated.sort((a, b) => {
|
|
288
|
+
const sevDiff = (SEVERITY_ORDER[b.severity] ?? 0) - (SEVERITY_ORDER[a.severity] ?? 0);
|
|
289
|
+
if (sevDiff !== 0) return sevDiff;
|
|
290
|
+
return b.confidenceScore - a.confidenceScore;
|
|
291
|
+
});
|
|
292
|
+
return { deduplicated, suppressedCount };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/scanner/rules/catalog.ts
|
|
296
|
+
var RULE_CATALOG = {
|
|
297
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
298
|
+
// CREDENTIALS (CRED)
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
300
|
+
CRED001: {
|
|
301
|
+
ruleId: "CRED001",
|
|
302
|
+
name: "Hardcoded API Key",
|
|
303
|
+
category: "credentials",
|
|
304
|
+
severity: "critical",
|
|
305
|
+
description: "Hardcoded API key (Stripe, AWS, OpenAI, etc.) in source code",
|
|
306
|
+
why: "If your code is ever exposed (git push, npm publish, source maps), these credentials will be compromised. Attackers automate scanning for leaked keys.",
|
|
307
|
+
fix: "Move to environment variable. Use process.env.API_KEY and add to .env (gitignored).",
|
|
308
|
+
tags: ["secrets", "api-key", "credentials"],
|
|
309
|
+
autoFixable: false,
|
|
310
|
+
cwe: "CWE-798"
|
|
311
|
+
},
|
|
312
|
+
CRED002: {
|
|
313
|
+
ruleId: "CRED002",
|
|
314
|
+
name: "Hardcoded Password",
|
|
315
|
+
category: "credentials",
|
|
316
|
+
severity: "critical",
|
|
317
|
+
description: "Hardcoded password or database credential in source code",
|
|
318
|
+
why: "Passwords in code can be extracted from builds, logs, or version history. A single leaked DB password can compromise all user data.",
|
|
319
|
+
fix: "Use environment variables or a secrets manager (Vault, AWS Secrets Manager).",
|
|
320
|
+
tags: ["secrets", "password", "database"],
|
|
321
|
+
autoFixable: false,
|
|
322
|
+
cwe: "CWE-798"
|
|
323
|
+
},
|
|
324
|
+
CRED003: {
|
|
325
|
+
ruleId: "CRED003",
|
|
326
|
+
name: "Hardcoded JWT Secret",
|
|
327
|
+
category: "credentials",
|
|
328
|
+
severity: "critical",
|
|
329
|
+
description: "JWT signing secret hardcoded in source",
|
|
330
|
+
why: "Anyone with the JWT secret can forge authentication tokens and impersonate any user, including admins.",
|
|
331
|
+
fix: "Store in JWT_SECRET environment variable. Rotate regularly.",
|
|
332
|
+
tags: ["secrets", "jwt", "auth"],
|
|
333
|
+
autoFixable: false,
|
|
334
|
+
cwe: "CWE-798"
|
|
335
|
+
},
|
|
336
|
+
CRED004: {
|
|
337
|
+
ruleId: "CRED004",
|
|
338
|
+
name: "Private Key in Source",
|
|
339
|
+
category: "credentials",
|
|
340
|
+
severity: "critical",
|
|
341
|
+
description: "Private key (RSA, PEM, SSH) embedded in source code",
|
|
342
|
+
why: "Private keys grant access to encrypted communications, server authentication, and code signing. A leaked key requires full rotation.",
|
|
343
|
+
fix: "Store in secure key management. Load from file path in env var.",
|
|
344
|
+
tags: ["secrets", "private-key", "cryptography"],
|
|
345
|
+
autoFixable: false,
|
|
346
|
+
cwe: "CWE-321"
|
|
347
|
+
},
|
|
348
|
+
CRED005: {
|
|
349
|
+
ruleId: "CRED005",
|
|
350
|
+
name: "Connection String Exposed",
|
|
351
|
+
category: "credentials",
|
|
352
|
+
severity: "critical",
|
|
353
|
+
description: "Database connection string with credentials in source",
|
|
354
|
+
why: "Connection strings contain host, username, password, and database name. One string = complete database access.",
|
|
355
|
+
fix: "Use DATABASE_URL environment variable. Never commit .env files.",
|
|
356
|
+
tags: ["secrets", "database", "connection-string"],
|
|
357
|
+
autoFixable: false,
|
|
358
|
+
cwe: "CWE-798"
|
|
359
|
+
},
|
|
360
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
361
|
+
// SECURITY (SEC)
|
|
362
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
363
|
+
SEC001: {
|
|
364
|
+
ruleId: "SEC001",
|
|
365
|
+
name: "SQL Injection Risk",
|
|
366
|
+
category: "security",
|
|
367
|
+
severity: "critical",
|
|
368
|
+
description: "User input concatenated into SQL query",
|
|
369
|
+
why: "SQL injection allows attackers to read, modify, or delete all database data. It remains the #1 web vulnerability.",
|
|
370
|
+
fix: "Use parameterized queries or an ORM. Never concatenate user input into SQL.",
|
|
371
|
+
tags: ["injection", "sql", "database"],
|
|
372
|
+
autoFixable: false,
|
|
373
|
+
cwe: "CWE-89"
|
|
374
|
+
},
|
|
375
|
+
SEC002: {
|
|
376
|
+
ruleId: "SEC002",
|
|
377
|
+
name: "Command Injection Risk",
|
|
378
|
+
category: "security",
|
|
379
|
+
severity: "critical",
|
|
380
|
+
description: "User input passed to shell execution (exec, execSync, spawn)",
|
|
381
|
+
why: "Command injection lets attackers run arbitrary OS commands on your server \u2014 install backdoors, exfiltrate data, destroy files.",
|
|
382
|
+
fix: "Use execFile() with args array. Never pass user input to exec().",
|
|
383
|
+
tags: ["injection", "command", "shell"],
|
|
384
|
+
autoFixable: false,
|
|
385
|
+
cwe: "CWE-78"
|
|
386
|
+
},
|
|
387
|
+
SEC003: {
|
|
388
|
+
ruleId: "SEC003",
|
|
389
|
+
name: "XSS Vulnerability",
|
|
390
|
+
category: "security",
|
|
391
|
+
severity: "high",
|
|
392
|
+
description: "Unescaped user input rendered in HTML (innerHTML, dangerouslySetInnerHTML)",
|
|
393
|
+
why: "XSS lets attackers execute JavaScript in other users' browsers \u2014 steal sessions, redirect to phishing, deface UI.",
|
|
394
|
+
fix: "Use textContent instead of innerHTML. Sanitize with DOMPurify if HTML is required.",
|
|
395
|
+
tags: ["xss", "injection", "html"],
|
|
396
|
+
autoFixable: false,
|
|
397
|
+
cwe: "CWE-79"
|
|
398
|
+
},
|
|
399
|
+
SEC004: {
|
|
400
|
+
ruleId: "SEC004",
|
|
401
|
+
name: "Path Traversal Risk",
|
|
402
|
+
category: "security",
|
|
403
|
+
severity: "high",
|
|
404
|
+
description: "User input used in file path without sanitization",
|
|
405
|
+
why: "Path traversal (../../etc/passwd) lets attackers read any file on the server, including configs and secrets.",
|
|
406
|
+
fix: "Use path.resolve() and verify the result is within allowed directory.",
|
|
407
|
+
tags: ["path-traversal", "file-access"],
|
|
408
|
+
autoFixable: false,
|
|
409
|
+
cwe: "CWE-22"
|
|
410
|
+
},
|
|
411
|
+
SEC005: {
|
|
412
|
+
ruleId: "SEC005",
|
|
413
|
+
name: "SSRF Risk",
|
|
414
|
+
category: "security",
|
|
415
|
+
severity: "high",
|
|
416
|
+
description: "User-controlled URL passed to server-side fetch/request",
|
|
417
|
+
why: "SSRF lets attackers make your server request internal services, cloud metadata endpoints (169.254.169.254), or scan your network.",
|
|
418
|
+
fix: "Validate URL against allowlist of domains. Block private IPs.",
|
|
419
|
+
tags: ["ssrf", "network"],
|
|
420
|
+
autoFixable: false,
|
|
421
|
+
cwe: "CWE-918"
|
|
422
|
+
},
|
|
423
|
+
SEC006: {
|
|
424
|
+
ruleId: "SEC006",
|
|
425
|
+
name: "Prototype Pollution",
|
|
426
|
+
category: "security",
|
|
427
|
+
severity: "high",
|
|
428
|
+
description: "__proto__ or prototype[] manipulation detected",
|
|
429
|
+
why: "Prototype pollution can lead to property injection across all objects, causing auth bypass or RCE.",
|
|
430
|
+
fix: "Use Object.create(null) for lookup objects. Validate property names.",
|
|
431
|
+
tags: ["prototype-pollution", "injection"],
|
|
432
|
+
autoFixable: false,
|
|
433
|
+
cwe: "CWE-1321"
|
|
434
|
+
},
|
|
435
|
+
SEC007: {
|
|
436
|
+
ruleId: "SEC007",
|
|
437
|
+
name: "eval() Usage",
|
|
438
|
+
category: "security",
|
|
439
|
+
severity: "critical",
|
|
440
|
+
description: "eval() or new Function() with potentially dynamic input",
|
|
441
|
+
why: "eval() executes arbitrary code. If any input is user-controlled, it's equivalent to giving the attacker a shell.",
|
|
442
|
+
fix: "Use JSON.parse() for data. Use a sandboxed interpreter for expressions.",
|
|
443
|
+
tags: ["eval", "code-execution"],
|
|
444
|
+
autoFixable: false,
|
|
445
|
+
cwe: "CWE-95"
|
|
446
|
+
},
|
|
447
|
+
SEC008: {
|
|
448
|
+
ruleId: "SEC008",
|
|
449
|
+
name: "Mass Assignment",
|
|
450
|
+
category: "security",
|
|
451
|
+
severity: "high",
|
|
452
|
+
description: "Request body spread directly into database/object update",
|
|
453
|
+
why: `Mass assignment lets attackers set fields they shouldn't (isAdmin: true, role: "admin") by adding extra fields to requests.`,
|
|
454
|
+
fix: "Whitelist allowed fields explicitly. Use DTOs or pick() to select safe properties.",
|
|
455
|
+
tags: ["mass-assignment", "injection"],
|
|
456
|
+
autoFixable: false,
|
|
457
|
+
cwe: "CWE-915"
|
|
458
|
+
},
|
|
459
|
+
SEC009: {
|
|
460
|
+
ruleId: "SEC009",
|
|
461
|
+
name: "Insecure Randomness",
|
|
462
|
+
category: "security",
|
|
463
|
+
severity: "high",
|
|
464
|
+
description: "Math.random() used for security-sensitive values (tokens, IDs, secrets)",
|
|
465
|
+
why: "Math.random() is not cryptographically secure. Tokens generated with it can be predicted.",
|
|
466
|
+
fix: "Use crypto.randomUUID() or crypto.randomBytes() for security-sensitive values.",
|
|
467
|
+
tags: ["randomness", "cryptography"],
|
|
468
|
+
autoFixable: true,
|
|
469
|
+
cwe: "CWE-330"
|
|
470
|
+
},
|
|
471
|
+
SEC010: {
|
|
472
|
+
ruleId: "SEC010",
|
|
473
|
+
name: "Regex DoS Risk",
|
|
474
|
+
category: "security",
|
|
475
|
+
severity: "medium",
|
|
476
|
+
description: "Regular expression with catastrophic backtracking potential",
|
|
477
|
+
why: "A crafted input string can make a vulnerable regex take minutes/hours, causing denial of service.",
|
|
478
|
+
fix: "Use linear-time regex engines (re2) or simplify the pattern.",
|
|
479
|
+
tags: ["redos", "regex", "dos"],
|
|
480
|
+
autoFixable: false,
|
|
481
|
+
cwe: "CWE-1333"
|
|
482
|
+
},
|
|
483
|
+
SEC011: {
|
|
484
|
+
ruleId: "SEC011",
|
|
485
|
+
name: "Unfiltered DELETE/DROP",
|
|
486
|
+
category: "security",
|
|
487
|
+
severity: "critical",
|
|
488
|
+
description: "DELETE FROM or DROP TABLE without WHERE clause",
|
|
489
|
+
why: "Unfiltered destructive SQL operations can wipe entire tables in a single request.",
|
|
490
|
+
fix: "Always include WHERE clause. Add confirmation logic for destructive operations.",
|
|
491
|
+
tags: ["sql", "destructive"],
|
|
492
|
+
autoFixable: false,
|
|
493
|
+
cwe: "CWE-89"
|
|
494
|
+
},
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
496
|
+
// HALLUCINATIONS (HAL) — AI-specific detection
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
498
|
+
HAL001: {
|
|
499
|
+
ruleId: "HAL001",
|
|
500
|
+
name: "Fake npm Package",
|
|
501
|
+
category: "hallucinations",
|
|
502
|
+
severity: "critical",
|
|
503
|
+
description: "Import from a package that doesn't exist on npm",
|
|
504
|
+
why: "AI models frequently hallucinate package names. Installing a non-existent package can pull in a typosquatting malware package instead.",
|
|
505
|
+
fix: "Verify package exists on npmjs.com before importing. Check package.json dependencies.",
|
|
506
|
+
tags: ["hallucination", "npm", "supply-chain"],
|
|
507
|
+
autoFixable: false,
|
|
508
|
+
cwe: "CWE-829"
|
|
509
|
+
},
|
|
510
|
+
HAL002: {
|
|
511
|
+
ruleId: "HAL002",
|
|
512
|
+
name: "Fake API Method",
|
|
513
|
+
category: "hallucinations",
|
|
514
|
+
severity: "high",
|
|
515
|
+
description: "Call to a method that doesn't exist on the referenced library/object",
|
|
516
|
+
why: "AI models invent plausible-sounding method names. Code compiles in TS only with 'any' types, then crashes at runtime.",
|
|
517
|
+
fix: "Check the library's actual API documentation. Use TypeScript strict mode.",
|
|
518
|
+
tags: ["hallucination", "api", "method"],
|
|
519
|
+
autoFixable: false
|
|
520
|
+
},
|
|
521
|
+
HAL003: {
|
|
522
|
+
ruleId: "HAL003",
|
|
523
|
+
name: "Placeholder URL",
|
|
524
|
+
category: "hallucinations",
|
|
525
|
+
severity: "high",
|
|
526
|
+
description: "URL containing example.com, placeholder, or clearly fake domain",
|
|
527
|
+
why: "AI generates placeholder URLs that look real. Shipping these means features silently fail or hit wrong endpoints.",
|
|
528
|
+
fix: "Replace with actual API endpoint from environment variable.",
|
|
529
|
+
tags: ["hallucination", "url", "placeholder"],
|
|
530
|
+
autoFixable: false
|
|
531
|
+
},
|
|
532
|
+
HAL004: {
|
|
533
|
+
ruleId: "HAL004",
|
|
534
|
+
name: "Invented Config Option",
|
|
535
|
+
category: "hallucinations",
|
|
536
|
+
severity: "medium",
|
|
537
|
+
description: "Configuration key that doesn't exist in the library's schema",
|
|
538
|
+
why: "AI invents plausible config options. The option is silently ignored, and the expected behavior never activates.",
|
|
539
|
+
fix: "Check the library docs for valid configuration options.",
|
|
540
|
+
tags: ["hallucination", "config"],
|
|
541
|
+
autoFixable: false
|
|
542
|
+
},
|
|
543
|
+
HAL005: {
|
|
544
|
+
ruleId: "HAL005",
|
|
545
|
+
name: "Ghost Route",
|
|
546
|
+
category: "hallucinations",
|
|
547
|
+
severity: "high",
|
|
548
|
+
description: "Frontend references an API route that doesn't exist in the backend",
|
|
549
|
+
why: "AI generates matching frontend + backend code, but sometimes only one side. The missing route means the feature silently 404s.",
|
|
550
|
+
fix: "Verify route exists in your API. Run vibecheck truth to generate route manifest.",
|
|
551
|
+
tags: ["hallucination", "route", "api"],
|
|
552
|
+
autoFixable: false
|
|
553
|
+
},
|
|
554
|
+
HAL006: {
|
|
555
|
+
ruleId: "HAL006",
|
|
556
|
+
name: "Ghost Env Variable",
|
|
557
|
+
category: "hallucinations",
|
|
558
|
+
severity: "high",
|
|
559
|
+
description: "process.env.X references a variable not declared in any .env file",
|
|
560
|
+
why: "AI adds env var references without updating .env. The code silently uses undefined, causing subtle runtime bugs.",
|
|
561
|
+
fix: "Add the variable to .env.example and .env. Use required() wrapper for critical vars.",
|
|
562
|
+
tags: ["hallucination", "env", "config"],
|
|
563
|
+
autoFixable: true
|
|
564
|
+
},
|
|
565
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
566
|
+
// FAKE FEATURES (FAKE) — Stubs, placeholders, empty implementations
|
|
567
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
568
|
+
FAKE001: {
|
|
569
|
+
ruleId: "FAKE001",
|
|
570
|
+
name: "Empty Function Body",
|
|
571
|
+
category: "fake-features",
|
|
572
|
+
severity: "high",
|
|
573
|
+
description: "Function declared but body is empty or returns nothing",
|
|
574
|
+
why: "AI generates function signatures that look complete but have empty bodies. The feature appears to exist but does nothing.",
|
|
575
|
+
fix: "Implement the function body or remove if not needed.",
|
|
576
|
+
tags: ["stub", "empty", "fake-feature"],
|
|
577
|
+
autoFixable: false
|
|
578
|
+
},
|
|
579
|
+
FAKE002: {
|
|
580
|
+
ruleId: "FAKE002",
|
|
581
|
+
name: "Hardcoded Success",
|
|
582
|
+
category: "fake-features",
|
|
583
|
+
severity: "high",
|
|
584
|
+
description: "Function always returns true/success without doing actual work",
|
|
585
|
+
why: "Auth checks, validation, and payment processing that always return success means no actual protection is in place.",
|
|
586
|
+
fix: "Implement actual logic. If intentionally stubbed, add @stub annotation.",
|
|
587
|
+
tags: ["fake-success", "stub", "auth"],
|
|
588
|
+
autoFixable: false
|
|
589
|
+
},
|
|
590
|
+
FAKE003: {
|
|
591
|
+
ruleId: "FAKE003",
|
|
592
|
+
name: "Silent Error Swallowing",
|
|
593
|
+
category: "fake-features",
|
|
594
|
+
severity: "high",
|
|
595
|
+
description: "Empty catch block or catch that returns null/undefined",
|
|
596
|
+
why: "Silent failures hide bugs. Users see no error but the operation didn't complete. Data loss and corruption follow.",
|
|
597
|
+
fix: "Log the error and either re-throw, return error state, or handle gracefully.",
|
|
598
|
+
tags: ["error-handling", "silent-failure"],
|
|
599
|
+
autoFixable: false
|
|
600
|
+
},
|
|
601
|
+
FAKE004: {
|
|
602
|
+
ruleId: "FAKE004",
|
|
603
|
+
name: "Placeholder/TODO in Production",
|
|
604
|
+
category: "fake-features",
|
|
605
|
+
severity: "medium",
|
|
606
|
+
description: "TODO, FIXME, HACK, WIP, CHANGEME markers in production code",
|
|
607
|
+
why: "Placeholder markers indicate incomplete implementation. Shipping them means features are partially broken.",
|
|
608
|
+
fix: "Complete the implementation or create a tracked issue.",
|
|
609
|
+
tags: ["placeholder", "todo", "incomplete"],
|
|
610
|
+
autoFixable: false
|
|
611
|
+
},
|
|
612
|
+
FAKE005: {
|
|
613
|
+
ruleId: "FAKE005",
|
|
614
|
+
name: "Auth Bypass Pattern",
|
|
615
|
+
category: "fake-features",
|
|
616
|
+
severity: "critical",
|
|
617
|
+
description: "Authentication check that can be bypassed (skipAuth, isAdmin=true)",
|
|
618
|
+
why: "Auth bypasses in code are the most common security vulnerability. Any user can access protected resources.",
|
|
619
|
+
fix: "Remove the bypass. Use proper role-based access control.",
|
|
620
|
+
tags: ["auth", "bypass", "security"],
|
|
621
|
+
autoFixable: false,
|
|
622
|
+
cwe: "CWE-287"
|
|
623
|
+
},
|
|
624
|
+
FAKE006: {
|
|
625
|
+
ruleId: "FAKE006",
|
|
626
|
+
name: "Dangerous Default Value",
|
|
627
|
+
category: "fake-features",
|
|
628
|
+
severity: "high",
|
|
629
|
+
description: "Secret/credential env var with insecure fallback default",
|
|
630
|
+
why: "If the env var is missing, the insecure default is used in production \u2014 often a test key or empty string.",
|
|
631
|
+
fix: "Throw an error if required env vars are missing. Never provide fallback defaults for secrets.",
|
|
632
|
+
tags: ["default", "secret", "env"],
|
|
633
|
+
autoFixable: false
|
|
634
|
+
},
|
|
635
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
636
|
+
// DEAD UI (UI)
|
|
637
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
638
|
+
UI001: {
|
|
639
|
+
ruleId: "UI001",
|
|
640
|
+
name: "Dead Link",
|
|
641
|
+
category: "dead-ui",
|
|
642
|
+
severity: "high",
|
|
643
|
+
description: 'href="#" or javascript:void(0) \u2014 link goes nowhere',
|
|
644
|
+
why: "Dead links frustrate users and indicate unfinished UI. Screen readers announce them as real links.",
|
|
645
|
+
fix: "Replace with actual route, use <button> for actions, or remove.",
|
|
646
|
+
tags: ["ui", "accessibility", "dead-link"],
|
|
647
|
+
autoFixable: false
|
|
648
|
+
},
|
|
649
|
+
UI002: {
|
|
650
|
+
ruleId: "UI002",
|
|
651
|
+
name: "Noop Click Handler",
|
|
652
|
+
category: "dead-ui",
|
|
653
|
+
severity: "high",
|
|
654
|
+
description: "onClick={() => {}} \u2014 button/element does nothing when clicked",
|
|
655
|
+
why: "Users click the button expecting something to happen. Nothing does. This erodes trust in the entire application.",
|
|
656
|
+
fix: "Implement the handler or remove the onClick.",
|
|
657
|
+
tags: ["ui", "noop", "handler"],
|
|
658
|
+
autoFixable: false
|
|
659
|
+
},
|
|
660
|
+
UI003: {
|
|
661
|
+
ruleId: "UI003",
|
|
662
|
+
name: "Coming Soon in Production",
|
|
663
|
+
category: "dead-ui",
|
|
664
|
+
severity: "medium",
|
|
665
|
+
description: '"Coming soon", "under construction", "not available" in production UI',
|
|
666
|
+
why: "Users see a broken product. Competitors see an incomplete product. Neither is a good look.",
|
|
667
|
+
fix: "Hide the feature until ready, or implement it.",
|
|
668
|
+
tags: ["ui", "placeholder", "coming-soon"],
|
|
669
|
+
autoFixable: false
|
|
670
|
+
},
|
|
671
|
+
UI004: {
|
|
672
|
+
ruleId: "UI004",
|
|
673
|
+
name: "Disabled Without Reason",
|
|
674
|
+
category: "dead-ui",
|
|
675
|
+
severity: "medium",
|
|
676
|
+
description: "Disabled button/input without tooltip or explanation",
|
|
677
|
+
why: "Users don't know why they can't click. This is a UX and accessibility failure.",
|
|
678
|
+
fix: "Add tooltip, aria-label, or visible text explaining why it's disabled.",
|
|
679
|
+
tags: ["ui", "accessibility", "disabled"],
|
|
680
|
+
autoFixable: false
|
|
681
|
+
},
|
|
682
|
+
UI005: {
|
|
683
|
+
ruleId: "UI005",
|
|
684
|
+
name: "Raw Fetch in Component",
|
|
685
|
+
category: "dead-ui",
|
|
686
|
+
severity: "low",
|
|
687
|
+
description: 'Direct fetch("/api/...") call in a UI component',
|
|
688
|
+
why: "Scattered fetch calls are hard to maintain, don't handle loading/error states consistently, and bypass caching.",
|
|
689
|
+
fix: "Use React Query, SWR, tRPC, or a centralized API client.",
|
|
690
|
+
tags: ["ui", "fetch", "architecture"],
|
|
691
|
+
autoFixable: false
|
|
692
|
+
},
|
|
693
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
694
|
+
// CODE QUALITY (QLT)
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
696
|
+
QLT001: {
|
|
697
|
+
ruleId: "QLT001",
|
|
698
|
+
name: "Console.log in Production",
|
|
699
|
+
category: "code-quality",
|
|
700
|
+
severity: "medium",
|
|
701
|
+
description: "console.log/debug/trace statement in production code",
|
|
702
|
+
why: "Console logs leak information in browser devtools, slow down rendering, and indicate debug code left behind.",
|
|
703
|
+
fix: "Remove or replace with a proper logging library (winston, pino).",
|
|
704
|
+
tags: ["console", "debug", "quality"],
|
|
705
|
+
autoFixable: true
|
|
706
|
+
},
|
|
707
|
+
QLT002: {
|
|
708
|
+
ruleId: "QLT002",
|
|
709
|
+
name: "Debugger Statement",
|
|
710
|
+
category: "code-quality",
|
|
711
|
+
severity: "high",
|
|
712
|
+
description: "debugger statement left in production code",
|
|
713
|
+
why: "Debugger statements freeze the browser for any user with devtools open. In production, this is a showstopper.",
|
|
714
|
+
fix: "Remove the debugger statement.",
|
|
715
|
+
tags: ["debugger", "debug", "quality"],
|
|
716
|
+
autoFixable: true
|
|
717
|
+
},
|
|
718
|
+
QLT003: {
|
|
719
|
+
ruleId: "QLT003",
|
|
720
|
+
name: "Hardcoded Mock Data",
|
|
721
|
+
category: "code-quality",
|
|
722
|
+
severity: "high",
|
|
723
|
+
description: "Mock/fake/dummy data in production source files",
|
|
724
|
+
why: "Mock data makes features look functional but returns stale/fake information to real users.",
|
|
725
|
+
fix: "Connect to actual data source. Move mock data to test files.",
|
|
726
|
+
tags: ["mock", "fake-data", "quality"],
|
|
727
|
+
autoFixable: false
|
|
728
|
+
},
|
|
729
|
+
QLT004: {
|
|
730
|
+
ruleId: "QLT004",
|
|
731
|
+
name: "Unused Import",
|
|
732
|
+
category: "code-quality",
|
|
733
|
+
severity: "low",
|
|
734
|
+
description: "Import that is never used in the file",
|
|
735
|
+
why: "Unused imports increase bundle size and confuse other developers about what the file depends on.",
|
|
736
|
+
fix: "Remove the unused import.",
|
|
737
|
+
tags: ["unused", "import", "quality"],
|
|
738
|
+
autoFixable: true
|
|
739
|
+
},
|
|
740
|
+
QLT005: {
|
|
741
|
+
ruleId: "QLT005",
|
|
742
|
+
name: "Type Assertion Abuse",
|
|
743
|
+
category: "code-quality",
|
|
744
|
+
severity: "medium",
|
|
745
|
+
description: '"as any" or type assertion bypassing type safety',
|
|
746
|
+
why: "Type assertions silence the compiler but don't fix the underlying type issue. Runtime errors follow.",
|
|
747
|
+
fix: "Fix the type properly. Use type guards or proper generic types.",
|
|
748
|
+
tags: ["typescript", "type-safety", "quality"],
|
|
749
|
+
autoFixable: false
|
|
750
|
+
},
|
|
751
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
752
|
+
// IMPORT GRAPH (IG) — Cross-file structural analysis
|
|
753
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
754
|
+
IG001: {
|
|
755
|
+
ruleId: "IG001",
|
|
756
|
+
name: "Circular Dependency",
|
|
757
|
+
category: "import-graph",
|
|
758
|
+
severity: "high",
|
|
759
|
+
description: "Circular import chain detected between modules",
|
|
760
|
+
why: "Circular dependencies cause unpredictable initialization order, undefined values at import time, and make refactoring dangerous.",
|
|
761
|
+
fix: "Extract shared types/interfaces into a separate module. Break the cycle with dependency inversion.",
|
|
762
|
+
tags: ["imports", "circular", "architecture"],
|
|
763
|
+
autoFixable: false
|
|
764
|
+
},
|
|
765
|
+
IG002: {
|
|
766
|
+
ruleId: "IG002",
|
|
767
|
+
name: "Orphan Module",
|
|
768
|
+
category: "import-graph",
|
|
769
|
+
severity: "medium",
|
|
770
|
+
description: "File is never imported by any other module in the project",
|
|
771
|
+
why: "Orphan modules are dead code that increases bundle size, confuses developers, and accumulates tech debt.",
|
|
772
|
+
fix: "Delete the file if unused, or add it to your entry points if it should be included.",
|
|
773
|
+
tags: ["imports", "dead-code", "orphan"],
|
|
774
|
+
autoFixable: false
|
|
775
|
+
},
|
|
776
|
+
IG003: {
|
|
777
|
+
ruleId: "IG003",
|
|
778
|
+
name: "Ghost Route",
|
|
779
|
+
category: "import-graph",
|
|
780
|
+
severity: "high",
|
|
781
|
+
description: "Frontend fetches an API route that has no corresponding backend handler",
|
|
782
|
+
why: "Ghost routes cause silent 404s in production. Users see loading spinners that never resolve or cryptic error messages.",
|
|
783
|
+
fix: "Create the missing API route handler, or remove the frontend fetch call.",
|
|
784
|
+
tags: ["imports", "route", "api", "ghost"],
|
|
785
|
+
autoFixable: false
|
|
786
|
+
},
|
|
787
|
+
IG004: {
|
|
788
|
+
ruleId: "IG004",
|
|
789
|
+
name: "Ghost Env Variable",
|
|
790
|
+
category: "import-graph",
|
|
791
|
+
severity: "high",
|
|
792
|
+
description: "process.env.X referenced in code but not declared in any .env file",
|
|
793
|
+
why: "Missing env vars silently evaluate to undefined, causing features to break in production with no error message.",
|
|
794
|
+
fix: "Add the variable to .env and .env.example with a placeholder value.",
|
|
795
|
+
tags: ["env", "config", "ghost"],
|
|
796
|
+
autoFixable: true
|
|
797
|
+
},
|
|
798
|
+
IG005: {
|
|
799
|
+
ruleId: "IG005",
|
|
800
|
+
name: "Barrel Re-export Bloat",
|
|
801
|
+
category: "import-graph",
|
|
802
|
+
severity: "low",
|
|
803
|
+
description: "Barrel index.ts re-exports everything, defeating tree-shaking",
|
|
804
|
+
why: "Barrel files that re-export entire modules prevent bundlers from tree-shaking unused code, increasing bundle size.",
|
|
805
|
+
fix: "Use direct imports instead of barrel imports for large modules.",
|
|
806
|
+
tags: ["imports", "barrel", "performance"],
|
|
807
|
+
autoFixable: false
|
|
808
|
+
},
|
|
809
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
810
|
+
// RUNTIME VERIFY (RV) — Static analysis for runtime issues
|
|
811
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
812
|
+
RV001: {
|
|
813
|
+
ruleId: "RV001",
|
|
814
|
+
name: "Unhandled Promise",
|
|
815
|
+
category: "runtime-verify",
|
|
816
|
+
severity: "high",
|
|
817
|
+
description: "Promise chain without .catch() or try/catch around await",
|
|
818
|
+
why: "Unhandled promise rejections crash Node.js processes and leave users with white screens in browsers.",
|
|
819
|
+
fix: "Add .catch() to the promise chain, or wrap await in try/catch.",
|
|
820
|
+
tags: ["async", "promise", "error-handling"],
|
|
821
|
+
autoFixable: false,
|
|
822
|
+
cwe: "CWE-755"
|
|
823
|
+
},
|
|
824
|
+
RV002: {
|
|
825
|
+
ruleId: "RV002",
|
|
826
|
+
name: "Async Without Await",
|
|
827
|
+
category: "runtime-verify",
|
|
828
|
+
severity: "medium",
|
|
829
|
+
description: "Async function declared but never uses await",
|
|
830
|
+
why: "An async function without await still returns a Promise, which can mask bugs if the caller doesn't await it.",
|
|
831
|
+
fix: "Remove async keyword if not needed, or add await for async operations inside.",
|
|
832
|
+
tags: ["async", "promise", "quality"],
|
|
833
|
+
autoFixable: false
|
|
834
|
+
},
|
|
835
|
+
RV003: {
|
|
836
|
+
ruleId: "RV003",
|
|
837
|
+
name: "Dead Export",
|
|
838
|
+
category: "runtime-verify",
|
|
839
|
+
severity: "medium",
|
|
840
|
+
description: "Exported function/variable is never imported by any other file",
|
|
841
|
+
why: "Dead exports increase API surface area, confuse developers about the public API, and prevent safe refactoring.",
|
|
842
|
+
fix: "Remove the export keyword if the symbol is only used locally, or delete if truly unused.",
|
|
843
|
+
tags: ["exports", "dead-code", "quality"],
|
|
844
|
+
autoFixable: false
|
|
845
|
+
},
|
|
846
|
+
RV004: {
|
|
847
|
+
ruleId: "RV004",
|
|
848
|
+
name: "Floating Promise",
|
|
849
|
+
category: "runtime-verify",
|
|
850
|
+
severity: "high",
|
|
851
|
+
description: "Async function called without await, .then(), or void operator",
|
|
852
|
+
why: "Floating promises run in the background with no error handling. Failures are silently swallowed.",
|
|
853
|
+
fix: "Add await, .then()/.catch(), or prefix with void if intentionally fire-and-forget.",
|
|
854
|
+
tags: ["async", "promise", "fire-and-forget"],
|
|
855
|
+
autoFixable: false,
|
|
856
|
+
cwe: "CWE-755"
|
|
857
|
+
},
|
|
858
|
+
RV005: {
|
|
859
|
+
ruleId: "RV005",
|
|
860
|
+
name: "Race Condition Risk",
|
|
861
|
+
category: "runtime-verify",
|
|
862
|
+
severity: "medium",
|
|
863
|
+
description: "Shared mutable state accessed in async context without synchronization",
|
|
864
|
+
why: "Concurrent async operations reading/writing the same variable cause intermittent bugs that are extremely hard to reproduce.",
|
|
865
|
+
fix: "Use a mutex/lock, or restructure to avoid shared mutable state.",
|
|
866
|
+
tags: ["async", "race-condition", "concurrency"],
|
|
867
|
+
autoFixable: false
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
function getRuleOrDefault(ruleId) {
|
|
871
|
+
return RULE_CATALOG[ruleId] ?? {
|
|
872
|
+
ruleId,
|
|
873
|
+
name: ruleId,
|
|
874
|
+
category: "code-quality",
|
|
875
|
+
severity: "medium",
|
|
876
|
+
description: `Rule ${ruleId}`,
|
|
877
|
+
why: "See rule documentation.",
|
|
878
|
+
fix: "Review and fix the flagged code.",
|
|
879
|
+
tags: [],
|
|
880
|
+
autoFixable: false
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/scanner/engines/credentials.ts
|
|
885
|
+
var PATTERNS = [
|
|
886
|
+
// ── Live payment keys (always flag) ──
|
|
887
|
+
{
|
|
888
|
+
name: "stripe-live-secret",
|
|
889
|
+
ruleId: "CRED001",
|
|
890
|
+
regex: /['"`](sk_live_[a-zA-Z0-9]{20,})['"`]/,
|
|
891
|
+
severity: "critical",
|
|
892
|
+
message: "Stripe live secret key hardcoded",
|
|
893
|
+
fix: "Use process.env.STRIPE_SECRET_KEY",
|
|
894
|
+
confidence: 99,
|
|
895
|
+
flagInTests: true,
|
|
896
|
+
skipInExamples: false,
|
|
897
|
+
cwe: "CWE-798"
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
name: "stripe-live-publishable",
|
|
901
|
+
ruleId: "CRED001",
|
|
902
|
+
regex: /['"`](pk_live_[a-zA-Z0-9]{20,})['"`]/,
|
|
903
|
+
severity: "high",
|
|
904
|
+
message: "Stripe live publishable key hardcoded",
|
|
905
|
+
fix: "Use process.env.NEXT_PUBLIC_STRIPE_KEY",
|
|
906
|
+
confidence: 95,
|
|
907
|
+
flagInTests: false,
|
|
908
|
+
skipInExamples: true
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
name: "stripe-test-secret",
|
|
912
|
+
ruleId: "CRED001",
|
|
913
|
+
regex: /['"`](sk_test_[a-zA-Z0-9]{20,})['"`]/,
|
|
914
|
+
severity: "high",
|
|
915
|
+
message: "Stripe test key hardcoded \u2014 use env var even for test keys",
|
|
916
|
+
fix: "Use process.env.STRIPE_TEST_KEY",
|
|
917
|
+
confidence: 90,
|
|
918
|
+
flagInTests: false,
|
|
919
|
+
skipInExamples: true
|
|
920
|
+
},
|
|
921
|
+
// ── AWS ──
|
|
922
|
+
{
|
|
923
|
+
name: "aws-access-key",
|
|
924
|
+
ruleId: "CRED001",
|
|
925
|
+
regex: /['"`](AKIA[0-9A-Z]{16})['"`]/,
|
|
926
|
+
severity: "critical",
|
|
927
|
+
message: "AWS Access Key ID hardcoded",
|
|
928
|
+
fix: "Use AWS SDK credential chain or AWS_ACCESS_KEY_ID env var",
|
|
929
|
+
confidence: 99,
|
|
930
|
+
flagInTests: true,
|
|
931
|
+
skipInExamples: false,
|
|
932
|
+
cwe: "CWE-798"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
name: "aws-secret-key",
|
|
936
|
+
ruleId: "CRED001",
|
|
937
|
+
regex: /aws_secret_access_key\s*[:=]\s*['"`]([^'"`]{20,})['"`]/i,
|
|
938
|
+
severity: "critical",
|
|
939
|
+
message: "AWS Secret Access Key hardcoded",
|
|
940
|
+
fix: "Use AWS_SECRET_ACCESS_KEY environment variable",
|
|
941
|
+
confidence: 98,
|
|
942
|
+
flagInTests: true,
|
|
943
|
+
skipInExamples: false,
|
|
944
|
+
cwe: "CWE-798"
|
|
945
|
+
},
|
|
946
|
+
// ── AI providers ──
|
|
947
|
+
{
|
|
948
|
+
name: "openai-key",
|
|
949
|
+
ruleId: "CRED001",
|
|
950
|
+
regex: /['"`](sk-[a-zA-Z0-9]{32,})['"`]/,
|
|
951
|
+
severity: "critical",
|
|
952
|
+
message: "OpenAI API key hardcoded",
|
|
953
|
+
fix: "Use process.env.OPENAI_API_KEY",
|
|
954
|
+
confidence: 92,
|
|
955
|
+
flagInTests: true,
|
|
956
|
+
skipInExamples: false
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: "anthropic-key",
|
|
960
|
+
ruleId: "CRED001",
|
|
961
|
+
regex: /['"`](sk-ant-[a-zA-Z0-9-]{20,})['"`]/,
|
|
962
|
+
severity: "critical",
|
|
963
|
+
message: "Anthropic API key hardcoded",
|
|
964
|
+
fix: "Use process.env.ANTHROPIC_API_KEY",
|
|
965
|
+
confidence: 98,
|
|
966
|
+
flagInTests: true,
|
|
967
|
+
skipInExamples: false
|
|
968
|
+
},
|
|
969
|
+
// ── GitHub / Git ──
|
|
970
|
+
{
|
|
971
|
+
name: "github-token",
|
|
972
|
+
ruleId: "CRED001",
|
|
973
|
+
regex: /['"`](ghp_[a-zA-Z0-9]{36,})['"`]/,
|
|
974
|
+
severity: "critical",
|
|
975
|
+
message: "GitHub personal access token hardcoded",
|
|
976
|
+
fix: "Use process.env.GITHUB_TOKEN",
|
|
977
|
+
confidence: 99,
|
|
978
|
+
flagInTests: true,
|
|
979
|
+
skipInExamples: false
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
name: "github-oauth",
|
|
983
|
+
ruleId: "CRED001",
|
|
984
|
+
regex: /['"`](gho_[a-zA-Z0-9]{36,})['"`]/,
|
|
985
|
+
severity: "critical",
|
|
986
|
+
message: "GitHub OAuth token hardcoded",
|
|
987
|
+
fix: "Use process.env.GITHUB_OAUTH_TOKEN",
|
|
988
|
+
confidence: 99,
|
|
989
|
+
flagInTests: true,
|
|
990
|
+
skipInExamples: false
|
|
991
|
+
},
|
|
992
|
+
// ── Google / Firebase ──
|
|
993
|
+
{
|
|
994
|
+
name: "google-api-key",
|
|
995
|
+
ruleId: "CRED001",
|
|
996
|
+
regex: /['"`](AIza[0-9A-Za-z_-]{35})['"`]/,
|
|
997
|
+
severity: "high",
|
|
998
|
+
message: "Google API key hardcoded",
|
|
999
|
+
fix: "Use process.env.GOOGLE_API_KEY",
|
|
1000
|
+
confidence: 95,
|
|
1001
|
+
flagInTests: false,
|
|
1002
|
+
skipInExamples: true
|
|
1003
|
+
},
|
|
1004
|
+
// ── Generic secrets ──
|
|
1005
|
+
{
|
|
1006
|
+
name: "jwt-secret",
|
|
1007
|
+
ruleId: "CRED003",
|
|
1008
|
+
regex: /(?:jwt[_-]?secret|JWT_SECRET)\s*[:=]\s*['"`]([^'"`]{8,})['"`]/i,
|
|
1009
|
+
severity: "critical",
|
|
1010
|
+
message: "JWT signing secret hardcoded",
|
|
1011
|
+
fix: "Use process.env.JWT_SECRET",
|
|
1012
|
+
confidence: 90,
|
|
1013
|
+
flagInTests: false,
|
|
1014
|
+
skipInExamples: true,
|
|
1015
|
+
cwe: "CWE-798"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
name: "generic-password",
|
|
1019
|
+
ruleId: "CRED002",
|
|
1020
|
+
regex: /(?:password|passwd|pwd)\s*[:=]\s*['"`]([^'"`]{4,})['"`]/i,
|
|
1021
|
+
severity: "high",
|
|
1022
|
+
message: "Hardcoded password detected",
|
|
1023
|
+
fix: "Use environment variable or secrets manager",
|
|
1024
|
+
confidence: 75,
|
|
1025
|
+
flagInTests: false,
|
|
1026
|
+
skipInExamples: true,
|
|
1027
|
+
cwe: "CWE-798"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: "generic-secret",
|
|
1031
|
+
ruleId: "CRED002",
|
|
1032
|
+
regex: /(?:secret|api[_-]?key|auth[_-]?token)\s*[:=]\s*['"`]([^'"`]{8,})['"`]/i,
|
|
1033
|
+
severity: "high",
|
|
1034
|
+
message: "Hardcoded secret/token detected",
|
|
1035
|
+
fix: "Use environment variable",
|
|
1036
|
+
confidence: 70,
|
|
1037
|
+
flagInTests: false,
|
|
1038
|
+
skipInExamples: true
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
name: "private-key-pem",
|
|
1042
|
+
ruleId: "CRED004",
|
|
1043
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
|
|
1044
|
+
severity: "critical",
|
|
1045
|
+
message: "Private key embedded in source code",
|
|
1046
|
+
fix: "Store key in file, reference via env var path",
|
|
1047
|
+
confidence: 99,
|
|
1048
|
+
flagInTests: true,
|
|
1049
|
+
skipInExamples: false,
|
|
1050
|
+
cwe: "CWE-321"
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
name: "connection-string",
|
|
1054
|
+
ruleId: "CRED005",
|
|
1055
|
+
regex: /['"`](?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\/\/[^'"`\s]{10,}['"`]/i,
|
|
1056
|
+
severity: "critical",
|
|
1057
|
+
message: "Database connection string with credentials in source",
|
|
1058
|
+
fix: "Use DATABASE_URL environment variable",
|
|
1059
|
+
confidence: 92,
|
|
1060
|
+
flagInTests: false,
|
|
1061
|
+
skipInExamples: true,
|
|
1062
|
+
cwe: "CWE-798"
|
|
1063
|
+
},
|
|
1064
|
+
// ── Additional providers ──
|
|
1065
|
+
{
|
|
1066
|
+
name: "slack-token",
|
|
1067
|
+
ruleId: "CRED001",
|
|
1068
|
+
regex: /['"`](xox[bpoas]-[0-9a-zA-Z-]{10,})['"`]/,
|
|
1069
|
+
severity: "critical",
|
|
1070
|
+
message: "Slack token hardcoded",
|
|
1071
|
+
fix: "Use process.env.SLACK_TOKEN",
|
|
1072
|
+
confidence: 97,
|
|
1073
|
+
flagInTests: true,
|
|
1074
|
+
skipInExamples: false
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
name: "sendgrid-key",
|
|
1078
|
+
ruleId: "CRED001",
|
|
1079
|
+
regex: /['"`](SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43})['"`]/,
|
|
1080
|
+
severity: "critical",
|
|
1081
|
+
message: "SendGrid API key hardcoded",
|
|
1082
|
+
fix: "Use process.env.SENDGRID_API_KEY",
|
|
1083
|
+
confidence: 99,
|
|
1084
|
+
flagInTests: true,
|
|
1085
|
+
skipInExamples: false
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: "twilio-key",
|
|
1089
|
+
ruleId: "CRED001",
|
|
1090
|
+
regex: /['"`](SK[a-f0-9]{32})['"`]/,
|
|
1091
|
+
severity: "high",
|
|
1092
|
+
message: "Twilio API key hardcoded",
|
|
1093
|
+
fix: "Use process.env.TWILIO_API_KEY",
|
|
1094
|
+
confidence: 85,
|
|
1095
|
+
flagInTests: false,
|
|
1096
|
+
skipInExamples: true
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
name: "npm-token",
|
|
1100
|
+
ruleId: "CRED001",
|
|
1101
|
+
regex: /['"`](npm_[a-zA-Z0-9]{36,})['"`]/,
|
|
1102
|
+
severity: "critical",
|
|
1103
|
+
message: "npm auth token hardcoded",
|
|
1104
|
+
fix: "Use .npmrc with env var interpolation",
|
|
1105
|
+
confidence: 98,
|
|
1106
|
+
flagInTests: true,
|
|
1107
|
+
skipInExamples: false
|
|
1108
|
+
}
|
|
1109
|
+
];
|
|
1110
|
+
var credentialsEngine = {
|
|
1111
|
+
name: "credentials",
|
|
1112
|
+
description: "Detects hardcoded API keys, secrets, passwords, tokens",
|
|
1113
|
+
async scan(files, options) {
|
|
1114
|
+
const findings = [];
|
|
1115
|
+
for (const [, file] of files) {
|
|
1116
|
+
const isTest = file.classification.category === "test";
|
|
1117
|
+
const isExample = /\.(example|sample|template)\b/.test(file.path);
|
|
1118
|
+
const isCritical = file.classification.isCriticalPath;
|
|
1119
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1120
|
+
const line = file.lines[i];
|
|
1121
|
+
const trimmed = line.trim();
|
|
1122
|
+
if (trimmed.startsWith("//") && !trimmed.includes("=")) continue;
|
|
1123
|
+
if (trimmed.startsWith("*")) continue;
|
|
1124
|
+
for (const pattern of PATTERNS) {
|
|
1125
|
+
if (isTest && !pattern.flagInTests) continue;
|
|
1126
|
+
if (isExample && pattern.skipInExamples) continue;
|
|
1127
|
+
const match = line.match(pattern.regex);
|
|
1128
|
+
if (!match) continue;
|
|
1129
|
+
if (pattern.name === "generic-password") {
|
|
1130
|
+
if (/type\s|interface\s|placeholder|example/i.test(line)) continue;
|
|
1131
|
+
const val = match[1];
|
|
1132
|
+
if (!val || val.length < 6) continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (pattern.name === "generic-secret") {
|
|
1135
|
+
const val = match[1];
|
|
1136
|
+
if (!val || /^[a-z_]+$/i.test(val)) continue;
|
|
1137
|
+
if (/placeholder|example|your[_-]/i.test(val)) continue;
|
|
1138
|
+
}
|
|
1139
|
+
const rule = getRuleOrDefault(pattern.ruleId);
|
|
1140
|
+
const severity = escalateSeverity(pattern.severity, isCritical);
|
|
1141
|
+
findings.push({
|
|
1142
|
+
id: `${pattern.ruleId}-${file.path}-${i + 1}`,
|
|
1143
|
+
ruleId: pattern.ruleId,
|
|
1144
|
+
engine: "credentials",
|
|
1145
|
+
category: "credentials",
|
|
1146
|
+
severity,
|
|
1147
|
+
confidence: pattern.confidence >= 90 ? "certain" : pattern.confidence >= 70 ? "likely" : "possible",
|
|
1148
|
+
confidenceScore: pattern.confidence,
|
|
1149
|
+
file: file.path,
|
|
1150
|
+
line: i + 1,
|
|
1151
|
+
column: match.index,
|
|
1152
|
+
code: trimmed,
|
|
1153
|
+
message: pattern.message,
|
|
1154
|
+
why: rule.why,
|
|
1155
|
+
fix: pattern.fix,
|
|
1156
|
+
autoFixable: false,
|
|
1157
|
+
tags: rule.tags,
|
|
1158
|
+
cwe: pattern.cwe,
|
|
1159
|
+
verified: false,
|
|
1160
|
+
_dedup: `${pattern.name}:${file.path}:${i + 1}`
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return findings;
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// src/scanner/engines/security.ts
|
|
1170
|
+
var PATTERNS2 = [
|
|
1171
|
+
// ── Injection ──
|
|
1172
|
+
{
|
|
1173
|
+
name: "sql-injection-concat",
|
|
1174
|
+
ruleId: "SEC001",
|
|
1175
|
+
regex: /(?:query|execute|raw)\s*\(\s*[`'"](?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\s[^`'"]*\$\{/i,
|
|
1176
|
+
severity: "critical",
|
|
1177
|
+
message: "SQL injection: template literal in query",
|
|
1178
|
+
fix: 'Use parameterized queries: db.query("SELECT * FROM users WHERE id = $1", [userId])',
|
|
1179
|
+
confidence: 92,
|
|
1180
|
+
excludeInTests: true,
|
|
1181
|
+
cwe: "CWE-89"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
name: "sql-injection-plus",
|
|
1185
|
+
ruleId: "SEC001",
|
|
1186
|
+
regex: /(?:query|execute|raw)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)\s[^'"]*['"]\s*\+/i,
|
|
1187
|
+
severity: "critical",
|
|
1188
|
+
message: "SQL injection: string concatenation in query",
|
|
1189
|
+
fix: "Use parameterized queries. Never concatenate user input into SQL.",
|
|
1190
|
+
confidence: 90,
|
|
1191
|
+
excludeInTests: true,
|
|
1192
|
+
cwe: "CWE-89"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
name: "unfiltered-delete",
|
|
1196
|
+
ruleId: "SEC011",
|
|
1197
|
+
regex: /DELETE\s+FROM\s+\w+\s*;?\s*$/im,
|
|
1198
|
+
severity: "critical",
|
|
1199
|
+
message: "Unfiltered DELETE statement (no WHERE clause)",
|
|
1200
|
+
fix: "Add WHERE clause. Add confirmation logic for destructive operations.",
|
|
1201
|
+
confidence: 85,
|
|
1202
|
+
excludeInTests: true,
|
|
1203
|
+
cwe: "CWE-89"
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: "drop-table",
|
|
1207
|
+
ruleId: "SEC011",
|
|
1208
|
+
regex: /DROP\s+TABLE/i,
|
|
1209
|
+
severity: "critical",
|
|
1210
|
+
message: "DROP TABLE statement in application code",
|
|
1211
|
+
fix: "Use migrations for schema changes. Never DROP in application logic.",
|
|
1212
|
+
confidence: 88,
|
|
1213
|
+
excludeInTests: true
|
|
1214
|
+
},
|
|
1215
|
+
// ── Command Injection ──
|
|
1216
|
+
{
|
|
1217
|
+
name: "exec-template",
|
|
1218
|
+
ruleId: "SEC002",
|
|
1219
|
+
regex: /(?:exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/,
|
|
1220
|
+
severity: "critical",
|
|
1221
|
+
message: "Command injection: template literal in shell command",
|
|
1222
|
+
fix: 'Use execFile() with args array: execFile("cmd", [arg1, arg2])',
|
|
1223
|
+
confidence: 95,
|
|
1224
|
+
excludeInTests: true,
|
|
1225
|
+
cwe: "CWE-78"
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
name: "exec-concat",
|
|
1229
|
+
ruleId: "SEC002",
|
|
1230
|
+
regex: /(?:exec|execSync)\s*\(\s*['"][^'"]*['"]\s*\+/,
|
|
1231
|
+
severity: "critical",
|
|
1232
|
+
message: "Command injection: concatenation in shell command",
|
|
1233
|
+
fix: "Use execFile() with args array.",
|
|
1234
|
+
confidence: 93,
|
|
1235
|
+
excludeInTests: true,
|
|
1236
|
+
cwe: "CWE-78"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: "child-process-exec",
|
|
1240
|
+
ruleId: "SEC002",
|
|
1241
|
+
regex: /child_process\s*\.\s*exec\s*\(/,
|
|
1242
|
+
severity: "high",
|
|
1243
|
+
message: "child_process.exec() can run arbitrary shell commands",
|
|
1244
|
+
fix: "Prefer execFile() or spawn() with explicit args array.",
|
|
1245
|
+
confidence: 80,
|
|
1246
|
+
excludeInTests: true,
|
|
1247
|
+
cwe: "CWE-78"
|
|
1248
|
+
},
|
|
1249
|
+
// ── XSS ──
|
|
1250
|
+
{
|
|
1251
|
+
name: "innerhtml-assignment",
|
|
1252
|
+
ruleId: "SEC003",
|
|
1253
|
+
regex: /\.innerHTML\s*=/,
|
|
1254
|
+
severity: "high",
|
|
1255
|
+
message: "innerHTML assignment \u2014 potential XSS if content is user-controlled",
|
|
1256
|
+
fix: "Use textContent for text. Use DOMPurify.sanitize() if HTML is needed.",
|
|
1257
|
+
confidence: 75,
|
|
1258
|
+
excludeInTests: true,
|
|
1259
|
+
cwe: "CWE-79"
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
name: "dangerously-set-innerhtml",
|
|
1263
|
+
ruleId: "SEC003",
|
|
1264
|
+
regex: /dangerouslySetInnerHTML/,
|
|
1265
|
+
severity: "high",
|
|
1266
|
+
message: "dangerouslySetInnerHTML \u2014 ensure content is sanitized",
|
|
1267
|
+
fix: "Sanitize with DOMPurify before passing to dangerouslySetInnerHTML.",
|
|
1268
|
+
confidence: 70,
|
|
1269
|
+
excludeInTests: true,
|
|
1270
|
+
cwe: "CWE-79"
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
name: "document-write",
|
|
1274
|
+
ruleId: "SEC003",
|
|
1275
|
+
regex: /document\.write\s*\(/,
|
|
1276
|
+
severity: "high",
|
|
1277
|
+
message: "document.write() can overwrite entire page with unescaped content",
|
|
1278
|
+
fix: "Use DOM manipulation methods instead.",
|
|
1279
|
+
confidence: 80,
|
|
1280
|
+
excludeInTests: true,
|
|
1281
|
+
cwe: "CWE-79"
|
|
1282
|
+
},
|
|
1283
|
+
// ── Path Traversal ──
|
|
1284
|
+
{
|
|
1285
|
+
name: "path-traversal-join",
|
|
1286
|
+
ruleId: "SEC004",
|
|
1287
|
+
regex: /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/,
|
|
1288
|
+
severity: "high",
|
|
1289
|
+
message: "User input in file path \u2014 potential path traversal",
|
|
1290
|
+
fix: "Validate resolved path is within allowed directory: resolvedPath.startsWith(allowedDir)",
|
|
1291
|
+
confidence: 82,
|
|
1292
|
+
excludeInTests: true,
|
|
1293
|
+
cwe: "CWE-22"
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
name: "fs-read-user-input",
|
|
1297
|
+
ruleId: "SEC004",
|
|
1298
|
+
regex: /fs\.(?:readFile|readFileSync|createReadStream)\s*\([^)]*(?:req\.|params\.|query\.)/,
|
|
1299
|
+
severity: "high",
|
|
1300
|
+
message: "User input passed directly to file read operation",
|
|
1301
|
+
fix: "Sanitize path and verify it's within allowed directory.",
|
|
1302
|
+
confidence: 85,
|
|
1303
|
+
excludeInTests: true,
|
|
1304
|
+
cwe: "CWE-22"
|
|
1305
|
+
},
|
|
1306
|
+
// ── SSRF ──
|
|
1307
|
+
{
|
|
1308
|
+
name: "ssrf-fetch",
|
|
1309
|
+
ruleId: "SEC005",
|
|
1310
|
+
regex: /fetch\s*\(\s*(?:req\.|params\.|query\.|body\.|url|href)/,
|
|
1311
|
+
severity: "high",
|
|
1312
|
+
message: "User-controlled URL in server-side fetch \u2014 SSRF risk",
|
|
1313
|
+
fix: "Validate URL against allowlist. Block private IPs (10.x, 172.16-31.x, 192.168.x, 169.254.x).",
|
|
1314
|
+
confidence: 78,
|
|
1315
|
+
excludeInTests: true,
|
|
1316
|
+
cwe: "CWE-918"
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
name: "ssrf-axios",
|
|
1320
|
+
ruleId: "SEC005",
|
|
1321
|
+
regex: /axios\s*(?:\.get|\.post|\.put|\.delete|\.request)\s*\(\s*(?:req\.|params\.|query\.|body\.)/,
|
|
1322
|
+
severity: "high",
|
|
1323
|
+
message: "User-controlled URL in axios request \u2014 SSRF risk",
|
|
1324
|
+
fix: "Validate URL against allowlist of allowed domains.",
|
|
1325
|
+
confidence: 78,
|
|
1326
|
+
excludeInTests: true,
|
|
1327
|
+
cwe: "CWE-918"
|
|
1328
|
+
},
|
|
1329
|
+
// ── Code Execution ──
|
|
1330
|
+
{
|
|
1331
|
+
name: "eval-usage",
|
|
1332
|
+
ruleId: "SEC007",
|
|
1333
|
+
regex: /\beval\s*\(/,
|
|
1334
|
+
severity: "critical",
|
|
1335
|
+
message: "eval() can execute arbitrary code",
|
|
1336
|
+
fix: "Use JSON.parse() for data. Use a sandboxed interpreter for expressions.",
|
|
1337
|
+
confidence: 88,
|
|
1338
|
+
excludeInTests: true,
|
|
1339
|
+
cwe: "CWE-95",
|
|
1340
|
+
validate: (line) => !/\/\/.*eval|['"]eval['"]/.test(line)
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
name: "new-function",
|
|
1344
|
+
ruleId: "SEC007",
|
|
1345
|
+
regex: /\bnew\s+Function\s*\(/,
|
|
1346
|
+
severity: "critical",
|
|
1347
|
+
message: "new Function() can execute arbitrary code",
|
|
1348
|
+
fix: "Avoid dynamic code generation. Use a template engine or parser.",
|
|
1349
|
+
confidence: 90,
|
|
1350
|
+
excludeInTests: true,
|
|
1351
|
+
cwe: "CWE-95"
|
|
1352
|
+
},
|
|
1353
|
+
// ── Prototype Pollution ──
|
|
1354
|
+
{
|
|
1355
|
+
name: "proto-access",
|
|
1356
|
+
ruleId: "SEC006",
|
|
1357
|
+
regex: /__proto__|prototype\s*\[/,
|
|
1358
|
+
severity: "high",
|
|
1359
|
+
message: "Prototype pollution risk \u2014 __proto__ or prototype[] access",
|
|
1360
|
+
fix: "Use Object.create(null) for lookup objects. Validate property names.",
|
|
1361
|
+
confidence: 82,
|
|
1362
|
+
excludeInTests: true,
|
|
1363
|
+
cwe: "CWE-1321"
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "mass-assignment",
|
|
1367
|
+
ruleId: "SEC008",
|
|
1368
|
+
regex: /Object\.assign\s*\([^,]+,\s*req\.body/,
|
|
1369
|
+
severity: "high",
|
|
1370
|
+
message: "Mass assignment \u2014 spreading request body into object",
|
|
1371
|
+
fix: "Whitelist allowed fields: const { name, email } = req.body",
|
|
1372
|
+
confidence: 85,
|
|
1373
|
+
excludeInTests: true,
|
|
1374
|
+
cwe: "CWE-915"
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
name: "spread-req-body",
|
|
1378
|
+
ruleId: "SEC008",
|
|
1379
|
+
regex: /\{\s*\.\.\.req\.body\s*\}/,
|
|
1380
|
+
severity: "high",
|
|
1381
|
+
message: "Mass assignment \u2014 spreading request body directly",
|
|
1382
|
+
fix: "Destructure only needed fields from req.body.",
|
|
1383
|
+
confidence: 88,
|
|
1384
|
+
excludeInTests: true,
|
|
1385
|
+
cwe: "CWE-915"
|
|
1386
|
+
},
|
|
1387
|
+
// ── Insecure Randomness ──
|
|
1388
|
+
{
|
|
1389
|
+
name: "math-random-token",
|
|
1390
|
+
ruleId: "SEC009",
|
|
1391
|
+
regex: /Math\.random\s*\(\s*\).*(?:token|id|key|secret|session|nonce|csrf)/i,
|
|
1392
|
+
severity: "high",
|
|
1393
|
+
message: "Math.random() used for security-sensitive value",
|
|
1394
|
+
fix: "Use crypto.randomUUID() or crypto.randomBytes()",
|
|
1395
|
+
confidence: 85,
|
|
1396
|
+
excludeInTests: true,
|
|
1397
|
+
cwe: "CWE-330"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
name: "math-random-hex",
|
|
1401
|
+
ruleId: "SEC009",
|
|
1402
|
+
regex: /Math\.random\(\)\.toString\((?:16|36)\)/,
|
|
1403
|
+
severity: "medium",
|
|
1404
|
+
message: "Math.random() for ID generation \u2014 not cryptographically secure",
|
|
1405
|
+
fix: "Use crypto.randomUUID() for unique IDs",
|
|
1406
|
+
confidence: 80,
|
|
1407
|
+
excludeInTests: true,
|
|
1408
|
+
cwe: "CWE-330"
|
|
1409
|
+
},
|
|
1410
|
+
// ── Destructive Operations ──
|
|
1411
|
+
{
|
|
1412
|
+
name: "rm-rf-root",
|
|
1413
|
+
ruleId: "SEC002",
|
|
1414
|
+
regex: /rm\s+-rf\s+\//,
|
|
1415
|
+
severity: "critical",
|
|
1416
|
+
message: "Recursive delete from root directory",
|
|
1417
|
+
fix: "Use explicit, validated path. Never delete from root.",
|
|
1418
|
+
confidence: 98,
|
|
1419
|
+
excludeInTests: false
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
name: "rm-rf-wildcard",
|
|
1423
|
+
ruleId: "SEC002",
|
|
1424
|
+
regex: /rm\s+-rf\s+\*/,
|
|
1425
|
+
severity: "critical",
|
|
1426
|
+
message: "Recursive delete with wildcard",
|
|
1427
|
+
fix: "Use explicit, validated path.",
|
|
1428
|
+
confidence: 95,
|
|
1429
|
+
excludeInTests: false
|
|
1430
|
+
},
|
|
1431
|
+
// ── Dynamic Require/Import ──
|
|
1432
|
+
{
|
|
1433
|
+
name: "dynamic-require",
|
|
1434
|
+
ruleId: "SEC007",
|
|
1435
|
+
regex: /require\s*\(\s*[^'"][a-zA-Z_$]/,
|
|
1436
|
+
severity: "medium",
|
|
1437
|
+
message: "Dynamic require() \u2014 path injection risk",
|
|
1438
|
+
fix: "Use static imports. Validate path against allowlist if dynamic is needed.",
|
|
1439
|
+
confidence: 72,
|
|
1440
|
+
excludeInTests: true
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: "dynamic-import",
|
|
1444
|
+
ruleId: "SEC007",
|
|
1445
|
+
regex: /import\s*\(\s*[^'"][a-zA-Z_$]/,
|
|
1446
|
+
severity: "medium",
|
|
1447
|
+
message: "Dynamic import() \u2014 path injection risk",
|
|
1448
|
+
fix: "Use static imports when possible. Validate path.",
|
|
1449
|
+
confidence: 68,
|
|
1450
|
+
excludeInTests: true
|
|
1451
|
+
},
|
|
1452
|
+
// ── File Deletion ──
|
|
1453
|
+
{
|
|
1454
|
+
name: "fs-unlink",
|
|
1455
|
+
ruleId: "SEC002",
|
|
1456
|
+
regex: /fs\.(?:unlink|rmdir|rm)(?:Sync)?\s*\(/,
|
|
1457
|
+
severity: "medium",
|
|
1458
|
+
message: "File/directory deletion \u2014 ensure path is validated",
|
|
1459
|
+
fix: "Validate path is within allowed directory before deletion.",
|
|
1460
|
+
confidence: 65,
|
|
1461
|
+
excludeInTests: true,
|
|
1462
|
+
validate: (line) => !line.includes("temp") && !line.includes("tmp")
|
|
1463
|
+
},
|
|
1464
|
+
// ── Weak Crypto ──
|
|
1465
|
+
{
|
|
1466
|
+
name: "md5-usage",
|
|
1467
|
+
ruleId: "SEC009",
|
|
1468
|
+
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/,
|
|
1469
|
+
severity: "medium",
|
|
1470
|
+
message: "MD5 hash \u2014 cryptographically broken",
|
|
1471
|
+
fix: 'Use SHA-256 or better: crypto.createHash("sha256")',
|
|
1472
|
+
confidence: 90,
|
|
1473
|
+
excludeInTests: true,
|
|
1474
|
+
cwe: "CWE-328"
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
name: "sha1-usage",
|
|
1478
|
+
ruleId: "SEC009",
|
|
1479
|
+
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/,
|
|
1480
|
+
severity: "medium",
|
|
1481
|
+
message: "SHA-1 hash \u2014 deprecated for security use",
|
|
1482
|
+
fix: 'Use SHA-256: crypto.createHash("sha256")',
|
|
1483
|
+
confidence: 85,
|
|
1484
|
+
excludeInTests: true,
|
|
1485
|
+
cwe: "CWE-328"
|
|
1486
|
+
},
|
|
1487
|
+
// ── CORS Misconfiguration ──
|
|
1488
|
+
{
|
|
1489
|
+
name: "cors-wildcard",
|
|
1490
|
+
ruleId: "SEC005",
|
|
1491
|
+
regex: /(?:Access-Control-Allow-Origin|origin)\s*[:=]\s*['"`]\*['"`]/,
|
|
1492
|
+
severity: "high",
|
|
1493
|
+
message: "CORS wildcard allows any origin",
|
|
1494
|
+
fix: "Restrict to specific allowed origins.",
|
|
1495
|
+
confidence: 80,
|
|
1496
|
+
excludeInTests: true
|
|
1497
|
+
},
|
|
1498
|
+
// ── NoSQL Injection ──
|
|
1499
|
+
{
|
|
1500
|
+
name: "nosql-injection",
|
|
1501
|
+
ruleId: "SEC001",
|
|
1502
|
+
regex: /\$(?:where|regex|gt|lt|ne|or|and)\s*[:=]\s*(?:req\.|params\.|query\.|body\.)/,
|
|
1503
|
+
severity: "high",
|
|
1504
|
+
message: "NoSQL injection \u2014 MongoDB operator from user input",
|
|
1505
|
+
fix: "Sanitize input. Use mongoose-sanitize or explicit field selection.",
|
|
1506
|
+
confidence: 82,
|
|
1507
|
+
excludeInTests: true,
|
|
1508
|
+
cwe: "CWE-943"
|
|
1509
|
+
}
|
|
1510
|
+
];
|
|
1511
|
+
var securityEngine = {
|
|
1512
|
+
name: "security",
|
|
1513
|
+
description: "Detects injection, XSS, SSRF, prototype pollution, and security anti-patterns",
|
|
1514
|
+
async scan(files, options) {
|
|
1515
|
+
const findings = [];
|
|
1516
|
+
for (const [, file] of files) {
|
|
1517
|
+
const isTest = file.classification.category === "test";
|
|
1518
|
+
const isCritical = file.classification.isCriticalPath;
|
|
1519
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1520
|
+
const line = file.lines[i];
|
|
1521
|
+
const trimmed = line.trim();
|
|
1522
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
1523
|
+
for (const pattern of PATTERNS2) {
|
|
1524
|
+
if (isTest && pattern.excludeInTests) continue;
|
|
1525
|
+
const match = line.match(pattern.regex);
|
|
1526
|
+
if (!match) continue;
|
|
1527
|
+
if (pattern.validate && !pattern.validate(line, file.lines, i)) continue;
|
|
1528
|
+
const rule = getRuleOrDefault(pattern.ruleId);
|
|
1529
|
+
const severity = escalateSeverity(pattern.severity, isCritical);
|
|
1530
|
+
findings.push({
|
|
1531
|
+
id: `${pattern.ruleId}-${file.path}-${i + 1}`,
|
|
1532
|
+
ruleId: pattern.ruleId,
|
|
1533
|
+
engine: "security",
|
|
1534
|
+
category: "security",
|
|
1535
|
+
severity,
|
|
1536
|
+
confidence: pattern.confidence >= 90 ? "certain" : pattern.confidence >= 70 ? "likely" : "possible",
|
|
1537
|
+
confidenceScore: pattern.confidence,
|
|
1538
|
+
file: file.path,
|
|
1539
|
+
line: i + 1,
|
|
1540
|
+
column: match.index,
|
|
1541
|
+
code: trimmed,
|
|
1542
|
+
message: pattern.message,
|
|
1543
|
+
why: rule.why,
|
|
1544
|
+
fix: pattern.fix,
|
|
1545
|
+
autoFixable: false,
|
|
1546
|
+
tags: rule.tags,
|
|
1547
|
+
cwe: pattern.cwe,
|
|
1548
|
+
verified: false,
|
|
1549
|
+
_dedup: `${pattern.name}:${file.path}:${i + 1}`
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return findings;
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
// src/scanner/engines/fake-features.ts
|
|
1559
|
+
var PLACEHOLDER_PATTERNS = [
|
|
1560
|
+
// Match 'placeholder' when used as value, not HTML attribute
|
|
1561
|
+
/[=:]\s*['"]?placeholder['"]?(?:\s*[,;}\]]|$)/i,
|
|
1562
|
+
// Match 'stub' in function/method context
|
|
1563
|
+
/\bstub(?:bed|bing)?\s*(?:function|method|implementation|out)/i,
|
|
1564
|
+
// Match 'dummy' in data contexts
|
|
1565
|
+
/\bdummy[-_]?(?:data|value|user|id|token|response)\b/i,
|
|
1566
|
+
// Match 'fake' in data contexts (not test files)
|
|
1567
|
+
/\bfake[-_]?(?:data|response|user|api|endpoint)\b/i,
|
|
1568
|
+
// Hardcoded credential placeholders
|
|
1569
|
+
/\bhardcoded\s*(?:secret|password|key|token|credential)/i,
|
|
1570
|
+
// TODO with action verbs
|
|
1571
|
+
/\bTODO\b.*(?:implement|fix|complete|replace|remove)/i,
|
|
1572
|
+
// Work in progress markers
|
|
1573
|
+
/\bWIP\b(?:\s*[-:]\s|\s+)/i,
|
|
1574
|
+
// TBD markers
|
|
1575
|
+
/\bTBD\b(?:\s*[-:]\s|\s+)/i,
|
|
1576
|
+
// Not yet implemented
|
|
1577
|
+
/\bNYI\b/i,
|
|
1578
|
+
/\bcoming\s+soon\b/i,
|
|
1579
|
+
// CHANGEME / REPLACEME
|
|
1580
|
+
/\bCHANGE[-_]?ME\b/i,
|
|
1581
|
+
/\bREPLACE[-_]?ME\b/i
|
|
1582
|
+
];
|
|
1583
|
+
var FAKE_SUCCESS_PATTERNS = [
|
|
1584
|
+
// Return true with nothing else in the function
|
|
1585
|
+
/^\s*return\s+true\s*;?\s*$/i,
|
|
1586
|
+
// Hardcoded success without checking
|
|
1587
|
+
/return\s*{\s*success:\s*true\s*}/i,
|
|
1588
|
+
// Always returning ok
|
|
1589
|
+
/return\s*{\s*status:\s*["']ok["']\s*}/i,
|
|
1590
|
+
// Mock/fake success comments
|
|
1591
|
+
/\/\/\s*(?:fake|mock|stub)\s*success/i
|
|
1592
|
+
];
|
|
1593
|
+
var SILENT_FAILURE_PATTERNS = [
|
|
1594
|
+
// Completely empty catch blocks
|
|
1595
|
+
/catch\s*\(\s*(?:_|e|err|error)?\s*\)\s*{\s*}/,
|
|
1596
|
+
// Catch that just returns undefined/null
|
|
1597
|
+
/catch\s*\([^)]*\)\s*{\s*return(?:\s+(?:undefined|null))?\s*;?\s*}/,
|
|
1598
|
+
// Catch with only a comment
|
|
1599
|
+
/catch\s*\([^)]*\)\s*{\s*\/\/[^\n]*\s*}/,
|
|
1600
|
+
// Catch with only console.log (no actual handling)
|
|
1601
|
+
/catch\s*\([^)]*\)\s*{\s*console\.log\([^)]+\)\s*;?\s*}/
|
|
1602
|
+
];
|
|
1603
|
+
var AUTH_BYPASS_PATTERNS = [
|
|
1604
|
+
// Hardcoded admin bypasses
|
|
1605
|
+
/(?:if|&&|\|\|)\s*\(\s*(?:true|1)\s*\)\s*.*(?:admin|auth)/i,
|
|
1606
|
+
// Skip/disable auth flags
|
|
1607
|
+
/\b(?:skip|disable|bypass)Auth(?:entication)?\s*[=:]\s*true\b/i,
|
|
1608
|
+
// Hardcoded isAdmin = true
|
|
1609
|
+
/\b(?:const|let|var)\s+isAdmin\s*=\s*true\b/i,
|
|
1610
|
+
// Auth bypass comments
|
|
1611
|
+
/\/\/\s*(?:bypass|skip|disable)\s*auth/i,
|
|
1612
|
+
// UI-only gating
|
|
1613
|
+
/\/\/\s*(?:ui|client)[-\s]*only\s*(?:check|gating|validation)/i,
|
|
1614
|
+
// Wildcard permissions
|
|
1615
|
+
/permissions?\s*[=:]\s*\[\s*['"]?\*['"]?\s*\]/i,
|
|
1616
|
+
// Always-true auth functions
|
|
1617
|
+
/(?:isAuth|isAdmin|hasPermission|canAccess)\s*[=:]\s*\(\)\s*=>\s*true/i
|
|
1618
|
+
];
|
|
1619
|
+
var DANGEROUS_DEFAULT_PATTERNS = [
|
|
1620
|
+
// Secret env vars with insecure defaults
|
|
1621
|
+
/process\.env\.(?:\w*(?:SECRET|KEY|TOKEN|PASSWORD|CREDENTIAL)\w*)\s*\|\|\s*["'][^'"]{0,50}["']/i,
|
|
1622
|
+
// Database URLs with placeholder passwords
|
|
1623
|
+
/(?:DATABASE|DB|MONGO|POSTGRES|MYSQL|REDIS)_URL\s*[=:]\s*["'][^'"]*(?:password|changeme|replace)[^'"]*["']/i,
|
|
1624
|
+
// Placeholder markers in credentials
|
|
1625
|
+
/(?:api[_-]?key|secret|token|password)\s*[=:]\s*["'](?:CHANGEME|REPLACE_ME|YOUR_[A-Z_]+|xxx+|TODO)["']/i,
|
|
1626
|
+
// Auth endpoints pointing to localhost
|
|
1627
|
+
/(?:AUTH|BILLING|PAYMENT|WEBHOOK)_(?:URL|ENDPOINT)\s*[=:]\s*["']https?:\/\/localhost/i,
|
|
1628
|
+
// Empty string defaults for required secrets
|
|
1629
|
+
/(?:SECRET|API_KEY|TOKEN|PASSWORD)\s*[=:]\s*process\.env\.\w+\s*\|\|\s*["']\s*["']/i
|
|
1630
|
+
];
|
|
1631
|
+
var EMPTY_FUNCTION_PATTERNS = [
|
|
1632
|
+
// async function with empty body
|
|
1633
|
+
/async\s+(?:function\s+\w+|(?:const|let)\s+\w+\s*=\s*async)\s*\([^)]*\)\s*(?::\s*\w+[<>\[\]]*\s*)?\s*{\s*}/,
|
|
1634
|
+
// function with only return undefined
|
|
1635
|
+
/function\s+\w+\s*\([^)]*\)\s*{\s*return\s*;?\s*}/,
|
|
1636
|
+
// arrow function with empty body
|
|
1637
|
+
/=>\s*{\s*}\s*[;,)]/
|
|
1638
|
+
];
|
|
1639
|
+
function isCommentLine(line) {
|
|
1640
|
+
return /^\s*(?:\/\/|\/\*|\*|#|<!--|"""|''')/.test(line);
|
|
1641
|
+
}
|
|
1642
|
+
function isDocContext(filePath, line) {
|
|
1643
|
+
const lp = filePath.toLowerCase();
|
|
1644
|
+
if (/\.(md|mdx|rst|txt)$/.test(lp)) return true;
|
|
1645
|
+
if (lp.includes("/docs/") || lp.includes("/documentation/")) return true;
|
|
1646
|
+
if (lp.includes("/examples/") || lp.includes("/example/")) return true;
|
|
1647
|
+
if (/^\s*\*\s*@(?:example|see|param|returns)/.test(line)) return true;
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
function isInErrorContext(lines, lineIndex) {
|
|
1651
|
+
const before = lines.slice(Math.max(0, lineIndex - 5), lineIndex).join("\n");
|
|
1652
|
+
return /catch|onError|fallback|error|exception/i.test(before);
|
|
1653
|
+
}
|
|
1654
|
+
function validatePlaceholder(line, filePath) {
|
|
1655
|
+
const ll = line.toLowerCase();
|
|
1656
|
+
if (/placeholder\s*[=:]/.test(ll) && !ll.includes("=")) return false;
|
|
1657
|
+
if (ll.includes("type ") || ll.includes("interface ")) return false;
|
|
1658
|
+
return true;
|
|
1659
|
+
}
|
|
1660
|
+
function validateFakeSuccess(line, filePath) {
|
|
1661
|
+
const ll = line.toLowerCase();
|
|
1662
|
+
if (ll.includes("expect(") || ll.includes("assert")) return false;
|
|
1663
|
+
if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
var fakeFeaturesEngine = {
|
|
1667
|
+
name: "fake-features",
|
|
1668
|
+
description: "Detects stubs, fake success, silent failures, auth bypasses, placeholders",
|
|
1669
|
+
async scan(files, options) {
|
|
1670
|
+
const findings = [];
|
|
1671
|
+
for (const [, file] of files) {
|
|
1672
|
+
if (file.classification.category === "test" && !options.includeTests) continue;
|
|
1673
|
+
if (file.classification.category === "documentation") continue;
|
|
1674
|
+
const isCritical = file.classification.isCriticalPath;
|
|
1675
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1676
|
+
const line = file.lines[i];
|
|
1677
|
+
const trimmed = line.trim();
|
|
1678
|
+
if (!isCommentLine(trimmed) || /\b(?:TODO|FIXME|WIP|TBD)\b/i.test(trimmed)) {
|
|
1679
|
+
if (!isDocContext(file.path, trimmed)) {
|
|
1680
|
+
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
1681
|
+
if (pattern.test(line) && validatePlaceholder(line, file.path)) {
|
|
1682
|
+
findings.push(makeFinding("FAKE004", file, i, trimmed, "placeholder", "medium", 65, isCritical));
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (isCommentLine(trimmed)) continue;
|
|
1689
|
+
const inErrorCtx = isInErrorContext(file.lines, i);
|
|
1690
|
+
for (const pattern of FAKE_SUCCESS_PATTERNS) {
|
|
1691
|
+
if (pattern.test(line) && validateFakeSuccess(line, file.path)) {
|
|
1692
|
+
const sev = inErrorCtx ? "high" : "medium";
|
|
1693
|
+
findings.push(makeFinding("FAKE002", file, i, trimmed, "fake-success", sev, inErrorCtx ? 80 : 65, isCritical));
|
|
1694
|
+
break;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
for (const pattern of SILENT_FAILURE_PATTERNS) {
|
|
1698
|
+
if (pattern.test(line)) {
|
|
1699
|
+
if (line.toLowerCase().includes("finally")) continue;
|
|
1700
|
+
findings.push(makeFinding("FAKE003", file, i, trimmed, "silent-failure", "high", 85, isCritical));
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
for (const pattern of AUTH_BYPASS_PATTERNS) {
|
|
1705
|
+
if (pattern.test(line)) {
|
|
1706
|
+
if (file.path.includes(".config.") || file.path.includes(".env")) continue;
|
|
1707
|
+
findings.push(makeFinding("FAKE005", file, i, trimmed, "auth-bypass", "critical", 88, isCritical));
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (const pattern of DANGEROUS_DEFAULT_PATTERNS) {
|
|
1712
|
+
if (pattern.test(line)) {
|
|
1713
|
+
if (file.path.includes(".example") || file.path.includes(".template")) continue;
|
|
1714
|
+
if (file.path.includes(".env.sample")) continue;
|
|
1715
|
+
findings.push(makeFinding("FAKE006", file, i, trimmed, "dangerous-default", "high", 82, isCritical));
|
|
1716
|
+
break;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
for (const pattern of EMPTY_FUNCTION_PATTERNS) {
|
|
1720
|
+
if (pattern.test(line)) {
|
|
1721
|
+
if (/noop|passthrough|abstract|interface|override/i.test(line)) continue;
|
|
1722
|
+
findings.push(makeFinding("FAKE001", file, i, trimmed, "empty-function", "high", 75, isCritical));
|
|
1723
|
+
break;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return findings;
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
function makeFinding(ruleId, file, lineIndex, code, tag, severity, confidence, isCritical) {
|
|
1732
|
+
const rule = getRuleOrDefault(ruleId);
|
|
1733
|
+
const finalSeverity = escalateSeverity(severity, isCritical);
|
|
1734
|
+
return {
|
|
1735
|
+
id: `${ruleId}-${file.path}-${lineIndex + 1}`,
|
|
1736
|
+
ruleId,
|
|
1737
|
+
engine: "fake-features",
|
|
1738
|
+
category: "fake-features",
|
|
1739
|
+
severity: finalSeverity,
|
|
1740
|
+
confidence: confidence >= 85 ? "certain" : confidence >= 65 ? "likely" : "possible",
|
|
1741
|
+
confidenceScore: confidence,
|
|
1742
|
+
file: file.path,
|
|
1743
|
+
line: lineIndex + 1,
|
|
1744
|
+
code,
|
|
1745
|
+
message: rule.description,
|
|
1746
|
+
why: rule.why,
|
|
1747
|
+
fix: rule.fix,
|
|
1748
|
+
autoFixable: rule.autoFixable,
|
|
1749
|
+
tags: [...rule.tags, tag],
|
|
1750
|
+
verified: false,
|
|
1751
|
+
_dedup: `${ruleId}:${file.path}:${lineIndex + 1}`
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/scanner/engines/hallucinations.ts
|
|
1756
|
+
var PATTERNS3 = [
|
|
1757
|
+
// ── Fake npm packages (known hallucinated package names) ──
|
|
1758
|
+
{
|
|
1759
|
+
name: "fake-npm-react-auth",
|
|
1760
|
+
ruleId: "HAL001",
|
|
1761
|
+
regex: /from\s+['"](?:react-auth-provider|react-secure-auth|next-auth-helpers|react-auth-kit-v2)['"]|require\s*\(\s*['"](?:react-auth-provider|react-secure-auth|next-auth-helpers)['"]/,
|
|
1762
|
+
severity: "critical",
|
|
1763
|
+
message: "Import from frequently hallucinated npm package",
|
|
1764
|
+
fix: "Verify package exists on npmjs.com. AI often invents auth/utility packages.",
|
|
1765
|
+
confidence: 88,
|
|
1766
|
+
excludeInTests: false
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
name: "fake-npm-util",
|
|
1770
|
+
ruleId: "HAL001",
|
|
1771
|
+
regex: /from\s+['"](?:string-utils-pro|array-helpers-ts|object-deep-merge|config-loader-pro|api-response-handler)['"]|require\s*\(\s*['"](?:string-utils-pro|array-helpers-ts|object-deep-merge)['"]/,
|
|
1772
|
+
severity: "critical",
|
|
1773
|
+
message: "Import from frequently hallucinated npm package",
|
|
1774
|
+
fix: "Check npmjs.com. These utility package names are commonly invented by AI.",
|
|
1775
|
+
confidence: 85,
|
|
1776
|
+
excludeInTests: false
|
|
1777
|
+
},
|
|
1778
|
+
// ── Placeholder URLs ──
|
|
1779
|
+
{
|
|
1780
|
+
name: "placeholder-url",
|
|
1781
|
+
ruleId: "HAL003",
|
|
1782
|
+
regex: /['"`]https?:\/\/(?:api\.)?example\.com[^'"`]*['"`]/i,
|
|
1783
|
+
severity: "high",
|
|
1784
|
+
message: "Placeholder URL (example.com) in source code",
|
|
1785
|
+
fix: "Replace with actual API endpoint from environment variable.",
|
|
1786
|
+
confidence: 92,
|
|
1787
|
+
excludeInTests: true
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
name: "placeholder-url-yoursite",
|
|
1791
|
+
ruleId: "HAL003",
|
|
1792
|
+
regex: /['"`]https?:\/\/(?:your-?(?:site|app|domain|api)|my-?(?:api|app|site))\.[a-z]+[^'"`]*['"`]/i,
|
|
1793
|
+
severity: "high",
|
|
1794
|
+
message: "Placeholder URL (your-site/my-api) \u2014 AI-generated placeholder",
|
|
1795
|
+
fix: "Replace with actual URL from environment variable.",
|
|
1796
|
+
confidence: 95,
|
|
1797
|
+
excludeInTests: true
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
name: "placeholder-url-todo",
|
|
1801
|
+
ruleId: "HAL003",
|
|
1802
|
+
regex: /['"`]https?:\/\/(?:todo|fixme|placeholder|changeme)\.[a-z]+[^'"`]*['"`]/i,
|
|
1803
|
+
severity: "high",
|
|
1804
|
+
message: "Placeholder URL with marker keyword",
|
|
1805
|
+
fix: "Replace with actual URL.",
|
|
1806
|
+
confidence: 98,
|
|
1807
|
+
excludeInTests: true
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
name: "localhost-in-prod",
|
|
1811
|
+
ruleId: "HAL003",
|
|
1812
|
+
regex: /(?:API_URL|BASE_URL|BACKEND_URL|SERVER_URL)\s*[=:]\s*['"`]https?:\/\/localhost/i,
|
|
1813
|
+
severity: "high",
|
|
1814
|
+
message: "Localhost URL in production configuration",
|
|
1815
|
+
fix: "Use environment variable: process.env.API_URL",
|
|
1816
|
+
confidence: 80,
|
|
1817
|
+
excludeInTests: true,
|
|
1818
|
+
validate: (line, file) => !file.path.includes(".env") && !file.path.includes(".local")
|
|
1819
|
+
},
|
|
1820
|
+
// ── Invented API methods ──
|
|
1821
|
+
{
|
|
1822
|
+
name: "prisma-hallucinated",
|
|
1823
|
+
ruleId: "HAL002",
|
|
1824
|
+
regex: /prisma\.\w+\.(?:getAll|fetchAll|search|getOne|fetchOne|removeAll|updateAll|createOrUpdate|findFirst|getById)\s*\(/,
|
|
1825
|
+
severity: "high",
|
|
1826
|
+
message: "Possible hallucinated Prisma method \u2014 verify against Prisma docs",
|
|
1827
|
+
fix: "Prisma uses findMany, findUnique, create, update, delete, upsert. Check docs.",
|
|
1828
|
+
confidence: 65,
|
|
1829
|
+
excludeInTests: true,
|
|
1830
|
+
validate: (line) => {
|
|
1831
|
+
if (line.includes("findFirst")) return false;
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
name: "express-hallucinated",
|
|
1837
|
+
ruleId: "HAL002",
|
|
1838
|
+
regex: /(?:app|router)\.(?:handle|serve|mount|listen)\s*\(\s*['"]\/\w/,
|
|
1839
|
+
severity: "medium",
|
|
1840
|
+
message: "Possible hallucinated Express method \u2014 verify against Express docs",
|
|
1841
|
+
fix: "Express uses .get(), .post(), .put(), .delete(), .use(), .all(). Check docs.",
|
|
1842
|
+
confidence: 60,
|
|
1843
|
+
excludeInTests: true
|
|
1844
|
+
},
|
|
1845
|
+
// ── Invented config options ──
|
|
1846
|
+
{
|
|
1847
|
+
name: "next-config-hallucinated",
|
|
1848
|
+
ruleId: "HAL004",
|
|
1849
|
+
regex: /(?:next\.config|nextConfig)\s*(?:=|\.)\s*\{[^}]*(?:enableSSR|enableCSR|autoOptimize|smartBundling|lazyHydration|autoCache)/,
|
|
1850
|
+
severity: "medium",
|
|
1851
|
+
message: "Possible hallucinated Next.js config option",
|
|
1852
|
+
fix: "Check next.config.js docs for valid options.",
|
|
1853
|
+
confidence: 70,
|
|
1854
|
+
excludeInTests: true
|
|
1855
|
+
},
|
|
1856
|
+
// ── Fake test data in production ──
|
|
1857
|
+
{
|
|
1858
|
+
name: "test-email-prod",
|
|
1859
|
+
ruleId: "HAL003",
|
|
1860
|
+
regex: /['"`](?:test|user|admin|john|jane|foo|bar)@(?:test|example|fake|dummy|placeholder)\.\w+['"`]/i,
|
|
1861
|
+
severity: "medium",
|
|
1862
|
+
message: "Test/fake email address in production code",
|
|
1863
|
+
fix: "Replace with actual user data or remove.",
|
|
1864
|
+
confidence: 78,
|
|
1865
|
+
excludeInTests: true
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
name: "test-phone-prod",
|
|
1869
|
+
ruleId: "HAL003",
|
|
1870
|
+
regex: /['"`]\+?1?[-.\s]?\(?(?:555|000|123)\)?[-.\s]?\d{3}[-.\s]?\d{4}['"`]/,
|
|
1871
|
+
severity: "medium",
|
|
1872
|
+
message: "Fake phone number (555/000/123 prefix) in production code",
|
|
1873
|
+
fix: "Replace with actual data or remove.",
|
|
1874
|
+
confidence: 82,
|
|
1875
|
+
excludeInTests: true
|
|
1876
|
+
},
|
|
1877
|
+
// ── Lorem ipsum ──
|
|
1878
|
+
{
|
|
1879
|
+
name: "lorem-ipsum",
|
|
1880
|
+
ruleId: "HAL003",
|
|
1881
|
+
regex: /lorem\s+ipsum/i,
|
|
1882
|
+
severity: "medium",
|
|
1883
|
+
message: "Lorem ipsum placeholder text in production code",
|
|
1884
|
+
fix: "Replace with actual content.",
|
|
1885
|
+
confidence: 95,
|
|
1886
|
+
excludeInTests: true
|
|
1887
|
+
}
|
|
1888
|
+
];
|
|
1889
|
+
var hallucinationsEngine = {
|
|
1890
|
+
name: "hallucinations",
|
|
1891
|
+
description: "Detects AI-hallucinated code: fake packages, invented methods, placeholder URLs",
|
|
1892
|
+
async scan(files, options) {
|
|
1893
|
+
const findings = [];
|
|
1894
|
+
for (const [, file] of files) {
|
|
1895
|
+
const isTest = file.classification.category === "test";
|
|
1896
|
+
const isCritical = file.classification.isCriticalPath;
|
|
1897
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1898
|
+
const line = file.lines[i];
|
|
1899
|
+
const trimmed = line.trim();
|
|
1900
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1901
|
+
for (const pattern of PATTERNS3) {
|
|
1902
|
+
if (isTest && pattern.excludeInTests) continue;
|
|
1903
|
+
const match = line.match(pattern.regex);
|
|
1904
|
+
if (!match) continue;
|
|
1905
|
+
if (pattern.validate && !pattern.validate(line, file)) continue;
|
|
1906
|
+
const rule = getRuleOrDefault(pattern.ruleId);
|
|
1907
|
+
const severity = escalateSeverity(pattern.severity, isCritical);
|
|
1908
|
+
findings.push({
|
|
1909
|
+
id: `${pattern.ruleId}-${file.path}-${i + 1}`,
|
|
1910
|
+
ruleId: pattern.ruleId,
|
|
1911
|
+
engine: "hallucinations",
|
|
1912
|
+
category: "hallucinations",
|
|
1913
|
+
severity,
|
|
1914
|
+
confidence: pattern.confidence >= 85 ? "certain" : pattern.confidence >= 65 ? "likely" : "possible",
|
|
1915
|
+
confidenceScore: pattern.confidence,
|
|
1916
|
+
file: file.path,
|
|
1917
|
+
line: i + 1,
|
|
1918
|
+
column: match.index,
|
|
1919
|
+
code: trimmed,
|
|
1920
|
+
message: pattern.message,
|
|
1921
|
+
why: rule.why,
|
|
1922
|
+
fix: pattern.fix,
|
|
1923
|
+
autoFixable: false,
|
|
1924
|
+
tags: [...rule.tags],
|
|
1925
|
+
verified: false,
|
|
1926
|
+
_dedup: `${pattern.name}:${file.path}:${i + 1}`
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return findings;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
// src/scanner/engines/dead-ui.ts
|
|
1936
|
+
var LEGIT_HASH_LINK = [/aria-controls=/i, /role=["']tab["']/i, /role=["']button["']/i, /data-toggle=/i, /data-bs-toggle=/i, /onClick\s*=\s*\{[^}]+\}/, /tabIndex=/i];
|
|
1937
|
+
var LEGIT_NOOP = [/disabled/i, /aria-disabled/i, /loading/i, /pending/i, /isLoading/i, /stopPropagation/i, /preventDefault/i];
|
|
1938
|
+
var LEGIT_COMING_SOON = [/changelog/i, /readme/i, /roadmap/i, /\/\//, /\/\*/, /translation/i, /i18n/i, /t\(['"`]/i];
|
|
1939
|
+
var LEGIT_DISABLED = [/aria-disabled/i, /isDisabled/i, /isLoading/i, /disabled=\{/i, /&&\s*disabled/i, /\?\s*disabled/i];
|
|
1940
|
+
var LEGIT_FETCH = [/useQuery/i, /useMutation/i, /useSWR/i, /createApi/i, /trpc/i, /\.server\./i, /route\.ts$/i, /api\/.*\.ts$/i];
|
|
1941
|
+
function hasContext(line, patterns) {
|
|
1942
|
+
return patterns.some((p) => p.test(line));
|
|
1943
|
+
}
|
|
1944
|
+
var deadUIEngine = {
|
|
1945
|
+
name: "dead-ui",
|
|
1946
|
+
description: "Detects dead links, noop handlers, coming soon UI, disabled without reason",
|
|
1947
|
+
async scan(files, options) {
|
|
1948
|
+
const findings = [];
|
|
1949
|
+
for (const [, file] of files) {
|
|
1950
|
+
if (!isUIFile(file.path)) continue;
|
|
1951
|
+
if (file.classification.category === "test" && !options.includeTests) continue;
|
|
1952
|
+
if (file.classification.category === "documentation") continue;
|
|
1953
|
+
const isApiFile = /\/api\/|\/routes\/|\.server\./.test(file.path.toLowerCase());
|
|
1954
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
1955
|
+
const line = file.lines[i];
|
|
1956
|
+
const trimmed = line.trim();
|
|
1957
|
+
if (/href=["']#["']|href=["']javascript:void\(0\)["']/g.test(line)) {
|
|
1958
|
+
if (!hasContext(line, LEGIT_HASH_LINK)) {
|
|
1959
|
+
findings.push(make("UI001", file, i, trimmed, "high", 88));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (/onClick\s*=\s*\{\s*\(\)\s*=>\s*\{\s*\}\s*\}/.test(line)) {
|
|
1963
|
+
if (!hasContext(line, LEGIT_NOOP)) {
|
|
1964
|
+
findings.push(make("UI002", file, i, trimmed, "high", 90));
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (/coming\s+soon|under\s+construction|not\s+available|work\s+in\s+progress/gi.test(line)) {
|
|
1968
|
+
if (!hasContext(line, LEGIT_COMING_SOON) && !trimmed.startsWith("//") && !trimmed.startsWith("*")) {
|
|
1969
|
+
findings.push(make("UI003", file, i, trimmed, "medium", 72));
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (line.includes("disabled") && !/tooltip|aria-label|aria-describedby|title|reason/i.test(line)) {
|
|
1973
|
+
if (!hasContext(line, LEGIT_DISABLED) && !/disabled=\{[^}]*\}/.test(line)) {
|
|
1974
|
+
findings.push(make("UI004", file, i, trimmed, "medium", 68));
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (!isApiFile && /fetch\s*\(\s*["'`]\/api\//.test(line)) {
|
|
1978
|
+
if (!hasContext(line, LEGIT_FETCH)) {
|
|
1979
|
+
findings.push(make("UI005", file, i, trimmed, "low", 60));
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return findings;
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
function make(ruleId, file, lineIdx, code, severity, confidence) {
|
|
1988
|
+
const rule = getRuleOrDefault(ruleId);
|
|
1989
|
+
return {
|
|
1990
|
+
id: `${ruleId}-${file.path}-${lineIdx + 1}`,
|
|
1991
|
+
ruleId,
|
|
1992
|
+
engine: "dead-ui",
|
|
1993
|
+
category: "dead-ui",
|
|
1994
|
+
severity,
|
|
1995
|
+
confidence: confidence >= 85 ? "certain" : confidence >= 65 ? "likely" : "possible",
|
|
1996
|
+
confidenceScore: confidence,
|
|
1997
|
+
file: file.path,
|
|
1998
|
+
line: lineIdx + 1,
|
|
1999
|
+
code,
|
|
2000
|
+
message: rule.description,
|
|
2001
|
+
why: rule.why,
|
|
2002
|
+
fix: rule.fix,
|
|
2003
|
+
autoFixable: false,
|
|
2004
|
+
tags: rule.tags,
|
|
2005
|
+
verified: false,
|
|
2006
|
+
_dedup: `${ruleId}:${file.path}:${lineIdx + 1}`
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/scanner/engines/code-quality.ts
|
|
2011
|
+
var PATTERNS4 = [
|
|
2012
|
+
// ── Debug Code ──
|
|
2013
|
+
{
|
|
2014
|
+
name: "console-log",
|
|
2015
|
+
ruleId: "QLT001",
|
|
2016
|
+
regex: /\bconsole\.log\s*\(/,
|
|
2017
|
+
severity: "medium",
|
|
2018
|
+
message: "console.log() in production code",
|
|
2019
|
+
fix: "Remove or replace with proper logger (winston, pino)",
|
|
2020
|
+
confidence: 92,
|
|
2021
|
+
excludeInTests: true,
|
|
2022
|
+
autoFixable: true
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
name: "console-debug",
|
|
2026
|
+
ruleId: "QLT001",
|
|
2027
|
+
regex: /\bconsole\.debug\s*\(/,
|
|
2028
|
+
severity: "medium",
|
|
2029
|
+
message: "console.debug() in production code",
|
|
2030
|
+
fix: "Remove or replace with proper logger",
|
|
2031
|
+
confidence: 92,
|
|
2032
|
+
excludeInTests: true,
|
|
2033
|
+
autoFixable: true
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
name: "console-trace",
|
|
2037
|
+
ruleId: "QLT001",
|
|
2038
|
+
regex: /\bconsole\.trace\s*\(/,
|
|
2039
|
+
severity: "medium",
|
|
2040
|
+
message: "console.trace() in production code",
|
|
2041
|
+
fix: "Remove or use proper debugging tools",
|
|
2042
|
+
confidence: 92,
|
|
2043
|
+
excludeInTests: true,
|
|
2044
|
+
autoFixable: true
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
name: "debugger",
|
|
2048
|
+
ruleId: "QLT002",
|
|
2049
|
+
regex: /\bdebugger\b\s*;?/,
|
|
2050
|
+
severity: "high",
|
|
2051
|
+
message: "debugger statement left in code",
|
|
2052
|
+
fix: "Remove the debugger statement",
|
|
2053
|
+
confidence: 98,
|
|
2054
|
+
excludeInTests: true,
|
|
2055
|
+
autoFixable: true
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
name: "alert-call",
|
|
2059
|
+
ruleId: "QLT001",
|
|
2060
|
+
regex: /\balert\s*\(\s*['"`]/,
|
|
2061
|
+
severity: "medium",
|
|
2062
|
+
message: "alert() call in production code",
|
|
2063
|
+
fix: "Use a proper notification/toast component",
|
|
2064
|
+
confidence: 85,
|
|
2065
|
+
excludeInTests: true,
|
|
2066
|
+
autoFixable: false
|
|
2067
|
+
},
|
|
2068
|
+
// ── Type Safety ──
|
|
2069
|
+
{
|
|
2070
|
+
name: "as-any",
|
|
2071
|
+
ruleId: "QLT005",
|
|
2072
|
+
regex: /\bas\s+any\b/,
|
|
2073
|
+
severity: "medium",
|
|
2074
|
+
message: '"as any" bypasses TypeScript safety',
|
|
2075
|
+
fix: "Fix the type properly. Use type guards or generics.",
|
|
2076
|
+
confidence: 88,
|
|
2077
|
+
excludeInTests: true,
|
|
2078
|
+
autoFixable: false
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
name: "ts-ignore",
|
|
2082
|
+
ruleId: "QLT005",
|
|
2083
|
+
regex: /@ts-ignore(?!\s*\()/,
|
|
2084
|
+
severity: "medium",
|
|
2085
|
+
message: "@ts-ignore suppresses type errors without explanation",
|
|
2086
|
+
fix: "Use @ts-expect-error with explanation, or fix the type.",
|
|
2087
|
+
confidence: 90,
|
|
2088
|
+
excludeInTests: true,
|
|
2089
|
+
autoFixable: false
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
name: "any-type-annotation",
|
|
2093
|
+
ruleId: "QLT005",
|
|
2094
|
+
regex: /:\s*any\b(?!\s*\[\])/,
|
|
2095
|
+
severity: "low",
|
|
2096
|
+
message: 'Explicit "any" type annotation',
|
|
2097
|
+
fix: "Use a proper type. Use unknown for truly dynamic values.",
|
|
2098
|
+
confidence: 75,
|
|
2099
|
+
excludeInTests: true,
|
|
2100
|
+
autoFixable: false
|
|
2101
|
+
},
|
|
2102
|
+
// ── Mock Data in Production ──
|
|
2103
|
+
{
|
|
2104
|
+
name: "mock-data-array",
|
|
2105
|
+
ruleId: "QLT003",
|
|
2106
|
+
regex: /(?:const|let|var)\s+(?:mock|fake|dummy|sample|test)(?:Data|Users?|Items?|Products?|Orders?)\s*[=:]/i,
|
|
2107
|
+
severity: "high",
|
|
2108
|
+
message: "Mock/fake data variable in production code",
|
|
2109
|
+
fix: "Connect to actual data source. Move mock data to test files.",
|
|
2110
|
+
confidence: 80,
|
|
2111
|
+
excludeInTests: true,
|
|
2112
|
+
autoFixable: false
|
|
2113
|
+
},
|
|
2114
|
+
{
|
|
2115
|
+
name: "hardcoded-user",
|
|
2116
|
+
ruleId: "QLT003",
|
|
2117
|
+
regex: /(?:name|email|username)\s*:\s*['"`](?:John\s*Doe|Jane\s*Doe|Test\s*User|Admin\s*User|user@example\.com)['"`]/i,
|
|
2118
|
+
severity: "high",
|
|
2119
|
+
message: "Hardcoded fake user data in production code",
|
|
2120
|
+
fix: "Replace with actual user data from database/auth.",
|
|
2121
|
+
confidence: 85,
|
|
2122
|
+
excludeInTests: true,
|
|
2123
|
+
autoFixable: false
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
name: "hardcoded-price",
|
|
2127
|
+
ruleId: "QLT003",
|
|
2128
|
+
regex: /(?:price|amount|cost)\s*:\s*(?:9\.99|19\.99|29\.99|49\.99|99\.99|0\.00)\b/i,
|
|
2129
|
+
severity: "medium",
|
|
2130
|
+
message: "Hardcoded price/amount \u2014 likely mock data",
|
|
2131
|
+
fix: "Fetch pricing from database/config.",
|
|
2132
|
+
confidence: 72,
|
|
2133
|
+
excludeInTests: true,
|
|
2134
|
+
autoFixable: false
|
|
2135
|
+
},
|
|
2136
|
+
// ── Error Handling Smells ──
|
|
2137
|
+
{
|
|
2138
|
+
name: "throw-string",
|
|
2139
|
+
ruleId: "QLT001",
|
|
2140
|
+
regex: /throw\s+['"`][^'"`]+['"`]/,
|
|
2141
|
+
severity: "medium",
|
|
2142
|
+
message: "Throwing a string instead of an Error object",
|
|
2143
|
+
fix: 'Throw an Error: throw new Error("message")',
|
|
2144
|
+
confidence: 92,
|
|
2145
|
+
excludeInTests: true,
|
|
2146
|
+
autoFixable: true
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
name: "promise-no-catch",
|
|
2150
|
+
ruleId: "QLT001",
|
|
2151
|
+
regex: /\.then\s*\([^)]*\)\s*(?:$|;|\n)(?!.*\.catch)/,
|
|
2152
|
+
severity: "medium",
|
|
2153
|
+
message: "Promise chain without .catch() \u2014 unhandled rejection risk",
|
|
2154
|
+
fix: "Add .catch() handler or use async/await with try/catch.",
|
|
2155
|
+
confidence: 68,
|
|
2156
|
+
excludeInTests: true,
|
|
2157
|
+
autoFixable: false
|
|
2158
|
+
},
|
|
2159
|
+
// ── Dead Code Indicators ──
|
|
2160
|
+
{
|
|
2161
|
+
name: "unreachable-code",
|
|
2162
|
+
ruleId: "QLT004",
|
|
2163
|
+
regex: /^\s*(?:const|let|var|function|class|return|throw)\b.*$/,
|
|
2164
|
+
severity: "low",
|
|
2165
|
+
message: "Potentially unreachable code after return/throw",
|
|
2166
|
+
fix: "Remove dead code.",
|
|
2167
|
+
confidence: 45,
|
|
2168
|
+
excludeInTests: true,
|
|
2169
|
+
autoFixable: true
|
|
2170
|
+
},
|
|
2171
|
+
// ── Performance ──
|
|
2172
|
+
{
|
|
2173
|
+
name: "sync-fs-in-handler",
|
|
2174
|
+
ruleId: "QLT001",
|
|
2175
|
+
regex: /(?:readFileSync|writeFileSync|existsSync|readdirSync|statSync)\s*\(/,
|
|
2176
|
+
severity: "medium",
|
|
2177
|
+
message: "Synchronous file operation \u2014 blocks event loop",
|
|
2178
|
+
fix: "Use async version: fs.promises.readFile() or fs.readFile()",
|
|
2179
|
+
confidence: 70,
|
|
2180
|
+
excludeInTests: true,
|
|
2181
|
+
autoFixable: false
|
|
2182
|
+
}
|
|
2183
|
+
];
|
|
2184
|
+
var codeQualityEngine = {
|
|
2185
|
+
name: "code-quality",
|
|
2186
|
+
description: "Detects debug code, type safety issues, mock data, and quality anti-patterns",
|
|
2187
|
+
async scan(files, options) {
|
|
2188
|
+
const findings = [];
|
|
2189
|
+
for (const [, file] of files) {
|
|
2190
|
+
const isTest = file.classification.category === "test";
|
|
2191
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2192
|
+
const line = file.lines[i];
|
|
2193
|
+
const trimmed = line.trim();
|
|
2194
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
2195
|
+
for (const pattern of PATTERNS4) {
|
|
2196
|
+
if (isTest && pattern.excludeInTests) continue;
|
|
2197
|
+
const match = line.match(pattern.regex);
|
|
2198
|
+
if (!match) continue;
|
|
2199
|
+
if (pattern.name === "unreachable-code") {
|
|
2200
|
+
if (i === 0 || !/\b(?:return|throw)\b/.test(file.lines[i - 1])) continue;
|
|
2201
|
+
}
|
|
2202
|
+
const rule = getRuleOrDefault(pattern.ruleId);
|
|
2203
|
+
findings.push({
|
|
2204
|
+
id: `${pattern.ruleId}-${file.path}-${i + 1}`,
|
|
2205
|
+
ruleId: pattern.ruleId,
|
|
2206
|
+
engine: "code-quality",
|
|
2207
|
+
category: "code-quality",
|
|
2208
|
+
severity: pattern.severity,
|
|
2209
|
+
confidence: pattern.confidence >= 85 ? "certain" : pattern.confidence >= 65 ? "likely" : "possible",
|
|
2210
|
+
confidenceScore: pattern.confidence,
|
|
2211
|
+
file: file.path,
|
|
2212
|
+
line: i + 1,
|
|
2213
|
+
column: match.index,
|
|
2214
|
+
code: trimmed,
|
|
2215
|
+
message: pattern.message,
|
|
2216
|
+
why: rule.why,
|
|
2217
|
+
fix: pattern.fix,
|
|
2218
|
+
autoFixable: pattern.autoFixable,
|
|
2219
|
+
tags: rule.tags,
|
|
2220
|
+
verified: false,
|
|
2221
|
+
_dedup: `${pattern.name}:${file.path}:${i + 1}`
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return findings;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
// src/scanner/engines/import-graph.ts
|
|
2231
|
+
var import_fs = require("fs");
|
|
2232
|
+
var import_path2 = require("path");
|
|
2233
|
+
function extractImports(file) {
|
|
2234
|
+
const edges = [];
|
|
2235
|
+
const importRegex = /(?:import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
2236
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2237
|
+
const line = file.lines[i];
|
|
2238
|
+
let match;
|
|
2239
|
+
importRegex.lastIndex = 0;
|
|
2240
|
+
const lineRegex = /(?:import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
2241
|
+
while ((match = lineRegex.exec(line)) !== null) {
|
|
2242
|
+
const specifier = match[1] || match[2] || match[3];
|
|
2243
|
+
if (specifier) {
|
|
2244
|
+
edges.push({
|
|
2245
|
+
from: file.path,
|
|
2246
|
+
to: specifier,
|
|
2247
|
+
specifier,
|
|
2248
|
+
line: i + 1
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return edges;
|
|
2254
|
+
}
|
|
2255
|
+
function resolveImport(from, specifier, fileMap) {
|
|
2256
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) return null;
|
|
2257
|
+
const fromDir = (0, import_path2.dirname)(from);
|
|
2258
|
+
const resolved = (0, import_path2.join)(fromDir, specifier).replace(/\\/g, "/");
|
|
2259
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
2260
|
+
for (const ext of extensions) {
|
|
2261
|
+
const candidate = resolved + ext;
|
|
2262
|
+
const normalized = candidate.replace(/\\/g, "/");
|
|
2263
|
+
if (fileMap.has(normalized)) return normalized;
|
|
2264
|
+
const winNormalized = candidate.replace(/\//g, "\\");
|
|
2265
|
+
if (fileMap.has(winNormalized)) return winNormalized;
|
|
2266
|
+
}
|
|
2267
|
+
for (const ext of extensions) {
|
|
2268
|
+
const candidate = (resolved.startsWith("./") ? resolved.slice(2) : resolved) + ext;
|
|
2269
|
+
if (fileMap.has(candidate)) return candidate;
|
|
2270
|
+
}
|
|
2271
|
+
return null;
|
|
2272
|
+
}
|
|
2273
|
+
function findCircularDeps(adjacency) {
|
|
2274
|
+
const cycles = [];
|
|
2275
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2276
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
2277
|
+
const path = [];
|
|
2278
|
+
function dfs(node) {
|
|
2279
|
+
if (inStack.has(node)) {
|
|
2280
|
+
const cycleStart = path.indexOf(node);
|
|
2281
|
+
if (cycleStart >= 0) {
|
|
2282
|
+
cycles.push(path.slice(cycleStart).concat(node));
|
|
2283
|
+
}
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (visited.has(node)) return;
|
|
2287
|
+
visited.add(node);
|
|
2288
|
+
inStack.add(node);
|
|
2289
|
+
path.push(node);
|
|
2290
|
+
for (const neighbor of adjacency.get(node) ?? []) {
|
|
2291
|
+
dfs(neighbor);
|
|
2292
|
+
}
|
|
2293
|
+
path.pop();
|
|
2294
|
+
inStack.delete(node);
|
|
2295
|
+
}
|
|
2296
|
+
for (const node of adjacency.keys()) {
|
|
2297
|
+
dfs(node);
|
|
2298
|
+
}
|
|
2299
|
+
return cycles;
|
|
2300
|
+
}
|
|
2301
|
+
function extractFetchRoutes(file) {
|
|
2302
|
+
const routes = [];
|
|
2303
|
+
const fetchRegex = /(?:fetch|axios\.(?:get|post|put|patch|delete)|\.get|\.post|\.put|\.patch|\.delete)\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g;
|
|
2304
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2305
|
+
let match;
|
|
2306
|
+
fetchRegex.lastIndex = 0;
|
|
2307
|
+
while ((match = fetchRegex.exec(file.lines[i])) !== null) {
|
|
2308
|
+
routes.push({ route: match[1], line: i + 1 });
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return routes;
|
|
2312
|
+
}
|
|
2313
|
+
function extractRouteHandlers(file) {
|
|
2314
|
+
const handlers = [];
|
|
2315
|
+
const handlerRegex = /(?:app|router|server)\s*\.\s*(?:get|post|put|patch|delete|all)\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g;
|
|
2316
|
+
for (const line of file.lines) {
|
|
2317
|
+
let match;
|
|
2318
|
+
handlerRegex.lastIndex = 0;
|
|
2319
|
+
while ((match = handlerRegex.exec(line)) !== null) {
|
|
2320
|
+
handlers.push(match[1]);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
const normalized = file.path.replace(/\\/g, "/");
|
|
2324
|
+
if (normalized.includes("/api/") || normalized.includes("/routes/")) {
|
|
2325
|
+
const apiMatch = normalized.match(/(?:pages|app)(\/api\/[^.]+)/);
|
|
2326
|
+
if (apiMatch) {
|
|
2327
|
+
handlers.push(apiMatch[1].replace(/\/route$/, "").replace(/\/index$/, ""));
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return handlers;
|
|
2331
|
+
}
|
|
2332
|
+
function extractEnvRefs(file) {
|
|
2333
|
+
const refs = [];
|
|
2334
|
+
const envRegex = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
2335
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2336
|
+
const line = file.lines[i];
|
|
2337
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
2338
|
+
let match;
|
|
2339
|
+
envRegex.lastIndex = 0;
|
|
2340
|
+
while ((match = envRegex.exec(line)) !== null) {
|
|
2341
|
+
refs.push({ name: match[1], line: i + 1 });
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return refs;
|
|
2345
|
+
}
|
|
2346
|
+
function loadEnvVars(projectRoot) {
|
|
2347
|
+
const vars = /* @__PURE__ */ new Set();
|
|
2348
|
+
const envFiles = [".env", ".env.local", ".env.development", ".env.production", ".env.example"];
|
|
2349
|
+
for (const envFile of envFiles) {
|
|
2350
|
+
const envPath = (0, import_path2.join)(projectRoot, envFile);
|
|
2351
|
+
try {
|
|
2352
|
+
if ((0, import_fs.existsSync)(envPath)) {
|
|
2353
|
+
const content = (0, import_fs.readFileSync)(envPath, "utf-8");
|
|
2354
|
+
for (const line of content.split("\n")) {
|
|
2355
|
+
const match = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=/);
|
|
2356
|
+
if (match) vars.add(match[1]);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const builtins = [
|
|
2363
|
+
"NODE_ENV",
|
|
2364
|
+
"PORT",
|
|
2365
|
+
"HOME",
|
|
2366
|
+
"PATH",
|
|
2367
|
+
"PWD",
|
|
2368
|
+
"USER",
|
|
2369
|
+
"SHELL",
|
|
2370
|
+
"TERM",
|
|
2371
|
+
"LANG",
|
|
2372
|
+
"CI",
|
|
2373
|
+
"GITHUB_ACTIONS",
|
|
2374
|
+
"VERCEL",
|
|
2375
|
+
"NETLIFY",
|
|
2376
|
+
"HEROKU",
|
|
2377
|
+
"npm_package_version"
|
|
2378
|
+
];
|
|
2379
|
+
for (const b of builtins) vars.add(b);
|
|
2380
|
+
return vars;
|
|
2381
|
+
}
|
|
2382
|
+
function isBarrelFile(file) {
|
|
2383
|
+
if (!file.path.endsWith("index.ts") && !file.path.endsWith("index.js")) {
|
|
2384
|
+
return { isBarrel: false, reExportCount: 0 };
|
|
2385
|
+
}
|
|
2386
|
+
let reExportCount = 0;
|
|
2387
|
+
for (const line of file.lines) {
|
|
2388
|
+
if (/^export\s+\*\s+from\s+/.test(line.trim()) || /^export\s+\{[^}]+\}\s+from\s+/.test(line.trim())) {
|
|
2389
|
+
reExportCount++;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return { isBarrel: reExportCount >= 5, reExportCount };
|
|
2393
|
+
}
|
|
2394
|
+
var importGraphEngine = {
|
|
2395
|
+
name: "import-graph",
|
|
2396
|
+
description: "Cross-file import graph analysis: circular deps, orphans, ghost routes, ghost env vars",
|
|
2397
|
+
async scan(files, options) {
|
|
2398
|
+
const findings = [];
|
|
2399
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2400
|
+
const allImported = /* @__PURE__ */ new Set();
|
|
2401
|
+
const allEdges = [];
|
|
2402
|
+
for (const [path, file] of files) {
|
|
2403
|
+
const edges = extractImports(file);
|
|
2404
|
+
const resolved = [];
|
|
2405
|
+
for (const edge of edges) {
|
|
2406
|
+
const target = resolveImport(path, edge.specifier, files);
|
|
2407
|
+
if (target) {
|
|
2408
|
+
resolved.push(target);
|
|
2409
|
+
allImported.add(target);
|
|
2410
|
+
allEdges.push({ ...edge, to: target });
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
adjacency.set(path, resolved);
|
|
2414
|
+
}
|
|
2415
|
+
const cycles = findCircularDeps(adjacency);
|
|
2416
|
+
const reportedCycles = /* @__PURE__ */ new Set();
|
|
2417
|
+
for (const cycle of cycles) {
|
|
2418
|
+
const cycleKey = [...cycle].sort().join("\u2192");
|
|
2419
|
+
if (reportedCycles.has(cycleKey)) continue;
|
|
2420
|
+
reportedCycles.add(cycleKey);
|
|
2421
|
+
const file = files.get(cycle[0]);
|
|
2422
|
+
if (!file) continue;
|
|
2423
|
+
const rule = getRuleOrDefault("IG001");
|
|
2424
|
+
const cyclePath = cycle.map((p) => p.split("/").pop()).join(" \u2192 ");
|
|
2425
|
+
findings.push({
|
|
2426
|
+
id: `IG001-${cycle[0]}-cycle`,
|
|
2427
|
+
ruleId: "IG001",
|
|
2428
|
+
engine: "import-graph",
|
|
2429
|
+
category: "import-graph",
|
|
2430
|
+
severity: "high",
|
|
2431
|
+
confidence: "certain",
|
|
2432
|
+
confidenceScore: 95,
|
|
2433
|
+
file: cycle[0],
|
|
2434
|
+
line: 1,
|
|
2435
|
+
code: `Circular: ${cyclePath}`,
|
|
2436
|
+
message: `Circular dependency chain: ${cyclePath}`,
|
|
2437
|
+
why: rule.why,
|
|
2438
|
+
fix: rule.fix,
|
|
2439
|
+
autoFixable: false,
|
|
2440
|
+
tags: rule.tags,
|
|
2441
|
+
verified: false,
|
|
2442
|
+
_dedup: `IG001:${cycleKey}`
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
const entryPatterns = /(?:index|main|app|server|cli|entry)\.[tj]sx?$/;
|
|
2446
|
+
for (const [path, file] of files) {
|
|
2447
|
+
if (entryPatterns.test(path)) continue;
|
|
2448
|
+
if (file.classification.category === "test") continue;
|
|
2449
|
+
if (file.classification.category === "config") continue;
|
|
2450
|
+
if (path.includes(".config.")) continue;
|
|
2451
|
+
if (path.includes(".d.ts")) continue;
|
|
2452
|
+
if (!allImported.has(path)) {
|
|
2453
|
+
const rule = getRuleOrDefault("IG002");
|
|
2454
|
+
findings.push({
|
|
2455
|
+
id: `IG002-${path}`,
|
|
2456
|
+
ruleId: "IG002",
|
|
2457
|
+
engine: "import-graph",
|
|
2458
|
+
category: "import-graph",
|
|
2459
|
+
severity: "medium",
|
|
2460
|
+
confidence: "likely",
|
|
2461
|
+
confidenceScore: 75,
|
|
2462
|
+
file: path,
|
|
2463
|
+
line: 1,
|
|
2464
|
+
code: path,
|
|
2465
|
+
message: `Orphan module: ${path.split(/[/\\]/).pop()} is never imported`,
|
|
2466
|
+
why: rule.why,
|
|
2467
|
+
fix: rule.fix,
|
|
2468
|
+
autoFixable: false,
|
|
2469
|
+
tags: rule.tags,
|
|
2470
|
+
verified: false,
|
|
2471
|
+
_dedup: `IG002:${path}`
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
const allFetchedRoutes = [];
|
|
2476
|
+
const allHandlerRoutes = /* @__PURE__ */ new Set();
|
|
2477
|
+
for (const [, file] of files) {
|
|
2478
|
+
const fetched = extractFetchRoutes(file);
|
|
2479
|
+
for (const f of fetched) {
|
|
2480
|
+
allFetchedRoutes.push({ ...f, file: file.path });
|
|
2481
|
+
}
|
|
2482
|
+
const handlers = extractRouteHandlers(file);
|
|
2483
|
+
for (const h of handlers) {
|
|
2484
|
+
allHandlerRoutes.add(h);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
for (const fetchedRoute of allFetchedRoutes) {
|
|
2488
|
+
const normalized = fetchedRoute.route.replace(/\/$/g, "").replace(/\/:[^/]+/g, "/[param]");
|
|
2489
|
+
const hasHandler = [...allHandlerRoutes].some((handler) => {
|
|
2490
|
+
const normHandler = handler.replace(/\/$/g, "").replace(/\/:[^/]+/g, "/[param]");
|
|
2491
|
+
return normHandler === normalized || normalized.startsWith(normHandler);
|
|
2492
|
+
});
|
|
2493
|
+
if (!hasHandler && allHandlerRoutes.size > 0) {
|
|
2494
|
+
const rule = getRuleOrDefault("IG003");
|
|
2495
|
+
findings.push({
|
|
2496
|
+
id: `IG003-${fetchedRoute.file}-${fetchedRoute.line}`,
|
|
2497
|
+
ruleId: "IG003",
|
|
2498
|
+
engine: "import-graph",
|
|
2499
|
+
category: "import-graph",
|
|
2500
|
+
severity: "high",
|
|
2501
|
+
confidence: "likely",
|
|
2502
|
+
confidenceScore: 80,
|
|
2503
|
+
file: fetchedRoute.file,
|
|
2504
|
+
line: fetchedRoute.line,
|
|
2505
|
+
code: `fetch("${fetchedRoute.route}")`,
|
|
2506
|
+
message: `Ghost route: ${fetchedRoute.route} has no matching backend handler`,
|
|
2507
|
+
why: rule.why,
|
|
2508
|
+
fix: rule.fix,
|
|
2509
|
+
autoFixable: false,
|
|
2510
|
+
tags: rule.tags,
|
|
2511
|
+
verified: false,
|
|
2512
|
+
_dedup: `IG003:${fetchedRoute.file}:${fetchedRoute.route}`
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const declaredEnvVars = loadEnvVars(options.projectRoot);
|
|
2517
|
+
const allEnvRefs = [];
|
|
2518
|
+
for (const [, file] of files) {
|
|
2519
|
+
const refs = extractEnvRefs(file);
|
|
2520
|
+
for (const ref of refs) {
|
|
2521
|
+
allEnvRefs.push({ ...ref, file: file.path });
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const reportedEnvVars = /* @__PURE__ */ new Set();
|
|
2525
|
+
for (const ref of allEnvRefs) {
|
|
2526
|
+
if (declaredEnvVars.has(ref.name)) continue;
|
|
2527
|
+
if (reportedEnvVars.has(ref.name)) continue;
|
|
2528
|
+
reportedEnvVars.add(ref.name);
|
|
2529
|
+
const rule = getRuleOrDefault("IG004");
|
|
2530
|
+
findings.push({
|
|
2531
|
+
id: `IG004-${ref.file}-${ref.line}`,
|
|
2532
|
+
ruleId: "IG004",
|
|
2533
|
+
engine: "import-graph",
|
|
2534
|
+
category: "import-graph",
|
|
2535
|
+
severity: "high",
|
|
2536
|
+
confidence: "likely",
|
|
2537
|
+
confidenceScore: 82,
|
|
2538
|
+
file: ref.file,
|
|
2539
|
+
line: ref.line,
|
|
2540
|
+
code: `process.env.${ref.name}`,
|
|
2541
|
+
message: `Ghost env variable: ${ref.name} not found in any .env file`,
|
|
2542
|
+
why: rule.why,
|
|
2543
|
+
fix: rule.fix,
|
|
2544
|
+
autoFixable: true,
|
|
2545
|
+
tags: rule.tags,
|
|
2546
|
+
verified: false,
|
|
2547
|
+
_dedup: `IG004:${ref.name}`
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
for (const [path, file] of files) {
|
|
2551
|
+
const { isBarrel, reExportCount } = isBarrelFile(file);
|
|
2552
|
+
if (isBarrel) {
|
|
2553
|
+
const rule = getRuleOrDefault("IG005");
|
|
2554
|
+
findings.push({
|
|
2555
|
+
id: `IG005-${path}`,
|
|
2556
|
+
ruleId: "IG005",
|
|
2557
|
+
engine: "import-graph",
|
|
2558
|
+
category: "import-graph",
|
|
2559
|
+
severity: "low",
|
|
2560
|
+
confidence: "likely",
|
|
2561
|
+
confidenceScore: 78,
|
|
2562
|
+
file: path,
|
|
2563
|
+
line: 1,
|
|
2564
|
+
code: `${reExportCount} re-exports from index file`,
|
|
2565
|
+
message: `Barrel file with ${reExportCount} re-exports may defeat tree-shaking`,
|
|
2566
|
+
why: rule.why,
|
|
2567
|
+
fix: rule.fix,
|
|
2568
|
+
autoFixable: false,
|
|
2569
|
+
tags: rule.tags,
|
|
2570
|
+
verified: false,
|
|
2571
|
+
_dedup: `IG005:${path}`
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return findings;
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
|
|
2579
|
+
// src/scanner/engines/runtime-verify.ts
|
|
2580
|
+
function extractExports(file) {
|
|
2581
|
+
const exports2 = [];
|
|
2582
|
+
const exportRegex = /export\s+(?:async\s+)?(?:function|const|let|var|class|enum|type|interface)\s+([A-Za-z_$][A-Za-z0-9_$]*)/;
|
|
2583
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2584
|
+
const line = file.lines[i];
|
|
2585
|
+
const match = line.match(exportRegex);
|
|
2586
|
+
if (match) {
|
|
2587
|
+
exports2.push({ name: match[1], line: i + 1 });
|
|
2588
|
+
}
|
|
2589
|
+
const namedExport = line.match(/export\s+\{([^}]+)\}/);
|
|
2590
|
+
if (namedExport) {
|
|
2591
|
+
const names = namedExport[1].split(",").map((n) => n.trim().split(/\s+as\s+/)[0].trim());
|
|
2592
|
+
for (const name of names) {
|
|
2593
|
+
if (name && /^[A-Za-z_$]/.test(name)) {
|
|
2594
|
+
exports2.push({ name, line: i + 1 });
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
return exports2;
|
|
2600
|
+
}
|
|
2601
|
+
function extractImportedSymbols(file) {
|
|
2602
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
2603
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
2604
|
+
const defaultImportRegex = /import\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from/g;
|
|
2605
|
+
for (const line of file.lines) {
|
|
2606
|
+
let match;
|
|
2607
|
+
importRegex.lastIndex = 0;
|
|
2608
|
+
while ((match = importRegex.exec(line)) !== null) {
|
|
2609
|
+
const names = match[1].split(",").map((n) => {
|
|
2610
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
2611
|
+
return parts[0].trim();
|
|
2612
|
+
});
|
|
2613
|
+
for (const name of names) {
|
|
2614
|
+
if (name) symbols.add(name);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
defaultImportRegex.lastIndex = 0;
|
|
2618
|
+
while ((match = defaultImportRegex.exec(line)) !== null) {
|
|
2619
|
+
symbols.add(match[1]);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return symbols;
|
|
2623
|
+
}
|
|
2624
|
+
function findAsyncFunctions(file) {
|
|
2625
|
+
const functions = [];
|
|
2626
|
+
const asyncDeclRegex = /(?:export\s+)?async\s+function\s+([A-Za-z_$][A-Za-z0-9_$]*)/;
|
|
2627
|
+
const asyncArrowRegex = /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*async/;
|
|
2628
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2629
|
+
const line = file.lines[i];
|
|
2630
|
+
const declMatch = line.match(asyncDeclRegex);
|
|
2631
|
+
const arrowMatch = line.match(asyncArrowRegex);
|
|
2632
|
+
const match = declMatch || arrowMatch;
|
|
2633
|
+
if (match) {
|
|
2634
|
+
const name = match[1];
|
|
2635
|
+
const startLine = i + 1;
|
|
2636
|
+
let braceCount = 0;
|
|
2637
|
+
let hasAwait = false;
|
|
2638
|
+
let endLine = startLine;
|
|
2639
|
+
let started = false;
|
|
2640
|
+
for (let j = i; j < file.lines.length; j++) {
|
|
2641
|
+
const fLine = file.lines[j];
|
|
2642
|
+
for (const ch of fLine) {
|
|
2643
|
+
if (ch === "{") {
|
|
2644
|
+
braceCount++;
|
|
2645
|
+
started = true;
|
|
2646
|
+
}
|
|
2647
|
+
if (ch === "}") braceCount--;
|
|
2648
|
+
}
|
|
2649
|
+
if (/\bawait\b/.test(fLine) && j > i) {
|
|
2650
|
+
hasAwait = true;
|
|
2651
|
+
}
|
|
2652
|
+
if (started && braceCount === 0) {
|
|
2653
|
+
endLine = j + 1;
|
|
2654
|
+
break;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (/\bawait\b/.test(line) && line.indexOf("await") > line.indexOf("async")) {
|
|
2658
|
+
hasAwait = true;
|
|
2659
|
+
}
|
|
2660
|
+
functions.push({ name, startLine, hasAwait, endLine });
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return functions;
|
|
2664
|
+
}
|
|
2665
|
+
function isInsideTryCatch(lines, lineIndex) {
|
|
2666
|
+
let braceDepth = 0;
|
|
2667
|
+
for (let i = lineIndex; i >= 0; i--) {
|
|
2668
|
+
const line = lines[i];
|
|
2669
|
+
for (let j = line.length - 1; j >= 0; j--) {
|
|
2670
|
+
if (line[j] === "}") braceDepth++;
|
|
2671
|
+
if (line[j] === "{") braceDepth--;
|
|
2672
|
+
}
|
|
2673
|
+
if (braceDepth < 0 && /\btry\b/.test(lines[i])) return true;
|
|
2674
|
+
if (braceDepth < 0 && /\bcatch\b/.test(lines[i])) return true;
|
|
2675
|
+
}
|
|
2676
|
+
return false;
|
|
2677
|
+
}
|
|
2678
|
+
var runtimeVerifyEngine = {
|
|
2679
|
+
name: "runtime-verify",
|
|
2680
|
+
description: "Static analysis for runtime issues: unhandled promises, dead exports, async bugs",
|
|
2681
|
+
async scan(files, options) {
|
|
2682
|
+
const findings = [];
|
|
2683
|
+
const allImportedSymbols = /* @__PURE__ */ new Set();
|
|
2684
|
+
for (const [, file] of files) {
|
|
2685
|
+
const symbols = extractImportedSymbols(file);
|
|
2686
|
+
for (const s of symbols) allImportedSymbols.add(s);
|
|
2687
|
+
}
|
|
2688
|
+
for (const [path, file] of files) {
|
|
2689
|
+
if (![".ts", ".tsx", ".js", ".jsx"].includes(file.ext)) continue;
|
|
2690
|
+
const asyncFunctions = findAsyncFunctions(file);
|
|
2691
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2692
|
+
const line = file.lines[i];
|
|
2693
|
+
const trimmed = line.trim();
|
|
2694
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
2695
|
+
if (/\.then\s*\(/.test(trimmed) && !/.catch\s*\(/.test(trimmed)) {
|
|
2696
|
+
const nextLines = file.lines.slice(i + 1, i + 4).join(" ");
|
|
2697
|
+
if (!/.catch\s*\(/.test(nextLines)) {
|
|
2698
|
+
const rule = getRuleOrDefault("RV001");
|
|
2699
|
+
findings.push({
|
|
2700
|
+
id: `RV001-${path}-${i + 1}`,
|
|
2701
|
+
ruleId: "RV001",
|
|
2702
|
+
engine: "runtime-verify",
|
|
2703
|
+
category: "runtime-verify",
|
|
2704
|
+
severity: "high",
|
|
2705
|
+
confidence: "likely",
|
|
2706
|
+
confidenceScore: 82,
|
|
2707
|
+
file: path,
|
|
2708
|
+
line: i + 1,
|
|
2709
|
+
code: trimmed,
|
|
2710
|
+
message: "Promise .then() without .catch() \u2014 rejection will be unhandled",
|
|
2711
|
+
why: rule.why,
|
|
2712
|
+
fix: rule.fix,
|
|
2713
|
+
autoFixable: false,
|
|
2714
|
+
tags: rule.tags,
|
|
2715
|
+
verified: false,
|
|
2716
|
+
_dedup: `RV001:${path}:${i + 1}`
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (/\bawait\b/.test(trimmed) && !isInsideTryCatch(file.lines, i)) {
|
|
2721
|
+
const rule = getRuleOrDefault("RV001");
|
|
2722
|
+
findings.push({
|
|
2723
|
+
id: `RV001-await-${path}-${i + 1}`,
|
|
2724
|
+
ruleId: "RV001",
|
|
2725
|
+
engine: "runtime-verify",
|
|
2726
|
+
category: "runtime-verify",
|
|
2727
|
+
severity: "medium",
|
|
2728
|
+
confidence: "possible",
|
|
2729
|
+
confidenceScore: 65,
|
|
2730
|
+
file: path,
|
|
2731
|
+
line: i + 1,
|
|
2732
|
+
code: trimmed,
|
|
2733
|
+
message: "await without surrounding try/catch \u2014 rejection may be unhandled",
|
|
2734
|
+
why: rule.why,
|
|
2735
|
+
fix: rule.fix,
|
|
2736
|
+
autoFixable: false,
|
|
2737
|
+
tags: rule.tags,
|
|
2738
|
+
verified: false,
|
|
2739
|
+
_dedup: `RV001-await:${path}:${i + 1}`
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
for (const fn of asyncFunctions) {
|
|
2744
|
+
if (!fn.hasAwait && fn.endLine - fn.startLine > 1) {
|
|
2745
|
+
const rule = getRuleOrDefault("RV002");
|
|
2746
|
+
findings.push({
|
|
2747
|
+
id: `RV002-${path}-${fn.startLine}`,
|
|
2748
|
+
ruleId: "RV002",
|
|
2749
|
+
engine: "runtime-verify",
|
|
2750
|
+
category: "runtime-verify",
|
|
2751
|
+
severity: "medium",
|
|
2752
|
+
confidence: "likely",
|
|
2753
|
+
confidenceScore: 80,
|
|
2754
|
+
file: path,
|
|
2755
|
+
line: fn.startLine,
|
|
2756
|
+
code: file.lines[fn.startLine - 1]?.trim() ?? "",
|
|
2757
|
+
message: `Async function '${fn.name}' never uses await`,
|
|
2758
|
+
why: rule.why,
|
|
2759
|
+
fix: rule.fix,
|
|
2760
|
+
autoFixable: false,
|
|
2761
|
+
tags: rule.tags,
|
|
2762
|
+
verified: false,
|
|
2763
|
+
_dedup: `RV002:${path}:${fn.startLine}`
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
const exports2 = extractExports(file);
|
|
2768
|
+
const isEntry = /(?:index|main|app|server|cli)\.[tj]sx?$/.test(path);
|
|
2769
|
+
if (!isEntry) {
|
|
2770
|
+
for (const exp of exports2) {
|
|
2771
|
+
if (!allImportedSymbols.has(exp.name)) {
|
|
2772
|
+
const rule = getRuleOrDefault("RV003");
|
|
2773
|
+
findings.push({
|
|
2774
|
+
id: `RV003-${path}-${exp.line}`,
|
|
2775
|
+
ruleId: "RV003",
|
|
2776
|
+
engine: "runtime-verify",
|
|
2777
|
+
category: "runtime-verify",
|
|
2778
|
+
severity: "medium",
|
|
2779
|
+
confidence: "possible",
|
|
2780
|
+
confidenceScore: 68,
|
|
2781
|
+
file: path,
|
|
2782
|
+
line: exp.line,
|
|
2783
|
+
code: file.lines[exp.line - 1]?.trim() ?? "",
|
|
2784
|
+
message: `Dead export: '${exp.name}' is exported but never imported`,
|
|
2785
|
+
why: rule.why,
|
|
2786
|
+
fix: rule.fix,
|
|
2787
|
+
autoFixable: false,
|
|
2788
|
+
tags: rule.tags,
|
|
2789
|
+
verified: false,
|
|
2790
|
+
_dedup: `RV003:${path}:${exp.name}`
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
const asyncFuncNames = new Set(asyncFunctions.map((f) => f.name));
|
|
2796
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2797
|
+
const line = file.lines[i];
|
|
2798
|
+
const trimmed = line.trim();
|
|
2799
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
2800
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("export ")) continue;
|
|
2801
|
+
if (/^(?:async\s+)?function\s/.test(trimmed)) continue;
|
|
2802
|
+
for (const asyncName of asyncFuncNames) {
|
|
2803
|
+
const callRegex = new RegExp(`(?<!await\\s)(?<!void\\s)\\b${asyncName}\\s*\\(`);
|
|
2804
|
+
if (callRegex.test(trimmed) && !trimmed.includes(".then(") && !trimmed.includes("await ")) {
|
|
2805
|
+
if (/(?:const|let|var|return)\s/.test(trimmed)) continue;
|
|
2806
|
+
const rule = getRuleOrDefault("RV004");
|
|
2807
|
+
findings.push({
|
|
2808
|
+
id: `RV004-${path}-${i + 1}`,
|
|
2809
|
+
ruleId: "RV004",
|
|
2810
|
+
engine: "runtime-verify",
|
|
2811
|
+
category: "runtime-verify",
|
|
2812
|
+
severity: "high",
|
|
2813
|
+
confidence: "likely",
|
|
2814
|
+
confidenceScore: 78,
|
|
2815
|
+
file: path,
|
|
2816
|
+
line: i + 1,
|
|
2817
|
+
code: trimmed,
|
|
2818
|
+
message: `Floating promise: '${asyncName}()' called without await or .then()`,
|
|
2819
|
+
why: rule.why,
|
|
2820
|
+
fix: rule.fix,
|
|
2821
|
+
autoFixable: false,
|
|
2822
|
+
tags: rule.tags,
|
|
2823
|
+
verified: false,
|
|
2824
|
+
_dedup: `RV004:${path}:${i + 1}`
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
const moduleVars = [];
|
|
2830
|
+
let depth = 0;
|
|
2831
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2832
|
+
const line = file.lines[i];
|
|
2833
|
+
for (const ch of line) {
|
|
2834
|
+
if (ch === "{") depth++;
|
|
2835
|
+
if (ch === "}") depth--;
|
|
2836
|
+
}
|
|
2837
|
+
if (depth === 0) {
|
|
2838
|
+
const varMatch = line.match(/(?:let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)/);
|
|
2839
|
+
if (varMatch) {
|
|
2840
|
+
moduleVars.push({ name: varMatch[1], line: i + 1 });
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
for (const fn of asyncFunctions) {
|
|
2845
|
+
for (const v of moduleVars) {
|
|
2846
|
+
for (let j = fn.startLine - 1; j < fn.endLine && j < file.lines.length; j++) {
|
|
2847
|
+
const fLine = file.lines[j];
|
|
2848
|
+
const writeRegex = new RegExp(`\\b${v.name}\\s*(?:=|\\+=|-=|\\+\\+|--)`);
|
|
2849
|
+
if (writeRegex.test(fLine)) {
|
|
2850
|
+
const rule = getRuleOrDefault("RV005");
|
|
2851
|
+
findings.push({
|
|
2852
|
+
id: `RV005-${path}-${j + 1}`,
|
|
2853
|
+
ruleId: "RV005",
|
|
2854
|
+
engine: "runtime-verify",
|
|
2855
|
+
category: "runtime-verify",
|
|
2856
|
+
severity: "medium",
|
|
2857
|
+
confidence: "possible",
|
|
2858
|
+
confidenceScore: 65,
|
|
2859
|
+
file: path,
|
|
2860
|
+
line: j + 1,
|
|
2861
|
+
code: fLine.trim(),
|
|
2862
|
+
message: `Race condition risk: async function '${fn.name}' mutates shared variable '${v.name}'`,
|
|
2863
|
+
why: rule.why,
|
|
2864
|
+
fix: rule.fix,
|
|
2865
|
+
autoFixable: false,
|
|
2866
|
+
tags: rule.tags,
|
|
2867
|
+
verified: false,
|
|
2868
|
+
_dedup: `RV005:${path}:${v.name}:${fn.name}`
|
|
2869
|
+
});
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return findings;
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
// src/scanner/engines/auto-fix.ts
|
|
2881
|
+
var import_fs2 = require("fs");
|
|
2882
|
+
var import_path3 = require("path");
|
|
2883
|
+
var fixStrategies = [
|
|
2884
|
+
// ── QLT001: Remove console.log ──
|
|
2885
|
+
{
|
|
2886
|
+
ruleId: "QLT001",
|
|
2887
|
+
apply(finding, lines) {
|
|
2888
|
+
const lineIdx = finding.line - 1;
|
|
2889
|
+
const line = lines[lineIdx];
|
|
2890
|
+
if (!line) return null;
|
|
2891
|
+
const trimmed = line.trim();
|
|
2892
|
+
if (/^\s*console\.(log|debug|trace|info|warn)\s*\(/.test(line)) {
|
|
2893
|
+
let fullStatement = trimmed;
|
|
2894
|
+
let endIdx = lineIdx;
|
|
2895
|
+
let parenCount = 0;
|
|
2896
|
+
for (let i = lineIdx; i < lines.length; i++) {
|
|
2897
|
+
for (const ch of lines[i]) {
|
|
2898
|
+
if (ch === "(") parenCount++;
|
|
2899
|
+
if (ch === ")") parenCount--;
|
|
2900
|
+
}
|
|
2901
|
+
if (parenCount <= 0) {
|
|
2902
|
+
endIdx = i;
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return {
|
|
2907
|
+
replacement: endIdx === lineIdx ? "" : "",
|
|
2908
|
+
description: `Remove ${trimmed.split("(")[0]}() statement`
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2913
|
+
},
|
|
2914
|
+
// ── QLT002: Remove debugger ──
|
|
2915
|
+
{
|
|
2916
|
+
ruleId: "QLT002",
|
|
2917
|
+
apply(finding, lines) {
|
|
2918
|
+
const lineIdx = finding.line - 1;
|
|
2919
|
+
const line = lines[lineIdx];
|
|
2920
|
+
if (!line) return null;
|
|
2921
|
+
if (/^\s*debugger\s*;?\s*$/.test(line)) {
|
|
2922
|
+
return {
|
|
2923
|
+
replacement: "",
|
|
2924
|
+
description: "Remove debugger statement"
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
return null;
|
|
2928
|
+
}
|
|
2929
|
+
},
|
|
2930
|
+
// ── SEC009: Replace Math.random() with crypto.randomUUID() ──
|
|
2931
|
+
{
|
|
2932
|
+
ruleId: "SEC009",
|
|
2933
|
+
apply(finding, lines) {
|
|
2934
|
+
const lineIdx = finding.line - 1;
|
|
2935
|
+
const line = lines[lineIdx];
|
|
2936
|
+
if (!line) return null;
|
|
2937
|
+
if (/Math\.random\(\)/.test(line)) {
|
|
2938
|
+
const fixed = line.replace(
|
|
2939
|
+
/Math\.random\(\)\.toString\(\d*\)(?:\.slice\(\d+\))?/g,
|
|
2940
|
+
"crypto.randomUUID()"
|
|
2941
|
+
).replace(
|
|
2942
|
+
/Math\.random\(\)/g,
|
|
2943
|
+
"crypto.randomUUID()"
|
|
2944
|
+
);
|
|
2945
|
+
if (fixed !== line) {
|
|
2946
|
+
return {
|
|
2947
|
+
replacement: fixed,
|
|
2948
|
+
description: "Replace Math.random() with crypto.randomUUID()"
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
return null;
|
|
2953
|
+
}
|
|
2954
|
+
},
|
|
2955
|
+
// ── IG004/HAL006: Add missing env var to .env.example ──
|
|
2956
|
+
{
|
|
2957
|
+
ruleId: "IG004",
|
|
2958
|
+
apply(finding, _lines, projectRoot) {
|
|
2959
|
+
const envVarMatch = finding.code.match(/process\.env\.([A-Z_][A-Z0-9_]*)/);
|
|
2960
|
+
if (!envVarMatch) return null;
|
|
2961
|
+
const varName = envVarMatch[1];
|
|
2962
|
+
const envExamplePath = (0, import_path3.join)(projectRoot, ".env.example");
|
|
2963
|
+
if ((0, import_fs2.existsSync)(envExamplePath)) {
|
|
2964
|
+
const content = (0, import_fs2.readFileSync)(envExamplePath, "utf-8");
|
|
2965
|
+
if (content.includes(`${varName}=`)) return null;
|
|
2966
|
+
}
|
|
2967
|
+
return {
|
|
2968
|
+
replacement: `${varName}=`,
|
|
2969
|
+
description: `Add ${varName} to .env.example`
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
];
|
|
2974
|
+
function applyFixes(options) {
|
|
2975
|
+
const startTime = Date.now();
|
|
2976
|
+
const fixResults = [];
|
|
2977
|
+
const fixable = options.findings.filter((f) => f.autoFixable);
|
|
2978
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2979
|
+
for (const f of fixable) {
|
|
2980
|
+
if (!byFile.has(f.file)) byFile.set(f.file, []);
|
|
2981
|
+
byFile.get(f.file).push(f);
|
|
2982
|
+
}
|
|
2983
|
+
const envVarsToAdd = [];
|
|
2984
|
+
for (const [filePath, findings] of byFile) {
|
|
2985
|
+
const absPath = (0, import_path3.join)(options.projectRoot, filePath);
|
|
2986
|
+
let lines;
|
|
2987
|
+
try {
|
|
2988
|
+
const content = (0, import_fs2.readFileSync)(absPath, "utf-8");
|
|
2989
|
+
lines = content.split("\n");
|
|
2990
|
+
} catch {
|
|
2991
|
+
for (const f of findings) {
|
|
2992
|
+
fixResults.push({
|
|
2993
|
+
findingId: f.id,
|
|
2994
|
+
ruleId: f.ruleId,
|
|
2995
|
+
file: filePath,
|
|
2996
|
+
line: f.line,
|
|
2997
|
+
original: f.code,
|
|
2998
|
+
replacement: "",
|
|
2999
|
+
applied: false,
|
|
3000
|
+
description: "File not readable, skipped"
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
const sorted = [...findings].sort((a, b) => b.line - a.line);
|
|
3006
|
+
let modified = false;
|
|
3007
|
+
for (const finding of sorted) {
|
|
3008
|
+
const strategy = fixStrategies.find((s) => s.ruleId === finding.ruleId);
|
|
3009
|
+
if (!strategy) {
|
|
3010
|
+
fixResults.push({
|
|
3011
|
+
findingId: finding.id,
|
|
3012
|
+
ruleId: finding.ruleId,
|
|
3013
|
+
file: filePath,
|
|
3014
|
+
line: finding.line,
|
|
3015
|
+
original: finding.code,
|
|
3016
|
+
replacement: "",
|
|
3017
|
+
applied: false,
|
|
3018
|
+
description: "No fix strategy available"
|
|
3019
|
+
});
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
const result = strategy.apply(finding, lines, options.projectRoot);
|
|
3023
|
+
if (!result) {
|
|
3024
|
+
fixResults.push({
|
|
3025
|
+
findingId: finding.id,
|
|
3026
|
+
ruleId: finding.ruleId,
|
|
3027
|
+
file: filePath,
|
|
3028
|
+
line: finding.line,
|
|
3029
|
+
original: finding.code,
|
|
3030
|
+
replacement: "",
|
|
3031
|
+
applied: false,
|
|
3032
|
+
description: "Fix not applicable to this code pattern"
|
|
3033
|
+
});
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
const lineIdx = finding.line - 1;
|
|
3037
|
+
const original = lines[lineIdx] ?? "";
|
|
3038
|
+
if (finding.ruleId === "IG004" || finding.ruleId === "HAL006") {
|
|
3039
|
+
envVarsToAdd.push(result.replacement);
|
|
3040
|
+
fixResults.push({
|
|
3041
|
+
findingId: finding.id,
|
|
3042
|
+
ruleId: finding.ruleId,
|
|
3043
|
+
file: ".env.example",
|
|
3044
|
+
line: 0,
|
|
3045
|
+
original: "",
|
|
3046
|
+
replacement: result.replacement,
|
|
3047
|
+
applied: !options.dryRun,
|
|
3048
|
+
description: result.description
|
|
3049
|
+
});
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
if (result.replacement === "") {
|
|
3053
|
+
lines.splice(lineIdx, 1);
|
|
3054
|
+
} else {
|
|
3055
|
+
lines[lineIdx] = result.replacement;
|
|
3056
|
+
}
|
|
3057
|
+
modified = true;
|
|
3058
|
+
fixResults.push({
|
|
3059
|
+
findingId: finding.id,
|
|
3060
|
+
ruleId: finding.ruleId,
|
|
3061
|
+
file: filePath,
|
|
3062
|
+
line: finding.line,
|
|
3063
|
+
original,
|
|
3064
|
+
replacement: result.replacement || "(line removed)",
|
|
3065
|
+
applied: !options.dryRun,
|
|
3066
|
+
description: result.description
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
if (modified && !options.dryRun) {
|
|
3070
|
+
try {
|
|
3071
|
+
(0, import_fs2.writeFileSync)(absPath, lines.join("\n"), "utf-8");
|
|
3072
|
+
} catch {
|
|
3073
|
+
for (const r of fixResults) {
|
|
3074
|
+
if (r.file === filePath) r.applied = false;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
if (envVarsToAdd.length > 0 && !options.dryRun) {
|
|
3080
|
+
const envExamplePath = (0, import_path3.join)(options.projectRoot, ".env.example");
|
|
3081
|
+
try {
|
|
3082
|
+
const additions = envVarsToAdd.map((v) => `${v}
|
|
3083
|
+
`).join("");
|
|
3084
|
+
if ((0, import_fs2.existsSync)(envExamplePath)) {
|
|
3085
|
+
(0, import_fs2.appendFileSync)(envExamplePath, `
|
|
3086
|
+
# Added by VibeCheck auto-fix
|
|
3087
|
+
${additions}`, "utf-8");
|
|
3088
|
+
} else {
|
|
3089
|
+
(0, import_fs2.writeFileSync)(envExamplePath, `# Environment Variables
|
|
3090
|
+
# Added by VibeCheck auto-fix
|
|
3091
|
+
${additions}`, "utf-8");
|
|
3092
|
+
}
|
|
3093
|
+
} catch {
|
|
3094
|
+
for (const r of fixResults) {
|
|
3095
|
+
if (r.file === ".env.example") r.applied = false;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const applied = fixResults.filter((r) => r.applied).length;
|
|
3100
|
+
return {
|
|
3101
|
+
totalFixable: fixable.length,
|
|
3102
|
+
applied,
|
|
3103
|
+
skipped: fixable.length - applied,
|
|
3104
|
+
fixes: fixResults,
|
|
3105
|
+
durationMs: Date.now() - startTime
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// src/scanner/index.ts
|
|
3110
|
+
var ALL_ENGINES = [
|
|
3111
|
+
credentialsEngine,
|
|
3112
|
+
securityEngine,
|
|
3113
|
+
fakeFeaturesEngine,
|
|
3114
|
+
hallucinationsEngine,
|
|
3115
|
+
deadUIEngine,
|
|
3116
|
+
codeQualityEngine,
|
|
3117
|
+
importGraphEngine,
|
|
3118
|
+
runtimeVerifyEngine
|
|
3119
|
+
];
|
|
3120
|
+
var DEFAULT_EXCLUDE = [
|
|
3121
|
+
"node_modules",
|
|
3122
|
+
".git",
|
|
3123
|
+
"dist",
|
|
3124
|
+
"build",
|
|
3125
|
+
".next",
|
|
3126
|
+
".nuxt",
|
|
3127
|
+
"coverage",
|
|
3128
|
+
"__pycache__",
|
|
3129
|
+
".venv",
|
|
3130
|
+
"venv",
|
|
3131
|
+
".output"
|
|
3132
|
+
];
|
|
3133
|
+
function loadFiles(projectRoot, options) {
|
|
3134
|
+
const files = /* @__PURE__ */ new Map();
|
|
3135
|
+
const exclude = options.exclude ?? DEFAULT_EXCLUDE;
|
|
3136
|
+
const maxFileSize = 512 * 1024;
|
|
3137
|
+
function walk(dir) {
|
|
3138
|
+
let entries;
|
|
3139
|
+
try {
|
|
3140
|
+
entries = (0, import_fs3.readdirSync)(dir);
|
|
3141
|
+
} catch {
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
for (const entry of entries) {
|
|
3145
|
+
const fullPath = (0, import_path4.join)(dir, entry);
|
|
3146
|
+
if (exclude.some((e) => entry === e || entry.startsWith("."))) {
|
|
3147
|
+
try {
|
|
3148
|
+
if ((0, import_fs3.statSync)(fullPath).isDirectory()) continue;
|
|
3149
|
+
} catch {
|
|
3150
|
+
continue;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
let stat;
|
|
3154
|
+
try {
|
|
3155
|
+
stat = (0, import_fs3.statSync)(fullPath);
|
|
3156
|
+
} catch {
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
if (stat.isDirectory()) {
|
|
3160
|
+
walk(fullPath);
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3163
|
+
if (!isCodeFile(fullPath)) continue;
|
|
3164
|
+
if (stat.size > maxFileSize) continue;
|
|
3165
|
+
const relativePath = (0, import_path4.relative)(projectRoot, fullPath);
|
|
3166
|
+
const classification = classifyPath(relativePath);
|
|
3167
|
+
if (classification.excludeByDefault && classification.category !== "test") continue;
|
|
3168
|
+
if (classification.category === "test" && !options.includeTests) continue;
|
|
3169
|
+
try {
|
|
3170
|
+
const content = (0, import_fs3.readFileSync)(fullPath, "utf-8");
|
|
3171
|
+
const hash = (0, import_crypto.createHash)("md5").update(content).digest("hex");
|
|
3172
|
+
files.set(relativePath, {
|
|
3173
|
+
path: relativePath,
|
|
3174
|
+
absolutePath: fullPath,
|
|
3175
|
+
content,
|
|
3176
|
+
lines: content.split("\n"),
|
|
3177
|
+
ext: (0, import_path4.extname)(fullPath).toLowerCase(),
|
|
3178
|
+
hash,
|
|
3179
|
+
classification
|
|
3180
|
+
});
|
|
3181
|
+
} catch {
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
if (options.files && options.files.length > 0) {
|
|
3186
|
+
for (const filePath of options.files) {
|
|
3187
|
+
const fullPath = (0, import_path4.join)(projectRoot, filePath);
|
|
3188
|
+
if (!(0, import_fs3.existsSync)(fullPath)) continue;
|
|
3189
|
+
try {
|
|
3190
|
+
const content = (0, import_fs3.readFileSync)(fullPath, "utf-8");
|
|
3191
|
+
const hash = (0, import_crypto.createHash)("md5").update(content).digest("hex");
|
|
3192
|
+
const classification = classifyPath(filePath);
|
|
3193
|
+
files.set(filePath, {
|
|
3194
|
+
path: filePath,
|
|
3195
|
+
absolutePath: fullPath,
|
|
3196
|
+
content,
|
|
3197
|
+
lines: content.split("\n"),
|
|
3198
|
+
ext: (0, import_path4.extname)(fullPath).toLowerCase(),
|
|
3199
|
+
hash,
|
|
3200
|
+
classification
|
|
3201
|
+
});
|
|
3202
|
+
} catch {
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
} else {
|
|
3206
|
+
walk(projectRoot);
|
|
3207
|
+
}
|
|
3208
|
+
return files;
|
|
3209
|
+
}
|
|
3210
|
+
function calculateHealthScore(findings) {
|
|
3211
|
+
let score = 100;
|
|
3212
|
+
for (const f of findings) {
|
|
3213
|
+
switch (f.severity) {
|
|
3214
|
+
case "critical":
|
|
3215
|
+
score -= 15;
|
|
3216
|
+
break;
|
|
3217
|
+
case "high":
|
|
3218
|
+
score -= 8;
|
|
3219
|
+
break;
|
|
3220
|
+
case "medium":
|
|
3221
|
+
score -= 3;
|
|
3222
|
+
break;
|
|
3223
|
+
case "low":
|
|
3224
|
+
score -= 1;
|
|
3225
|
+
break;
|
|
3226
|
+
}
|
|
3227
|
+
if (f.category === "hallucinations") score -= 3;
|
|
3228
|
+
if (f.ruleId === "FAKE005") score -= 5;
|
|
3229
|
+
}
|
|
3230
|
+
return Math.max(0, Math.min(100, score));
|
|
3231
|
+
}
|
|
3232
|
+
async function scan(options) {
|
|
3233
|
+
const startTime = Date.now();
|
|
3234
|
+
const scanId = `scan-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
3235
|
+
options.onProgress?.({
|
|
3236
|
+
phase: "loading",
|
|
3237
|
+
processed: 0,
|
|
3238
|
+
total: 0,
|
|
3239
|
+
percentage: 0,
|
|
3240
|
+
elapsedMs: 0
|
|
3241
|
+
});
|
|
3242
|
+
const files = loadFiles(options.projectRoot, options);
|
|
3243
|
+
options.onProgress?.({
|
|
3244
|
+
phase: "classifying",
|
|
3245
|
+
processed: files.size,
|
|
3246
|
+
total: files.size,
|
|
3247
|
+
percentage: 10,
|
|
3248
|
+
elapsedMs: Date.now() - startTime
|
|
3249
|
+
});
|
|
3250
|
+
const enabledEngines = options.engines ? ALL_ENGINES.filter((e) => options.engines.includes(e.name)) : ALL_ENGINES;
|
|
3251
|
+
const engineResults = [];
|
|
3252
|
+
let allFindings = [];
|
|
3253
|
+
if (options.parallel !== false) {
|
|
3254
|
+
const promises = enabledEngines.map(async (engine) => {
|
|
3255
|
+
const engineStart = Date.now();
|
|
3256
|
+
try {
|
|
3257
|
+
const findings = await Promise.race([
|
|
3258
|
+
engine.scan(files, options),
|
|
3259
|
+
new Promise(
|
|
3260
|
+
(_, reject) => setTimeout(() => reject(new Error("Engine timeout")), options.engineTimeout ?? 3e4)
|
|
3261
|
+
)
|
|
3262
|
+
]);
|
|
3263
|
+
return {
|
|
3264
|
+
engine: engine.name,
|
|
3265
|
+
findings: findings.length,
|
|
3266
|
+
durationMs: Date.now() - engineStart,
|
|
3267
|
+
success: true,
|
|
3268
|
+
result: findings
|
|
3269
|
+
};
|
|
3270
|
+
} catch (err) {
|
|
3271
|
+
return {
|
|
3272
|
+
engine: engine.name,
|
|
3273
|
+
findings: 0,
|
|
3274
|
+
durationMs: Date.now() - engineStart,
|
|
3275
|
+
success: false,
|
|
3276
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
3277
|
+
result: []
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
3281
|
+
const results = await Promise.all(promises);
|
|
3282
|
+
for (const r of results) {
|
|
3283
|
+
engineResults.push({
|
|
3284
|
+
engine: r.engine,
|
|
3285
|
+
findings: r.findings,
|
|
3286
|
+
durationMs: r.durationMs,
|
|
3287
|
+
success: r.success,
|
|
3288
|
+
error: r.error
|
|
3289
|
+
});
|
|
3290
|
+
allFindings.push(...r.result);
|
|
3291
|
+
}
|
|
3292
|
+
} else {
|
|
3293
|
+
for (const engine of enabledEngines) {
|
|
3294
|
+
const engineStart = Date.now();
|
|
3295
|
+
try {
|
|
3296
|
+
const findings = await engine.scan(files, options);
|
|
3297
|
+
engineResults.push({
|
|
3298
|
+
engine: engine.name,
|
|
3299
|
+
findings: findings.length,
|
|
3300
|
+
durationMs: Date.now() - engineStart,
|
|
3301
|
+
success: true
|
|
3302
|
+
});
|
|
3303
|
+
allFindings.push(...findings);
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
engineResults.push({
|
|
3306
|
+
engine: engine.name,
|
|
3307
|
+
findings: 0,
|
|
3308
|
+
durationMs: Date.now() - engineStart,
|
|
3309
|
+
success: false,
|
|
3310
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
options.onProgress?.({
|
|
3316
|
+
phase: "deduplicating",
|
|
3317
|
+
processed: allFindings.length,
|
|
3318
|
+
total: allFindings.length,
|
|
3319
|
+
percentage: 85,
|
|
3320
|
+
elapsedMs: Date.now() - startTime
|
|
3321
|
+
});
|
|
3322
|
+
const { deduplicated, suppressedCount } = deduplicateFindings(allFindings);
|
|
3323
|
+
const severityOrder = ["low", "medium", "high", "critical"];
|
|
3324
|
+
const minSeverityIndex = severityOrder.indexOf(options.severityThreshold ?? "low");
|
|
3325
|
+
const filtered = deduplicated.filter(
|
|
3326
|
+
(f) => severityOrder.indexOf(f.severity) >= minSeverityIndex
|
|
3327
|
+
);
|
|
3328
|
+
if (options.onFinding) {
|
|
3329
|
+
for (const f of filtered) {
|
|
3330
|
+
options.onFinding(f);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
3334
|
+
const byCategory = {};
|
|
3335
|
+
const byEngine = {};
|
|
3336
|
+
for (const f of filtered) {
|
|
3337
|
+
bySeverity[f.severity]++;
|
|
3338
|
+
byCategory[f.category] = (byCategory[f.category] ?? 0) + 1;
|
|
3339
|
+
byEngine[f.engine] = (byEngine[f.engine] ?? 0) + 1;
|
|
3340
|
+
}
|
|
3341
|
+
const durationMs = Date.now() - startTime;
|
|
3342
|
+
const engineTimings = {};
|
|
3343
|
+
for (const r of engineResults) {
|
|
3344
|
+
engineTimings[r.engine] = r.durationMs;
|
|
3345
|
+
}
|
|
3346
|
+
options.onProgress?.({
|
|
3347
|
+
phase: "complete",
|
|
3348
|
+
processed: files.size,
|
|
3349
|
+
total: files.size,
|
|
3350
|
+
percentage: 100,
|
|
3351
|
+
elapsedMs: durationMs
|
|
3352
|
+
});
|
|
3353
|
+
return {
|
|
3354
|
+
scanId,
|
|
3355
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3356
|
+
findings: filtered,
|
|
3357
|
+
summary: {
|
|
3358
|
+
totalFiles: files.size,
|
|
3359
|
+
filesScanned: files.size,
|
|
3360
|
+
totalFindings: filtered.length,
|
|
3361
|
+
bySeverity,
|
|
3362
|
+
byCategory,
|
|
3363
|
+
byEngine,
|
|
3364
|
+
autoFixable: filtered.filter((f) => f.autoFixable).length,
|
|
3365
|
+
suppressedDuplicates: suppressedCount
|
|
3366
|
+
},
|
|
3367
|
+
healthScore: calculateHealthScore(filtered),
|
|
3368
|
+
engineResults,
|
|
3369
|
+
metrics: {
|
|
3370
|
+
durationMs,
|
|
3371
|
+
filesPerSecond: files.size > 0 ? Math.round(files.size / durationMs * 1e3) : 0,
|
|
3372
|
+
engineTimings
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
async function fix(options) {
|
|
3377
|
+
const report = await scan(options);
|
|
3378
|
+
const fixReport = applyFixes({
|
|
3379
|
+
findings: report.findings,
|
|
3380
|
+
projectRoot: options.projectRoot,
|
|
3381
|
+
dryRun: options.dryRun ?? false
|
|
3382
|
+
});
|
|
3383
|
+
return { report, fixReport };
|
|
3384
|
+
}
|
|
3385
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3386
|
+
0 && (module.exports = {
|
|
3387
|
+
ALL_ENGINES,
|
|
3388
|
+
RULE_CATALOG,
|
|
3389
|
+
applyFixes,
|
|
3390
|
+
classifyPath,
|
|
3391
|
+
fix,
|
|
3392
|
+
getRuleOrDefault,
|
|
3393
|
+
scan
|
|
3394
|
+
});
|
|
3395
|
+
//# sourceMappingURL=index.js.map
|