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,2500 @@
|
|
|
1
|
+
// bin/runners/lib/analyzers.js
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fg = require("fast-glob");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const { URL } = require("url");
|
|
9
|
+
const parser = require("@babel/parser");
|
|
10
|
+
const traverse = require("@babel/traverse").default;
|
|
11
|
+
const t = require("@babel/types");
|
|
12
|
+
|
|
13
|
+
const { routeMatches } = require("./claims");
|
|
14
|
+
const { matcherCoversPath } = require("./auth-truth");
|
|
15
|
+
|
|
16
|
+
/* ============================================================================
|
|
17
|
+
* STANDARD IGNORE PATTERNS
|
|
18
|
+
* Used by all analyzers to exclude non-production code
|
|
19
|
+
* ========================================================================== */
|
|
20
|
+
const STANDARD_IGNORE_PATTERNS = [
|
|
21
|
+
// Core excludes
|
|
22
|
+
"**/node_modules/**",
|
|
23
|
+
"**/.next/**",
|
|
24
|
+
"**/dist/**",
|
|
25
|
+
"**/build/**",
|
|
26
|
+
"**/*.d.ts",
|
|
27
|
+
"**/*.d.ts.map",
|
|
28
|
+
// Test files
|
|
29
|
+
"**/__tests__/**",
|
|
30
|
+
"**/tests/**",
|
|
31
|
+
"**/*.test.ts",
|
|
32
|
+
"**/*.test.tsx",
|
|
33
|
+
"**/*.test.js",
|
|
34
|
+
"**/*.spec.ts",
|
|
35
|
+
"**/*.spec.tsx",
|
|
36
|
+
"**/*.spec.js",
|
|
37
|
+
"**/fixtures/**",
|
|
38
|
+
// Internal tooling
|
|
39
|
+
"**/mcp-server/**",
|
|
40
|
+
"**/bin/**",
|
|
41
|
+
"**/packages/cli/**",
|
|
42
|
+
// Examples and templates
|
|
43
|
+
"**/examples/**",
|
|
44
|
+
"**/templates/**",
|
|
45
|
+
"**/docs/**",
|
|
46
|
+
// Cache and generated
|
|
47
|
+
"**/.guardrail/**",
|
|
48
|
+
"**/.cursor/**",
|
|
49
|
+
"**/.vibecheck/**",
|
|
50
|
+
"**/coverage/**",
|
|
51
|
+
"**/_archive/**",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/* ============================================================================
|
|
55
|
+
* WORLD-CLASS INFRA HELPERS
|
|
56
|
+
* - file caching (speed + consistent evidence)
|
|
57
|
+
* - stable IDs (diff-friendly)
|
|
58
|
+
* - safe regex usage (fixes /g + .test() state bugs)
|
|
59
|
+
* - memory management (clearFileCache for monorepos)
|
|
60
|
+
* ========================================================================== */
|
|
61
|
+
|
|
62
|
+
const _FILE_TEXT = new Map();
|
|
63
|
+
const _FILE_LINES = new Map();
|
|
64
|
+
|
|
65
|
+
function readFileCached(fileAbs) {
|
|
66
|
+
if (_FILE_TEXT.has(fileAbs)) return _FILE_TEXT.get(fileAbs);
|
|
67
|
+
const txt = fs.readFileSync(fileAbs, "utf8");
|
|
68
|
+
_FILE_TEXT.set(fileAbs, txt);
|
|
69
|
+
return txt;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readLinesCached(fileAbs) {
|
|
73
|
+
if (_FILE_LINES.has(fileAbs)) return _FILE_LINES.get(fileAbs);
|
|
74
|
+
const lines = readFileCached(fileAbs).split(/\r?\n/);
|
|
75
|
+
_FILE_LINES.set(fileAbs, lines);
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* V3: Clear file cache to prevent memory leaks in large monorepos.
|
|
81
|
+
* Call this after a scan completes or between major steps.
|
|
82
|
+
*/
|
|
83
|
+
function clearFileCache() {
|
|
84
|
+
_FILE_TEXT.clear();
|
|
85
|
+
_FILE_LINES.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* V3: Shannon Entropy calculator for detecting high-randomness strings (likely secrets).
|
|
90
|
+
* Entropy > 4.5 typically indicates a random/secret string vs structured data.
|
|
91
|
+
* Git SHAs (hex only) have lower effective entropy due to limited charset.
|
|
92
|
+
*/
|
|
93
|
+
function getShannonEntropy(str) {
|
|
94
|
+
if (!str || str.length === 0) return 0;
|
|
95
|
+
const len = str.length;
|
|
96
|
+
const frequencies = {};
|
|
97
|
+
for (let i = 0; i < len; i++) {
|
|
98
|
+
const char = str[i];
|
|
99
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let entropy = 0;
|
|
103
|
+
for (const char in frequencies) {
|
|
104
|
+
const p = frequencies[char] / len;
|
|
105
|
+
entropy -= p * Math.log2(p);
|
|
106
|
+
}
|
|
107
|
+
return entropy;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sha256(text) {
|
|
111
|
+
return "sha256:" + crypto.createHash("sha256").update(String(text || "")).digest("hex");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stableId(prefix, key) {
|
|
115
|
+
const h = crypto.createHash("sha256").update(String(key || "")).digest("hex").slice(0, 10);
|
|
116
|
+
return `${prefix}_${h}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// IMPORTANT: /g + .test() is stateful. This helper makes it deterministic.
|
|
120
|
+
function rxTest(rx, s) {
|
|
121
|
+
if (!rx) return false;
|
|
122
|
+
rx.lastIndex = 0;
|
|
123
|
+
return rx.test(s);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Try to use the engine's globalASTCache if available for better performance
|
|
127
|
+
let _globalASTCache = null;
|
|
128
|
+
try {
|
|
129
|
+
_globalASTCache = require("./engine/ast-cache").globalASTCache;
|
|
130
|
+
} catch {
|
|
131
|
+
// Engine not available, will use direct parsing
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseFile(code, fileAbsForErrors = "") {
|
|
135
|
+
// Use globalASTCache if available (engine v2 integration)
|
|
136
|
+
if (_globalASTCache) {
|
|
137
|
+
const result = _globalASTCache.parse(code, fileAbsForErrors);
|
|
138
|
+
if (result.ast) return result.ast;
|
|
139
|
+
// Fall through to direct parse if cache failed
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Error recovery avoids hard-failing on mixed TS/JS/JSX edge cases.
|
|
143
|
+
return parser.parse(code, {
|
|
144
|
+
sourceType: "unambiguous",
|
|
145
|
+
errorRecovery: true,
|
|
146
|
+
allowReturnOutsideFunction: true,
|
|
147
|
+
plugins: [
|
|
148
|
+
"typescript",
|
|
149
|
+
"jsx",
|
|
150
|
+
"dynamicImport",
|
|
151
|
+
"topLevelAwait",
|
|
152
|
+
"classProperties",
|
|
153
|
+
"classPrivateProperties",
|
|
154
|
+
"classPrivateMethods",
|
|
155
|
+
"decorators-legacy",
|
|
156
|
+
"optionalChaining",
|
|
157
|
+
"nullishCoalescingOperator",
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function evidenceFromLoc(fileAbs, repoRoot, loc, reason) {
|
|
163
|
+
if (!loc) return null;
|
|
164
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
165
|
+
const lines = readLinesCached(fileAbs);
|
|
166
|
+
const start = Math.max(1, loc.start?.line || 1);
|
|
167
|
+
const end = Math.max(start, loc.end?.line || start);
|
|
168
|
+
const snippet = lines.slice(start - 1, end).join("\n");
|
|
169
|
+
return {
|
|
170
|
+
id: stableId("ev", `${fileRel}:${start}-${end}:${reason || ""}:${sha256(snippet)}`),
|
|
171
|
+
file: fileRel,
|
|
172
|
+
lines: `${start}-${end}`,
|
|
173
|
+
snippetHash: sha256(snippet),
|
|
174
|
+
reason,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ============================================================================
|
|
179
|
+
* ROUTE GAP ENGINE (world-class missing route logic)
|
|
180
|
+
* ========================================================================== */
|
|
181
|
+
|
|
182
|
+
function safeUrlParse(maybeUrl) {
|
|
183
|
+
try {
|
|
184
|
+
// URL() needs protocol; allow //host/path too
|
|
185
|
+
if (typeof maybeUrl !== "string") return null;
|
|
186
|
+
if (/^https?:\/\//i.test(maybeUrl)) return new URL(maybeUrl);
|
|
187
|
+
if (/^\/\//.test(maybeUrl)) return new URL("https:" + maybeUrl);
|
|
188
|
+
return null;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizePath(raw) {
|
|
195
|
+
if (!raw) return "/";
|
|
196
|
+
let p = String(raw).trim();
|
|
197
|
+
|
|
198
|
+
// If full URL, strip to pathname.
|
|
199
|
+
const u = safeUrlParse(p);
|
|
200
|
+
if (u) p = u.pathname || "/";
|
|
201
|
+
|
|
202
|
+
// Strip query/hash if present
|
|
203
|
+
p = p.split("?")[0].split("#")[0];
|
|
204
|
+
|
|
205
|
+
// Decode safely
|
|
206
|
+
try {
|
|
207
|
+
p = decodeURIComponent(p);
|
|
208
|
+
} catch {
|
|
209
|
+
// keep original
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ensure leading slash
|
|
213
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
214
|
+
|
|
215
|
+
// Collapse duplicate slashes
|
|
216
|
+
p = p.replace(/\/{2,}/g, "/");
|
|
217
|
+
|
|
218
|
+
// Remove trailing slash (except root)
|
|
219
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
220
|
+
|
|
221
|
+
return p;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pathLooksLikeAsset(p) {
|
|
225
|
+
const s = String(p || "");
|
|
226
|
+
// Common Next/static + file extensions that are not API routes
|
|
227
|
+
if (/^\/(_next|static|assets)\b/i.test(s)) return true;
|
|
228
|
+
if (/\.(png|jpg|jpeg|gif|webp|svg|ico|css|js|map|txt|xml|woff2?|ttf|eot)$/i.test(s)) return true;
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isInternalUtilityRoute(p) {
|
|
233
|
+
// Common internal/utility routes that should NOT be flagged as missing
|
|
234
|
+
// These are typically framework-specific, monitoring, or debugging endpoints
|
|
235
|
+
const internalPatterns = [
|
|
236
|
+
// Health/monitoring
|
|
237
|
+
/^\/(health|healthz|healthcheck|ready|readyz|live|livez|liveness|readiness|metrics|status|ping|version)/i,
|
|
238
|
+
// Internal/debug
|
|
239
|
+
/^\/(debug|internal|_internal|__internal|security|\.well-known)/i,
|
|
240
|
+
// WebSockets
|
|
241
|
+
/^\/(websocket|ws|socket\.io|sockjs)/i,
|
|
242
|
+
// Admin/dashboard
|
|
243
|
+
/^\/(admin|dashboard|_admin|__admin)/i,
|
|
244
|
+
// Next.js internals
|
|
245
|
+
/^\/_next\//i,
|
|
246
|
+
// Vite/dev tools
|
|
247
|
+
/^\/@vite|^\/@fs|^\/__vite/i,
|
|
248
|
+
// GraphQL
|
|
249
|
+
/^\/(graphql|graphiql|playground)/i,
|
|
250
|
+
// Swagger/API docs
|
|
251
|
+
/^\/(swagger|api-docs|openapi|docs|redoc)/i,
|
|
252
|
+
// Auth callbacks
|
|
253
|
+
/^\/(auth|oauth|callback|login|logout|signin|signout)\/?(callback|redirect)?$/i,
|
|
254
|
+
// Common framework routes
|
|
255
|
+
/^\/(favicon\.ico|robots\.txt|sitemap\.xml|manifest\.json)$/i,
|
|
256
|
+
// Vercel/serverless
|
|
257
|
+
/^\/api\/_/i,
|
|
258
|
+
// Hidden paths
|
|
259
|
+
/^\/[._]/i,
|
|
260
|
+
];
|
|
261
|
+
return internalPatterns.some(rx => rxTest(rx, p));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function looksInventedRoute(p) {
|
|
265
|
+
// Stuff AI loves to hallucinate - but be VERY precise to avoid false positives
|
|
266
|
+
// Only flag patterns that are CLEARLY fake/placeholder
|
|
267
|
+
|
|
268
|
+
// Require routes to start with these patterns (not just contain them)
|
|
269
|
+
// This avoids flagging legitimate routes like /users/foo-bar-123
|
|
270
|
+
const clearlyFakeStarts = [
|
|
271
|
+
/^\/(fake|dummy|placeholder|asdf|qwerty|lorem|ipsum)\b/i,
|
|
272
|
+
/^\/(foo|bar|baz|xxx|yyy)$/i, // Only if it's the ENTIRE route segment
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
for (const rx of clearlyFakeStarts) {
|
|
276
|
+
if (rxTest(rx, p)) return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Obvious "ai generated" patterns - must be at route start
|
|
280
|
+
if (rxTest(/^\/(generated|auto[-_]?gen|ai[-_]?gen)\b/i, p)) return true;
|
|
281
|
+
|
|
282
|
+
// Obvious placeholder test data patterns (test123, abc123, demo123)
|
|
283
|
+
// Only if the ENTIRE segment is clearly placeholder
|
|
284
|
+
if (rxTest(/\/(test123|abc123|demo123|sample123|example123)$/i, p)) return true;
|
|
285
|
+
|
|
286
|
+
// Very long hex strings in route (32+ chars) that aren't IDs
|
|
287
|
+
// Skip if it looks like a valid UUID pattern or session token
|
|
288
|
+
const segments = p.split('/').filter(Boolean);
|
|
289
|
+
for (const seg of segments) {
|
|
290
|
+
// Long hex that's NOT a UUID format and NOT a reasonable ID length
|
|
291
|
+
if (/^[a-f0-9]{40,}$/i.test(seg) && !/^[a-f0-9]{8}-/.test(seg)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function canonicalizeDynamicSegments(p) {
|
|
300
|
+
// Convert common dynamic segments to a stable token so "/users/123" can match "/users/:id"
|
|
301
|
+
// NOTE: This function returns a string, not a boolean - name is for canonicalization, not validation
|
|
302
|
+
const segs = normalizePath(p).split("/").filter(Boolean);
|
|
303
|
+
const canon = segs.map((seg) => {
|
|
304
|
+
if (!seg) return seg;
|
|
305
|
+
// UUID
|
|
306
|
+
if (rxTest(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, seg)) return ":id";
|
|
307
|
+
// Numeric IDs
|
|
308
|
+
if (rxTest(/^\d{1,18}$/i, seg)) return ":id";
|
|
309
|
+
// Long hex
|
|
310
|
+
if (rxTest(/^(0x)?[0-9a-f]{16,}$/i, seg)) return ":id";
|
|
311
|
+
// Next-ish catchalls
|
|
312
|
+
if (seg === "[...slug]" || seg === "[[...slug]]") return ":slug";
|
|
313
|
+
return seg;
|
|
314
|
+
});
|
|
315
|
+
return "/" + canon.join("/");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function firstSegment(p) {
|
|
319
|
+
const seg = normalizePath(p).split("/").filter(Boolean)[0];
|
|
320
|
+
return seg || "";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function inferDominantPrefix(paths, minShare = 0.7) {
|
|
324
|
+
// Find a dominant first segment like "api" across a set of paths
|
|
325
|
+
const counts = new Map();
|
|
326
|
+
for (const p of paths) {
|
|
327
|
+
const seg = firstSegment(p);
|
|
328
|
+
if (!seg) continue;
|
|
329
|
+
counts.set(seg, (counts.get(seg) || 0) + 1);
|
|
330
|
+
}
|
|
331
|
+
let best = { seg: "", n: 0 };
|
|
332
|
+
for (const [seg, n] of counts.entries()) {
|
|
333
|
+
if (n > best.n) best = { seg, n };
|
|
334
|
+
}
|
|
335
|
+
if (!best.seg) return null;
|
|
336
|
+
const share = best.n / Math.max(1, paths.length);
|
|
337
|
+
return share >= minShare ? "/" + best.seg : null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildServerRouteIndex(serverRoutes) {
|
|
341
|
+
// Index by method + first segment for fast shortlist
|
|
342
|
+
const byMethod = new Map(); // method -> seg -> routes[]
|
|
343
|
+
const all = [];
|
|
344
|
+
|
|
345
|
+
for (const r of serverRoutes) {
|
|
346
|
+
const method = String(r.method || "*").toUpperCase();
|
|
347
|
+
const pNorm = normalizePath(r.path);
|
|
348
|
+
const seg = firstSegment(pNorm);
|
|
349
|
+
|
|
350
|
+
const rec = { ...r, _method: method, _pathNorm: pNorm, _seg: seg, _canon: canonicalizeDynamicSegments(pNorm) };
|
|
351
|
+
all.push(rec);
|
|
352
|
+
|
|
353
|
+
if (!byMethod.has(method)) byMethod.set(method, new Map());
|
|
354
|
+
const segMap = byMethod.get(method);
|
|
355
|
+
if (!segMap.has(seg)) segMap.set(seg, []);
|
|
356
|
+
segMap.get(seg).push(rec);
|
|
357
|
+
|
|
358
|
+
// Also index wildcard bucket
|
|
359
|
+
if (!byMethod.has("*")) byMethod.set("*", new Map());
|
|
360
|
+
const w = byMethod.get("*");
|
|
361
|
+
if (!w.has(seg)) w.set(seg, []);
|
|
362
|
+
w.get(seg).push(rec);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { byMethod, all };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function shortlistServerRoutes(index, method, pNorm) {
|
|
369
|
+
const m = String(method || "*").toUpperCase();
|
|
370
|
+
const seg = firstSegment(pNorm);
|
|
371
|
+
|
|
372
|
+
const pick = (meth) => {
|
|
373
|
+
const segMap = index.byMethod.get(meth);
|
|
374
|
+
if (!segMap) return [];
|
|
375
|
+
const bucket = segMap.get(seg) || [];
|
|
376
|
+
// If seg is empty or dynamic roots exist, include a fallback bucket
|
|
377
|
+
const rootBucket = segMap.get("") || [];
|
|
378
|
+
return bucket.concat(rootBucket);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// prioritize exact method, then wildcard
|
|
382
|
+
const a = pick(m);
|
|
383
|
+
const b = pick("*");
|
|
384
|
+
// de-dupe by path+method
|
|
385
|
+
const seen = new Set();
|
|
386
|
+
const out = [];
|
|
387
|
+
for (const r of a.concat(b)) {
|
|
388
|
+
const k = `${r._method}:${r._pathNorm}`;
|
|
389
|
+
if (seen.has(k)) continue;
|
|
390
|
+
seen.add(k);
|
|
391
|
+
out.push(r);
|
|
392
|
+
}
|
|
393
|
+
return out.length ? out : index.all;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function routeSimilarityScore(refPath, serverPathPattern) {
|
|
397
|
+
// Score 0..1 based on static segment overlap + prefix alignment
|
|
398
|
+
const a = canonicalizeDynamicSegments(refPath).split("/").filter(Boolean);
|
|
399
|
+
const b = canonicalizeDynamicSegments(serverPathPattern).split("/").filter(Boolean);
|
|
400
|
+
|
|
401
|
+
if (!a.length || !b.length) return 0;
|
|
402
|
+
|
|
403
|
+
const aStatic = a.filter((s) => !s.startsWith(":") && !s.startsWith("["));
|
|
404
|
+
const bStatic = b.filter((s) => !s.startsWith(":") && !s.startsWith("["));
|
|
405
|
+
|
|
406
|
+
const setA = new Set(aStatic);
|
|
407
|
+
const setB = new Set(bStatic);
|
|
408
|
+
|
|
409
|
+
let inter = 0;
|
|
410
|
+
for (const s of setA) if (setB.has(s)) inter++;
|
|
411
|
+
|
|
412
|
+
const union = new Set([...setA, ...setB]).size || 1;
|
|
413
|
+
|
|
414
|
+
const jaccard = inter / union;
|
|
415
|
+
|
|
416
|
+
// prefix bonus if first 1-2 segments align
|
|
417
|
+
const prefix1 = a[0] && b[0] && a[0] === b[0] ? 0.15 : 0;
|
|
418
|
+
const prefix2 = a[1] && b[1] && a[1] === b[1] ? 0.10 : 0;
|
|
419
|
+
|
|
420
|
+
// length penalty if wildly different
|
|
421
|
+
const lenPenalty = Math.min(0.25, Math.abs(a.length - b.length) * 0.05);
|
|
422
|
+
|
|
423
|
+
const score = Math.max(0, Math.min(1, jaccard + prefix1 + prefix2 - lenPenalty));
|
|
424
|
+
return score;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function compileAllowPatterns(patterns) {
|
|
428
|
+
const out = [];
|
|
429
|
+
for (const p of patterns || []) {
|
|
430
|
+
if (!p) continue;
|
|
431
|
+
if (p instanceof RegExp) {
|
|
432
|
+
out.push(p);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const s = String(p);
|
|
436
|
+
// Support "/.../i" style
|
|
437
|
+
const m = s.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
438
|
+
if (m) {
|
|
439
|
+
try {
|
|
440
|
+
out.push(new RegExp(m[1], m[2]));
|
|
441
|
+
continue;
|
|
442
|
+
} catch {
|
|
443
|
+
// fall through
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Simple wildcard "*" and "?" -> regex
|
|
447
|
+
const esc = s
|
|
448
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
449
|
+
.replace(/\*/g, ".*")
|
|
450
|
+
.replace(/\?/g, ".");
|
|
451
|
+
try {
|
|
452
|
+
out.push(new RegExp("^" + esc + "$", "i"));
|
|
453
|
+
} catch {
|
|
454
|
+
// ignore bad patterns
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function findMissingRoutes(truthpack) {
|
|
461
|
+
const findings = [];
|
|
462
|
+
|
|
463
|
+
const server = truthpack?.routes?.server || [];
|
|
464
|
+
const refs = truthpack?.routes?.clientRefs || [];
|
|
465
|
+
const gaps = truthpack?.routes?.gaps || [];
|
|
466
|
+
|
|
467
|
+
const hasGaps = gaps.length > 0;
|
|
468
|
+
|
|
469
|
+
// Allowlist/suppressions (lets users kill known false positives cleanly)
|
|
470
|
+
const allowMethods = new Set((truthpack?.routes?.allowlistMethods || []).map((m) => String(m).toUpperCase()));
|
|
471
|
+
const allowPatterns = compileAllowPatterns(truthpack?.routes?.allowlist || truthpack?.routes?.allowlistPatterns || []);
|
|
472
|
+
const ignorePatterns = compileAllowPatterns(truthpack?.routes?.ignore || truthpack?.routes?.ignorePatterns || []);
|
|
473
|
+
|
|
474
|
+
const isSuppressed = (method, pNorm) => {
|
|
475
|
+
const m = String(method || "*").toUpperCase();
|
|
476
|
+
if (allowMethods.size && !allowMethods.has(m) && !allowMethods.has("*")) {
|
|
477
|
+
// method not allowed by allowMethods -> don't suppress
|
|
478
|
+
}
|
|
479
|
+
for (const rx of ignorePatterns) if (rxTest(rx, `${m} ${pNorm}`) || rxTest(rx, pNorm)) return true;
|
|
480
|
+
for (const rx of allowPatterns) if (rxTest(rx, `${m} ${pNorm}`) || rxTest(rx, pNorm)) return true;
|
|
481
|
+
return false;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const serverCount = server.length;
|
|
485
|
+
const refCount = refs.length;
|
|
486
|
+
|
|
487
|
+
// Monorepo heuristic (keep, but make it less dumb)
|
|
488
|
+
const isLikelyMonorepo = refCount > Math.max(30, serverCount * 2.5);
|
|
489
|
+
|
|
490
|
+
const serverPaths = server.map((r) => normalizePath(r.path));
|
|
491
|
+
const refPaths = refs.map((r) => normalizePath(r.path));
|
|
492
|
+
|
|
493
|
+
// Dominant prefixes (commonly "/api")
|
|
494
|
+
const dominantServerPrefix = inferDominantPrefix(serverPaths, 0.7);
|
|
495
|
+
const dominantRefPrefix = inferDominantPrefix(refPaths, 0.7);
|
|
496
|
+
|
|
497
|
+
const serverPrefix = dominantServerPrefix || null;
|
|
498
|
+
const refPrefix = dominantRefPrefix || null;
|
|
499
|
+
|
|
500
|
+
const index = buildServerRouteIndex(server);
|
|
501
|
+
|
|
502
|
+
// Route-map quality gating:
|
|
503
|
+
// If we have unresolved gaps or tiny server map, DO NOT BLOCK unless obviously invented.
|
|
504
|
+
const routeMapQuality =
|
|
505
|
+
serverCount >= 10 && !hasGaps ? "strong" :
|
|
506
|
+
serverCount >= 5 ? "medium" :
|
|
507
|
+
"weak";
|
|
508
|
+
|
|
509
|
+
// More generous caps (but still bounded)
|
|
510
|
+
const MAX_WARNINGS = isLikelyMonorepo ? 35 : 60;
|
|
511
|
+
const MAX_BLOCKS = 15;
|
|
512
|
+
|
|
513
|
+
let warnCount = 0;
|
|
514
|
+
let blockCount = 0;
|
|
515
|
+
|
|
516
|
+
// Summaries to help you fix extraction instead of drowning in noise
|
|
517
|
+
const unmatchedByPrefix = new Map();
|
|
518
|
+
const externalRefs = [];
|
|
519
|
+
|
|
520
|
+
function addUnmatchedPrefix(pNorm) {
|
|
521
|
+
const seg = firstSegment(pNorm) || "/";
|
|
522
|
+
unmatchedByPrefix.set(seg, (unmatchedByPrefix.get(seg) || 0) + 1);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function tryMatch(method, pNorm) {
|
|
526
|
+
const shortlist = shortlistServerRoutes(index, method, pNorm);
|
|
527
|
+
|
|
528
|
+
// Try exact + canonicalized matching
|
|
529
|
+
const pCanon = canonicalizeDynamicSegments(pNorm);
|
|
530
|
+
|
|
531
|
+
for (const r of shortlist) {
|
|
532
|
+
if (routeMatches(r, method, pNorm) || routeMatches(r, "*", pNorm)) return { ok: true, matched: r };
|
|
533
|
+
// Try canonicalized path vs canonicalized server path
|
|
534
|
+
if (routeMatches({ ...r, path: r._canon }, method, pCanon) || routeMatches({ ...r, path: r._canon }, "*", pCanon)) {
|
|
535
|
+
return { ok: true, matched: r, usedCanon: true };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { ok: false };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function closestSuggestions(method, pNorm) {
|
|
542
|
+
// Use a bounded scan: shortlist first, then broaden if needed
|
|
543
|
+
const shortlist = shortlistServerRoutes(index, method, pNorm);
|
|
544
|
+
const pool = shortlist.length ? shortlist : index.all;
|
|
545
|
+
|
|
546
|
+
const scored = pool
|
|
547
|
+
.map((r) => ({ r, score: routeSimilarityScore(pNorm, r._pathNorm) }))
|
|
548
|
+
.sort((a, b) => b.score - a.score)
|
|
549
|
+
.slice(0, 3);
|
|
550
|
+
|
|
551
|
+
// Raise threshold to 0.50 to only show genuinely similar routes
|
|
552
|
+
// This reduces "did you mean" noise for unrelated routes
|
|
553
|
+
return scored.filter((x) => x.score >= 0.50).map((x) => ({
|
|
554
|
+
method: x.r._method,
|
|
555
|
+
path: x.r._pathNorm,
|
|
556
|
+
score: Number(x.score.toFixed(2)),
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function detectMethodMismatch(pNorm, method) {
|
|
561
|
+
// If the path exists but only under other method(s), that's not "missing route"
|
|
562
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "*"];
|
|
563
|
+
const hits = [];
|
|
564
|
+
for (const m of methods) {
|
|
565
|
+
if (String(m) === String(method).toUpperCase()) continue;
|
|
566
|
+
const res = tryMatch(m, pNorm);
|
|
567
|
+
if (res.ok) hits.push(m);
|
|
568
|
+
}
|
|
569
|
+
return hits.length ? hits : null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (const ref of refs) {
|
|
573
|
+
const rawPath = ref?.path;
|
|
574
|
+
const method = String(ref?.method || "*").toUpperCase();
|
|
575
|
+
|
|
576
|
+
// Normalize & classify
|
|
577
|
+
const u = safeUrlParse(rawPath);
|
|
578
|
+
const pNorm = normalizePath(rawPath);
|
|
579
|
+
|
|
580
|
+
// Skip asset-ish refs
|
|
581
|
+
if (pathLooksLikeAsset(pNorm)) continue;
|
|
582
|
+
|
|
583
|
+
// External refs: if full URL and host isn't localhost, treat as external service.
|
|
584
|
+
if (u && u.host && !/^(localhost|127\.0\.0\.1)(:\d+)?$/i.test(u.host)) {
|
|
585
|
+
externalRefs.push({ host: u.host, method, path: pNorm, evidence: ref.evidence || [] });
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Skip suppressed
|
|
590
|
+
if (isSuppressed(method, pNorm)) continue;
|
|
591
|
+
|
|
592
|
+
// Build candidate variants (this kills a lot of false positives)
|
|
593
|
+
const candidates = new Set();
|
|
594
|
+
candidates.add(pNorm);
|
|
595
|
+
|
|
596
|
+
// trailing slash variant
|
|
597
|
+
if (pNorm.length > 1) {
|
|
598
|
+
candidates.add(pNorm + "/");
|
|
599
|
+
candidates.add(pNorm.replace(/\/+$/g, ""));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Toggle dominant server prefix (ex: /api) if mismatch likely
|
|
603
|
+
if (serverPrefix && serverPrefix !== "/" && !pNorm.startsWith(serverPrefix + "/") && pNorm !== serverPrefix) {
|
|
604
|
+
candidates.add(normalizePath(serverPrefix + pNorm));
|
|
605
|
+
}
|
|
606
|
+
if (serverPrefix && serverPrefix !== "/" && pNorm.startsWith(serverPrefix + "/")) {
|
|
607
|
+
candidates.add(normalizePath(pNorm.slice(serverPrefix.length)));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Toggle dominant ref prefix similarly (sometimes refs have /api but server routes stored without it)
|
|
611
|
+
if (refPrefix && refPrefix !== "/" && !pNorm.startsWith(refPrefix + "/") && pNorm !== refPrefix) {
|
|
612
|
+
candidates.add(normalizePath(refPrefix + pNorm));
|
|
613
|
+
}
|
|
614
|
+
if (refPrefix && refPrefix !== "/" && pNorm.startsWith(refPrefix + "/")) {
|
|
615
|
+
candidates.add(normalizePath(pNorm.slice(refPrefix.length)));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Canonicalized variant
|
|
619
|
+
candidates.add(canonicalizeDynamicSegments(pNorm));
|
|
620
|
+
|
|
621
|
+
// Try match all candidates
|
|
622
|
+
let matched = null;
|
|
623
|
+
let usedCanon = false;
|
|
624
|
+
for (const cand of candidates) {
|
|
625
|
+
const res = tryMatch(method, cand);
|
|
626
|
+
if (res.ok) {
|
|
627
|
+
matched = res.matched;
|
|
628
|
+
usedCanon = !!res.usedCanon;
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (matched) continue;
|
|
633
|
+
|
|
634
|
+
addUnmatchedPrefix(pNorm);
|
|
635
|
+
|
|
636
|
+
// Method mismatch detection (not missing route)
|
|
637
|
+
const methodMismatch = detectMethodMismatch(pNorm, method);
|
|
638
|
+
if (methodMismatch) {
|
|
639
|
+
// Keep as WARN (not missing route) — reduces noise dramatically
|
|
640
|
+
if (warnCount >= MAX_WARNINGS) continue;
|
|
641
|
+
warnCount++;
|
|
642
|
+
|
|
643
|
+
findings.push({
|
|
644
|
+
id: stableId("F_ROUTE_METHOD_MISMATCH", `${method} ${pNorm}`),
|
|
645
|
+
severity: "WARN",
|
|
646
|
+
category: "MissingRoute",
|
|
647
|
+
title: `Method mismatch for route: ${method} ${pNorm}`,
|
|
648
|
+
why: `A server route exists for this path, but not for method ${method}. This is often a client bug or an incorrect assumption.`,
|
|
649
|
+
confidence: routeMapQuality === "strong" ? "high" : "med",
|
|
650
|
+
evidence: ref.evidence || [],
|
|
651
|
+
fixHints: [
|
|
652
|
+
`Check the client call method. Server supports: ${methodMismatch.join(", ")} for ${pNorm}`,
|
|
653
|
+
"If this is intentional, update the server to accept this method or adjust the client.",
|
|
654
|
+
],
|
|
655
|
+
});
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const invented = looksInventedRoute(pNorm);
|
|
660
|
+
const internal = isInternalUtilityRoute(pNorm);
|
|
661
|
+
|
|
662
|
+
// Skip internal utility routes entirely - they're almost never real issues
|
|
663
|
+
if (internal) continue;
|
|
664
|
+
|
|
665
|
+
// Similarity suggestions
|
|
666
|
+
const suggestions = closestSuggestions(method, pNorm);
|
|
667
|
+
|
|
668
|
+
// Confidence + severity gating - CONSERVATIVE by default to reduce noise
|
|
669
|
+
let confidence = "low";
|
|
670
|
+
let severity = "WARN";
|
|
671
|
+
|
|
672
|
+
if (invented) {
|
|
673
|
+
// Only BLOCK truly invented routes - and require high confidence
|
|
674
|
+
severity = "BLOCK";
|
|
675
|
+
confidence = "high";
|
|
676
|
+
} else if (routeMapQuality === "strong" && !isLikelyMonorepo) {
|
|
677
|
+
// Even with strong route map, be conservative
|
|
678
|
+
const best = suggestions[0]?.score ?? 0;
|
|
679
|
+
if (best < 0.30) {
|
|
680
|
+
// No close matches - more likely to be a real issue, but still WARN
|
|
681
|
+
confidence = "med";
|
|
682
|
+
severity = "WARN";
|
|
683
|
+
} else if (best >= 0.70) {
|
|
684
|
+
// Very close match exists - probably a typo, keep as low-priority WARN
|
|
685
|
+
confidence = "low";
|
|
686
|
+
severity = "WARN";
|
|
687
|
+
} else {
|
|
688
|
+
// Moderate similarity - unclear
|
|
689
|
+
confidence = "low";
|
|
690
|
+
severity = "WARN";
|
|
691
|
+
}
|
|
692
|
+
} else {
|
|
693
|
+
// Weak route map or monorepo - don't trust findings, always WARN with low confidence
|
|
694
|
+
confidence = "low";
|
|
695
|
+
severity = "WARN";
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// caps
|
|
699
|
+
if (severity === "BLOCK") {
|
|
700
|
+
if (blockCount >= MAX_BLOCKS) continue;
|
|
701
|
+
blockCount++;
|
|
702
|
+
} else {
|
|
703
|
+
if (warnCount >= MAX_WARNINGS) continue;
|
|
704
|
+
warnCount++;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const didYouMean = suggestions.length
|
|
708
|
+
? `Closest server routes: ${suggestions.map((s) => `${s.method} ${s.path} (${s.score})`).join(" • ")}`
|
|
709
|
+
: "No close server route candidates were found (based on static segment similarity).";
|
|
710
|
+
|
|
711
|
+
findings.push({
|
|
712
|
+
id: stableId("F_MISSING_ROUTE", `${method} ${pNorm}`),
|
|
713
|
+
severity,
|
|
714
|
+
category: "MissingRoute",
|
|
715
|
+
title: `Client references route not found in detected server map: ${method} ${pNorm}`,
|
|
716
|
+
why:
|
|
717
|
+
severity === "BLOCK"
|
|
718
|
+
? "This looks invented. Shipping this will break flows (404 / silent failure)."
|
|
719
|
+
: routeMapQuality === "weak" || hasGaps
|
|
720
|
+
? "Route reference didn't match the detected server map. Route detection may be incomplete (dynamic registration, plugins, prefixes)."
|
|
721
|
+
: "Route reference didn't match the detected server map. This can be a real missing endpoint or an undetected server route.",
|
|
722
|
+
confidence,
|
|
723
|
+
evidence: ref.evidence || [],
|
|
724
|
+
fixHints: [
|
|
725
|
+
didYouMean,
|
|
726
|
+
usedCanon ? "Note: matching tried canonicalized ID segments (/:id normalization)." : "Matching tried normalization (origin/query/trailing slash/prefix toggles).",
|
|
727
|
+
hasGaps ? `Route map had ${gaps.length} unresolved sources; fix route extraction to reduce false positives.` : "If this is a real endpoint, add it server-side or correct the client call.",
|
|
728
|
+
isLikelyMonorepo ? "Monorepo/microservices likely: consider allowlisting external services or feeding service domains into truthpack." : "If this is an external service call, store it as external/allowlisted so it won't be flagged.",
|
|
729
|
+
],
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Route map diagnostics (actionable, reduces "blame the analyzer" loops)
|
|
734
|
+
if (hasGaps) {
|
|
735
|
+
findings.push({
|
|
736
|
+
id: stableId("F_ROUTE_MAP_GAPS", String(gaps.length)),
|
|
737
|
+
severity: "WARN",
|
|
738
|
+
category: "RouteMapGaps",
|
|
739
|
+
title: `Route map incomplete (${gaps.length} unresolved route sources)`,
|
|
740
|
+
why: "Dynamic registration, unresolved plugin imports, or non-standard routing prevented complete detection. Missing route findings may be false positives.",
|
|
741
|
+
confidence: "med",
|
|
742
|
+
evidence: [],
|
|
743
|
+
fixHints: [
|
|
744
|
+
"Fix route extraction: resolve Fastify plugins and prefix registration (fastify.register(...,{ prefix })) and inline fastify.get/post routes.",
|
|
745
|
+
"If using Next App Router: ensure route handlers (app/**/route.ts) are included in server route extraction.",
|
|
746
|
+
"Add allowlistPatterns for external services to silence expected gaps.",
|
|
747
|
+
],
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// External refs summary (useful in microservices)
|
|
752
|
+
if (externalRefs.length) {
|
|
753
|
+
const topHosts = new Map();
|
|
754
|
+
for (const r of externalRefs) topHosts.set(r.host, (topHosts.get(r.host) || 0) + 1);
|
|
755
|
+
const hostSummary = [...topHosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5)
|
|
756
|
+
.map(([h, n]) => `${h} (${n})`).join(", ");
|
|
757
|
+
|
|
758
|
+
findings.push({
|
|
759
|
+
id: stableId("F_EXTERNAL_ROUTE_REFS", hostSummary),
|
|
760
|
+
severity: "INFO",
|
|
761
|
+
category: "MissingRoute",
|
|
762
|
+
title: `External service routes detected in client refs (${externalRefs.length})`,
|
|
763
|
+
why: "These are full-URL calls to non-local hosts; they are not expected to match server route maps.",
|
|
764
|
+
confidence: "high",
|
|
765
|
+
evidence: [],
|
|
766
|
+
fixHints: [
|
|
767
|
+
`Top hosts: ${hostSummary}`,
|
|
768
|
+
"If you want to validate external APIs, add a separate analyzer that checks OpenAPI/spec contracts for those services.",
|
|
769
|
+
"Optionally add allowlistPatterns like '/^https?:\\/\\/(api\\.stripe\\.com|...)/' at truthpack.routes.allowlistPatterns.",
|
|
770
|
+
],
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Biggest unmatched prefixes (points directly at extraction gaps)
|
|
775
|
+
if (unmatchedByPrefix.size) {
|
|
776
|
+
const top = [...unmatchedByPrefix.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
777
|
+
const summary = top.map(([k, v]) => `${k}:${v}`).join(" • ");
|
|
778
|
+
findings.push({
|
|
779
|
+
id: stableId("F_ROUTE_UNMATCHED_PREFIXES", summary),
|
|
780
|
+
severity: "INFO",
|
|
781
|
+
category: "MissingRoute",
|
|
782
|
+
title: "Unmatched client route prefixes (helps fix extraction/allowlists)",
|
|
783
|
+
why: "When one prefix dominates unmatched refs, it usually means server extraction missed a router/plugin prefix or the client is calling a different service.",
|
|
784
|
+
confidence: "med",
|
|
785
|
+
evidence: [],
|
|
786
|
+
fixHints: [
|
|
787
|
+
`Top unmatched prefixes: ${summary}`,
|
|
788
|
+
serverPrefix ? `Dominant server prefix inferred: ${serverPrefix}` : "No dominant server prefix detected.",
|
|
789
|
+
"If these should be local, improve route extraction for that prefix (Fastify register(prefix), Next middleware rewrites, basePath, etc.).",
|
|
790
|
+
],
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return findings;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/* ============================================================================
|
|
798
|
+
* ENV GAPS ANALYZER (tightened + fewer false positives)
|
|
799
|
+
* ========================================================================== */
|
|
800
|
+
|
|
801
|
+
function findEnvGaps(truthpack) {
|
|
802
|
+
const findings = [];
|
|
803
|
+
const used = truthpack?.env?.vars || [];
|
|
804
|
+
const declared = new Set(truthpack?.env?.declared || []);
|
|
805
|
+
const declaredSources = truthpack?.env?.declaredSources || [];
|
|
806
|
+
|
|
807
|
+
// Well-known system/CI env vars that shouldn't be flagged
|
|
808
|
+
const systemEnvVars = new Set([
|
|
809
|
+
"HOME","USER","PATH","PWD","SHELL","TERM","LANG","TZ","TMPDIR","TEMP","TMP","COLORTERM","FORCE_COLOR","NO_COLOR",
|
|
810
|
+
"APPDATA","LOCALAPPDATA","USERPROFILE","COMPUTERNAME","USERNAME","HOMEDRIVE","HOMEPATH","SYSTEMROOT","WINDIR",
|
|
811
|
+
"PROGRAMFILES","PROGRAMDATA","COMMONPROGRAMFILES",
|
|
812
|
+
"NODE_ENV","NODE_OPTIONS","NODE_PATH","NODE_DEBUG","NODE_NO_WARNINGS",
|
|
813
|
+
"CI","CONTINUOUS_INTEGRATION","BUILD_NUMBER","BUILD_ID",
|
|
814
|
+
"GITHUB_ACTIONS","GITHUB_WORKFLOW","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SHA","GITHUB_REF","GITHUB_ACTOR",
|
|
815
|
+
"GITLAB_CI","CI_COMMIT_SHA","CI_PIPELINE_ID","CI_JOB_ID",
|
|
816
|
+
"CIRCLECI","CIRCLE_BUILD_NUM","CIRCLE_SHA1","CIRCLE_BRANCH",
|
|
817
|
+
"TRAVIS","TRAVIS_BUILD_NUMBER","TRAVIS_COMMIT",
|
|
818
|
+
"JENKINS_URL","BUILD_TAG","GIT_COMMIT",
|
|
819
|
+
"BUILDKITE","BUILDKITE_BUILD_NUMBER","BUILDKITE_COMMIT",
|
|
820
|
+
"CODEBUILD_BUILD_ID","CODEBUILD_RESOLVED_SOURCE_VERSION",
|
|
821
|
+
"VERCEL","VERCEL_ENV","VERCEL_URL","VERCEL_GIT_COMMIT_SHA",
|
|
822
|
+
"NETLIFY","CONTEXT","DEPLOY_PRIME_URL",
|
|
823
|
+
"RAILWAY_ENVIRONMENT","RAILWAY_GIT_COMMIT_SHA",
|
|
824
|
+
"HEROKU","DYNO","RENDER","FLY_APP_NAME",
|
|
825
|
+
"HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy",
|
|
826
|
+
"HOSTNAME","HOST",
|
|
827
|
+
"DEBUG","VERBOSE","LOG_LEVEL",
|
|
828
|
+
"EDITOR","VISUAL","VSCODE_PID","TERM_SESSION_ID",
|
|
829
|
+
"PORT","npm_package_version","npm_package_name",
|
|
830
|
+
]);
|
|
831
|
+
|
|
832
|
+
// Patterns for env vars that are commonly optional/internal
|
|
833
|
+
const optionalPatterns = [
|
|
834
|
+
/^(OPENAI|ANTHROPIC|COHERE|AZURE|AWS|GCP|GOOGLE)_/i,
|
|
835
|
+
/^(STRIPE|PAYPAL|PLAID)_/i,
|
|
836
|
+
/^(SENDGRID|RESEND|MAILGUN|SES)_/i,
|
|
837
|
+
/^(SENTRY|DATADOG|NEWRELIC|LOGROCKET)_/i,
|
|
838
|
+
/^(REDIS|POSTGRES|MYSQL|MONGO|DATABASE)_/i,
|
|
839
|
+
/^(NEXT_|NUXT_|VITE_|REACT_APP_)/i,
|
|
840
|
+
/^(VIBECHECK|GUARDRAIL)_/i,
|
|
841
|
+
/_(URL|KEY|SECRET|TOKEN|ID|PASSWORD|HOST|PORT)$/i,
|
|
842
|
+
/^(ENABLE_|DISABLE_|USE_|SKIP_|ALLOW_|NO_)/i,
|
|
843
|
+
/^(MAX_|MIN_|DEFAULT_|TIMEOUT_|LIMIT_|RATE_)/i,
|
|
844
|
+
/^(LOG_|DEBUG_|VERBOSE_|TRACE_)/i,
|
|
845
|
+
/^(TEST_|DEV_|STAGING_|PROD_)/i,
|
|
846
|
+
/^(ARTIFACTS_|CACHE_|TMP_|OUTPUT_)/i,
|
|
847
|
+
/^npm_/i,
|
|
848
|
+
];
|
|
849
|
+
|
|
850
|
+
function isOptionalEnvVar(name) {
|
|
851
|
+
return optionalPatterns.some((p) => rxTest(p, name));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Heuristic: treat vars referenced only in tooling/scripts as WARN (not BLOCK)
|
|
855
|
+
function evidenceIsToolingOnly(v) {
|
|
856
|
+
const refs = v.references || [];
|
|
857
|
+
if (!refs.length) return false;
|
|
858
|
+
return refs.every((r) => {
|
|
859
|
+
const f = String(r.file || "");
|
|
860
|
+
return /(^|\/)(scripts|tools|bin|cli|devops|infra|config)\//i.test(f);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
for (const v of used) {
|
|
865
|
+
if (!v?.name) continue;
|
|
866
|
+
if (declared.has(v.name)) continue;
|
|
867
|
+
if (systemEnvVars.has(v.name)) continue;
|
|
868
|
+
if (isOptionalEnvVar(v.name)) continue;
|
|
869
|
+
|
|
870
|
+
const toolingOnly = evidenceIsToolingOnly(v);
|
|
871
|
+
|
|
872
|
+
// Only BLOCK if it's truly required, no fallback, and not tooling-only
|
|
873
|
+
const isReallyRequired = !!v.required && !v.hasFallback && !toolingOnly;
|
|
874
|
+
const sev = isReallyRequired ? "BLOCK" : "WARN";
|
|
875
|
+
|
|
876
|
+
findings.push({
|
|
877
|
+
id: `F_ENV_UNDECLARED_${v.name}`,
|
|
878
|
+
severity: sev,
|
|
879
|
+
category: "EnvContract",
|
|
880
|
+
title: `Env var used but not declared in env templates: ${v.name}`,
|
|
881
|
+
why: isReallyRequired
|
|
882
|
+
? "Required env var is used with no fallback and not documented. This ships broken installs."
|
|
883
|
+
: toolingOnly
|
|
884
|
+
? "Env var appears used in tooling/scripts. Document it if users need it; otherwise ignore."
|
|
885
|
+
: "Env var appears optional/guarded but should still be documented to prevent guesswork.",
|
|
886
|
+
confidence: isReallyRequired ? "high" : "low",
|
|
887
|
+
evidence: v.references || [],
|
|
888
|
+
fixHints: [
|
|
889
|
+
`Add to .env.example: ${v.name}=your_value_here`,
|
|
890
|
+
isReallyRequired
|
|
891
|
+
? `Add fallback: const value = process.env.${v.name} ?? 'default';`
|
|
892
|
+
: `Guard usage: if (process.env.${v.name}) { /* use it */ }`,
|
|
893
|
+
"Docs: https://12factor.net/config",
|
|
894
|
+
],
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Declared but never used
|
|
899
|
+
const usedSet = new Set(used.map((v) => v.name));
|
|
900
|
+
for (const name of declared) {
|
|
901
|
+
if (usedSet.has(name)) continue;
|
|
902
|
+
findings.push({
|
|
903
|
+
id: `F_ENV_UNUSED_${name}`,
|
|
904
|
+
severity: "WARN",
|
|
905
|
+
category: "EnvContract",
|
|
906
|
+
title: `Env var declared but never used: ${name}`,
|
|
907
|
+
why: "Dead config creates confusion and invites hallucinated wiring.",
|
|
908
|
+
confidence: "med",
|
|
909
|
+
evidence: [],
|
|
910
|
+
fixHints: [
|
|
911
|
+
"Remove it from templates if obsolete, or wire it intentionally.",
|
|
912
|
+
"If used only in infra/runtime, document that explicitly (where/why).",
|
|
913
|
+
],
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (!declaredSources.length && used.length) {
|
|
918
|
+
findings.push({
|
|
919
|
+
id: "F_ENV_NO_TEMPLATE",
|
|
920
|
+
severity: "WARN",
|
|
921
|
+
category: "EnvContract",
|
|
922
|
+
title: "No .env.example/.env.template found",
|
|
923
|
+
why: "Without an env contract, humans and AI guess env vars and ship broken setups.",
|
|
924
|
+
confidence: "high",
|
|
925
|
+
evidence: [],
|
|
926
|
+
fixHints: ["Add a .env.example listing required/optional vars with comments."],
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return findings;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* ============================================================================
|
|
934
|
+
* FAKE SUCCESS ANALYZER (kept, but made safer & less noisy)
|
|
935
|
+
* ========================================================================== */
|
|
936
|
+
|
|
937
|
+
function isToastSuccessCall(node) {
|
|
938
|
+
return !!(
|
|
939
|
+
t.isCallExpression(node) &&
|
|
940
|
+
t.isMemberExpression(node.callee) &&
|
|
941
|
+
t.isIdentifier(node.callee.object, { name: "toast" }) &&
|
|
942
|
+
t.isIdentifier(node.callee.property, { name: "success" })
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function isRouterPushCall(node) {
|
|
947
|
+
return (
|
|
948
|
+
t.isCallExpression(node) &&
|
|
949
|
+
((t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property, { name: "push" })) ||
|
|
950
|
+
(t.isIdentifier(node.callee) && node.callee.name === "navigate"))
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function isFetchCall(node) {
|
|
955
|
+
return !!(t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "fetch" }));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function isAxiosCall(node) {
|
|
959
|
+
return !!(
|
|
960
|
+
t.isCallExpression(node) &&
|
|
961
|
+
t.isMemberExpression(node.callee) &&
|
|
962
|
+
t.isIdentifier(node.callee.object, { name: "axios" }) &&
|
|
963
|
+
t.isIdentifier(node.callee.property) &&
|
|
964
|
+
["get", "post", "put", "patch", "delete"].includes(node.callee.property.name)
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function findFakeSuccess(repoRoot) {
|
|
969
|
+
const findings = [];
|
|
970
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
971
|
+
cwd: repoRoot,
|
|
972
|
+
absolute: true,
|
|
973
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
for (const fileAbs of files) {
|
|
977
|
+
const code = readFileCached(fileAbs);
|
|
978
|
+
|
|
979
|
+
// V3: FAST PATH OPTIMIZATION
|
|
980
|
+
// AST parsing is 100x slower than regex. Skip files that don't contain
|
|
981
|
+
// relevant keywords (toast/push/navigate AND fetch/axios).
|
|
982
|
+
const hasSuccessUI = /\b(toast|\.push|navigate)\b/.test(code);
|
|
983
|
+
const hasNetworkCall = /\b(fetch|axios)\b/.test(code);
|
|
984
|
+
if (!hasSuccessUI || !hasNetworkCall) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
let ast;
|
|
989
|
+
try {
|
|
990
|
+
ast = parseFile(code, fileAbs);
|
|
991
|
+
} catch {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
try {
|
|
996
|
+
traverse(ast, {
|
|
997
|
+
Function(pathFn) {
|
|
998
|
+
// Collect call sites with positions to reduce false positives.
|
|
999
|
+
const successCalls = [];
|
|
1000
|
+
const networkCalls = [];
|
|
1001
|
+
const okChecks = [];
|
|
1002
|
+
|
|
1003
|
+
pathFn.traverse({
|
|
1004
|
+
CallExpression(p) {
|
|
1005
|
+
const n = p.node;
|
|
1006
|
+
|
|
1007
|
+
if (isToastSuccessCall(n) || isRouterPushCall(n)) {
|
|
1008
|
+
successCalls.push({ loc: n.loc, pos: n.start ?? 0 });
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (isFetchCall(n) || isAxiosCall(n)) {
|
|
1012
|
+
const isAwaited = p.parentPath && p.parentPath.isAwaitExpression();
|
|
1013
|
+
networkCalls.push({ pos: n.start ?? 0, awaited: !!isAwaited });
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
IfStatement(p) {
|
|
1017
|
+
const test = p.node.test;
|
|
1018
|
+
const txt = code.slice(test.start || 0, test.end || 0);
|
|
1019
|
+
// Check for response status validation (res.ok, status)
|
|
1020
|
+
if (/\b(res|response)\b/i.test(txt) && /\b(ok|status)\b/i.test(txt)) okChecks.push({ pos: p.node.start ?? 0 });
|
|
1021
|
+
// Check for response body validation (data, error, success property checks)
|
|
1022
|
+
if (/\b(data|result|response)\b/i.test(txt) && /\b(error|success|\.data|\.result)\b/i.test(txt)) okChecks.push({ pos: p.node.start ?? 0 });
|
|
1023
|
+
},
|
|
1024
|
+
// Also check for try-catch around the network call as a form of validation
|
|
1025
|
+
TryStatement(tryPath) {
|
|
1026
|
+
if (tryPath.node.handler) {
|
|
1027
|
+
okChecks.push({ pos: tryPath.node.start ?? 0 });
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
if (!successCalls.length || !networkCalls.length) return;
|
|
1033
|
+
|
|
1034
|
+
// For each success call: if there exists an awaited network call before it, it's less severe.
|
|
1035
|
+
for (const sc of successCalls) {
|
|
1036
|
+
const netBefore = networkCalls.filter((n) => n.pos < sc.pos);
|
|
1037
|
+
const awaitedBefore = netBefore.some((n) => n.awaited);
|
|
1038
|
+
const okCheckBefore = okChecks.some((c) => c.pos < sc.pos);
|
|
1039
|
+
|
|
1040
|
+
// If no awaited call before success -> strong signal
|
|
1041
|
+
const severity = awaitedBefore ? (okCheckBefore ? null : "WARN") : "BLOCK";
|
|
1042
|
+
if (!severity) continue;
|
|
1043
|
+
|
|
1044
|
+
const ev = evidenceFromLoc(fileAbs, repoRoot, sc.loc, "Success UI call in networked flow");
|
|
1045
|
+
findings.push({
|
|
1046
|
+
id: stableId("F_FAKE_SUCCESS", `${path.relative(repoRoot, fileAbs)}:${sc.pos}:${severity}`),
|
|
1047
|
+
severity,
|
|
1048
|
+
category: "FakeSuccess",
|
|
1049
|
+
title:
|
|
1050
|
+
severity === "BLOCK"
|
|
1051
|
+
? "Success UI triggered without awaiting network call"
|
|
1052
|
+
: "Success UI triggered without verifying network result (res.ok/status)",
|
|
1053
|
+
why:
|
|
1054
|
+
severity === "BLOCK"
|
|
1055
|
+
? "This ships lies. Users see success even when the request never completed."
|
|
1056
|
+
: "You're not gating success on a real response; this often ships false success.",
|
|
1057
|
+
confidence: "med",
|
|
1058
|
+
evidence: ev ? [ev] : [],
|
|
1059
|
+
fixHints: [
|
|
1060
|
+
"const res = await fetch(...); if (!res.ok) throw new Error('Request failed');",
|
|
1061
|
+
"const data = await res.json(); if (data.success) toast.success('Done!'); else toast.error(data.error);",
|
|
1062
|
+
"Use try-catch: try { await api(); toast.success(); } catch (e) { toast.error(e.message); }",
|
|
1063
|
+
],
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
} catch {
|
|
1069
|
+
// Babel traverse can fail on some edge-case files; skip them
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return findings;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/* ============================================================================
|
|
1078
|
+
* GHOST AUTH ANALYZER (kept)
|
|
1079
|
+
* ========================================================================== */
|
|
1080
|
+
|
|
1081
|
+
function looksSensitive(pathStr) {
|
|
1082
|
+
const p = String(pathStr || "");
|
|
1083
|
+
return (
|
|
1084
|
+
p.startsWith("/api/admin") ||
|
|
1085
|
+
p.startsWith("/api/billing") ||
|
|
1086
|
+
p.startsWith("/api/stripe") ||
|
|
1087
|
+
p.startsWith("/api/org") ||
|
|
1088
|
+
p.startsWith("/api/team") ||
|
|
1089
|
+
p.startsWith("/api/account") ||
|
|
1090
|
+
p.startsWith("/api/settings") ||
|
|
1091
|
+
p.startsWith("/api/users") ||
|
|
1092
|
+
p.startsWith("/api/user")
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function hasRouteLevelProtection(routeDef) {
|
|
1097
|
+
const hooks = routeDef.hooks || [];
|
|
1098
|
+
return !!(hooks.includes("preHandler") || hooks.includes("onRequest") || hooks.includes("preValidation"));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function handlerHasAuthSignal(repoRoot, handlerRel) {
|
|
1102
|
+
const abs = path.join(repoRoot, handlerRel);
|
|
1103
|
+
if (!fs.existsSync(abs)) return false;
|
|
1104
|
+
const code = readFileCached(abs);
|
|
1105
|
+
|
|
1106
|
+
return (
|
|
1107
|
+
/\bgetServerSession\b|\bauth\(\)\b|\bclerk\b|@clerk\/nextjs|\bcreateRouteHandlerClient\b|@supabase/i.test(code) ||
|
|
1108
|
+
/\b(jwtVerify|authorization|bearer|verifyToken|verifyJWT)\b/i.test(code) ||
|
|
1109
|
+
/\b(isAdmin|adminOnly|permissions|rbac)\b/i.test(code)
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function isProtectedByNextMiddleware(truthpack, routePath) {
|
|
1114
|
+
const patterns = truthpack?.auth?.nextMatcherPatterns || [];
|
|
1115
|
+
return !!matcherCoversPath(patterns, routePath);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function findGhostAuth(truthpack, repoRoot) {
|
|
1119
|
+
const findings = [];
|
|
1120
|
+
const server = truthpack?.routes?.server || [];
|
|
1121
|
+
|
|
1122
|
+
// Track mutation routes without CSRF protection
|
|
1123
|
+
const mutationMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1124
|
+
|
|
1125
|
+
for (const r of server) {
|
|
1126
|
+
const isMutation = mutationMethods.has(String(r.method).toUpperCase());
|
|
1127
|
+
|
|
1128
|
+
if (!looksSensitive(r.path)) continue;
|
|
1129
|
+
|
|
1130
|
+
const middlewareProtected = isProtectedByNextMiddleware(truthpack, r.path);
|
|
1131
|
+
const routeHooksProtected = hasRouteLevelProtection(r);
|
|
1132
|
+
const handlerProtected = r.handler ? handlerHasAuthSignal(repoRoot, r.handler) : false;
|
|
1133
|
+
|
|
1134
|
+
const protectedSomehow = middlewareProtected || routeHooksProtected || handlerProtected;
|
|
1135
|
+
|
|
1136
|
+
if (!protectedSomehow) {
|
|
1137
|
+
findings.push({
|
|
1138
|
+
id: stableId("F_GHOST_AUTH", `${r.method} ${r.path}`),
|
|
1139
|
+
severity: "BLOCK",
|
|
1140
|
+
category: "GhostAuth",
|
|
1141
|
+
title: `Sensitive endpoint appears unprotected: ${r.method} ${r.path}`,
|
|
1142
|
+
why: "If the server doesn't enforce auth, it's public. UI gating is irrelevant.",
|
|
1143
|
+
confidence: "med",
|
|
1144
|
+
evidence: (r.evidence || []).slice(0, 2),
|
|
1145
|
+
fixHints: [
|
|
1146
|
+
`Next.js: const session = await getServerSession(); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });`,
|
|
1147
|
+
`Fastify: fastify.addHook('preHandler', async (req, reply) => { if (!req.user) reply.code(401).send({ error: 'Unauthorized' }); });`,
|
|
1148
|
+
"Or add to middleware.ts matcher: export const config = { matcher: ['/api/admin/:path*'] };",
|
|
1149
|
+
"Docs: https://nextjs.org/docs/app/building-your-application/authentication",
|
|
1150
|
+
],
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Check for CSRF protection on mutations
|
|
1155
|
+
if (isMutation && r.handler) {
|
|
1156
|
+
const abs = path.join(repoRoot, r.handler);
|
|
1157
|
+
if (fs.existsSync(abs)) {
|
|
1158
|
+
const code = readFileCached(abs);
|
|
1159
|
+
const hasCSRFCheck = /\b(csrf|csrfToken|_csrf|x-csrf-token|xsrf|anti-forgery)\b/i.test(code);
|
|
1160
|
+
const isAPIRoute = r.path.startsWith("/api/");
|
|
1161
|
+
|
|
1162
|
+
// Only warn for non-API routes (API routes typically use bearer tokens)
|
|
1163
|
+
if (!hasCSRFCheck && !isAPIRoute) {
|
|
1164
|
+
findings.push({
|
|
1165
|
+
id: stableId("F_GHOST_AUTH_NO_CSRF", `${r.method} ${r.path}`),
|
|
1166
|
+
severity: "WARN",
|
|
1167
|
+
category: "GhostAuth",
|
|
1168
|
+
title: `Mutation endpoint without CSRF protection: ${r.method} ${r.path}`,
|
|
1169
|
+
why: "State-changing endpoints should verify CSRF tokens to prevent cross-site request forgery attacks.",
|
|
1170
|
+
confidence: "low",
|
|
1171
|
+
evidence: (r.evidence || []).slice(0, 2),
|
|
1172
|
+
fixHints: [
|
|
1173
|
+
"Add CSRF token validation for form submissions.",
|
|
1174
|
+
"Or use SameSite cookies + Origin header validation.",
|
|
1175
|
+
"API routes using bearer tokens are generally exempt.",
|
|
1176
|
+
],
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const patterns = truthpack?.auth?.nextMatcherPatterns || [];
|
|
1184
|
+
if (patterns.length) {
|
|
1185
|
+
const coversApi = patterns.some((p) => String(p).includes("/api"));
|
|
1186
|
+
if (!coversApi) {
|
|
1187
|
+
findings.push({
|
|
1188
|
+
id: "F_MIDDLEWARE_NOT_COVERING_API",
|
|
1189
|
+
severity: "WARN",
|
|
1190
|
+
category: "GhostAuth",
|
|
1191
|
+
title: "Next middleware exists but does not appear to cover /api routes",
|
|
1192
|
+
why: "People assume middleware protects APIs. Often it doesn't. Verify matcher patterns.",
|
|
1193
|
+
confidence: "high",
|
|
1194
|
+
evidence: (truthpack?.auth?.nextMiddleware?.[0]?.evidence || []).slice(0, 3),
|
|
1195
|
+
fixHints: ["Add /api/:path* to middleware matcher if your design expects API auth protection."],
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return findings;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/* ============================================================================
|
|
1204
|
+
* STRIPE WEBHOOK VIOLATIONS (kept)
|
|
1205
|
+
* ========================================================================== */
|
|
1206
|
+
|
|
1207
|
+
function findStripeWebhookViolations(truthpack) {
|
|
1208
|
+
const findings = [];
|
|
1209
|
+
const billing = truthpack?.billing;
|
|
1210
|
+
|
|
1211
|
+
if (!billing?.hasStripe) return findings;
|
|
1212
|
+
|
|
1213
|
+
const candidates = billing.webhookCandidates || [];
|
|
1214
|
+
|
|
1215
|
+
if (!candidates.length) {
|
|
1216
|
+
findings.push({
|
|
1217
|
+
id: "F_STRIPE_NO_WEBHOOK_HANDLER",
|
|
1218
|
+
severity: "WARN",
|
|
1219
|
+
category: "Billing",
|
|
1220
|
+
title: "Stripe appears used but no webhook handler candidate detected",
|
|
1221
|
+
why: "Stripe billing usually needs webhooks; missing them causes subscription state desync.",
|
|
1222
|
+
confidence: "med",
|
|
1223
|
+
evidence: [],
|
|
1224
|
+
fixHints: ["Add a Stripe webhook handler with signature verification and idempotency."],
|
|
1225
|
+
});
|
|
1226
|
+
return findings;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
for (const w of candidates) {
|
|
1230
|
+
const verified = w.signals.webhookConstructEvent && w.signals.rawBodySignal && w.signals.readsStripeSignatureHeader;
|
|
1231
|
+
const idempotent = !!w.signals.idempotencySignal;
|
|
1232
|
+
|
|
1233
|
+
if (!verified) {
|
|
1234
|
+
findings.push({
|
|
1235
|
+
id: stableId("F_STRIPE_WEBHOOK_NOT_VERIFIED", w.file),
|
|
1236
|
+
severity: "BLOCK",
|
|
1237
|
+
category: "Billing",
|
|
1238
|
+
title: `Stripe webhook handler not clearly signature-verified: ${w.file}`,
|
|
1239
|
+
why: "Unverified webhooks = spoofable billing state.",
|
|
1240
|
+
confidence: "high",
|
|
1241
|
+
evidence: (w.evidence || []).slice(0, 4),
|
|
1242
|
+
fixHints: [
|
|
1243
|
+
"Use stripe.webhooks.constructEvent(rawBody, sigHeader, STRIPE_WEBHOOK_SECRET).",
|
|
1244
|
+
"Ensure raw body is used (disable bodyParser in pages router; in app router read req.text()/arrayBuffer).",
|
|
1245
|
+
"Reject if signature missing/invalid.",
|
|
1246
|
+
],
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (!idempotent) {
|
|
1251
|
+
findings.push({
|
|
1252
|
+
id: stableId("F_STRIPE_WEBHOOK_NOT_IDEMPOTENT", w.file),
|
|
1253
|
+
severity: "BLOCK",
|
|
1254
|
+
category: "Billing",
|
|
1255
|
+
title: `Stripe webhook handler not clearly idempotent: ${w.file}`,
|
|
1256
|
+
why: "Stripe retries webhooks; without dedupe you can double-grant access or double-write state.",
|
|
1257
|
+
confidence: "med",
|
|
1258
|
+
evidence: (w.evidence || []).slice(0, 4),
|
|
1259
|
+
fixHints: [
|
|
1260
|
+
"Persist event.id as processed (DB/Redis). If seen, return 200 immediately.",
|
|
1261
|
+
"Wrap state mutation in a transaction keyed by event.id.",
|
|
1262
|
+
],
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return findings;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/* ============================================================================
|
|
1271
|
+
* PAID SURFACE NOT ENFORCED (kept)
|
|
1272
|
+
* ========================================================================== */
|
|
1273
|
+
|
|
1274
|
+
function findPaidSurfaceNotEnforced(truthpack) {
|
|
1275
|
+
const findings = [];
|
|
1276
|
+
const enforcement = truthpack?.enforcement;
|
|
1277
|
+
const checks = enforcement?.checks || [];
|
|
1278
|
+
|
|
1279
|
+
for (const c of checks) {
|
|
1280
|
+
if (c.enforced) continue;
|
|
1281
|
+
findings.push({
|
|
1282
|
+
id: stableId("F_PAID_SURFACE_NOT_ENFORCED", `${c.method} ${c.path}`),
|
|
1283
|
+
severity: "BLOCK",
|
|
1284
|
+
category: "Entitlements",
|
|
1285
|
+
title: `Paid surface appears un-enforced server-side: ${c.method} ${c.path}`,
|
|
1286
|
+
why: "If enforcement is only in the CLI/UI, users can call the endpoint directly.",
|
|
1287
|
+
confidence: "med",
|
|
1288
|
+
evidence: [],
|
|
1289
|
+
fixHints: [
|
|
1290
|
+
"Enforce in the server handler BEFORE doing work.",
|
|
1291
|
+
"Return 402/403 with a structured error code.",
|
|
1292
|
+
"Make the CLI treat that code as an upgrade prompt.",
|
|
1293
|
+
],
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
return findings;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/* ============================================================================
|
|
1300
|
+
* OWNER MODE BYPASS (kept; uses deterministic regex testing)
|
|
1301
|
+
* ========================================================================== */
|
|
1302
|
+
|
|
1303
|
+
function findOwnerModeBypass(repoRoot) {
|
|
1304
|
+
const findings = [];
|
|
1305
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1306
|
+
cwd: repoRoot,
|
|
1307
|
+
absolute: true,
|
|
1308
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
const patterns = [
|
|
1312
|
+
/OWNER_MODE/i,
|
|
1313
|
+
/GUARDRAIL_OWNER_MODE/i,
|
|
1314
|
+
/VIBECHECK_OWNER_MODE/i,
|
|
1315
|
+
/process\.env\.[A-Z0-9_]*OWNER[A-Z0-9_]*/i,
|
|
1316
|
+
];
|
|
1317
|
+
|
|
1318
|
+
for (const fileAbs of files) {
|
|
1319
|
+
const code = readFileCached(fileAbs);
|
|
1320
|
+
const hit = patterns.some((rx) => rxTest(rx, code));
|
|
1321
|
+
if (!hit) continue;
|
|
1322
|
+
|
|
1323
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1324
|
+
|
|
1325
|
+
findings.push({
|
|
1326
|
+
id: stableId("F_OWNER_MODE_BYPASS", fileRel),
|
|
1327
|
+
severity: "BLOCK",
|
|
1328
|
+
category: "Security",
|
|
1329
|
+
title: `Owner mode / env bypass signal detected: ${fileRel}`,
|
|
1330
|
+
why: "This is a production backdoor unless cryptographically gated. It cannot ship.",
|
|
1331
|
+
confidence: "high",
|
|
1332
|
+
evidence: [],
|
|
1333
|
+
fixHints: [
|
|
1334
|
+
"Delete owner mode bypass. If you need dev override, require a signed admin token + non-prod environment.",
|
|
1335
|
+
"Add a test that asserts no OWNER_MODE env var grants entitlements.",
|
|
1336
|
+
],
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return findings;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/* ============================================================================
|
|
1344
|
+
* MOCK DATA DETECTOR (fixed /g+.test() bug + better line discovery)
|
|
1345
|
+
* ========================================================================== */
|
|
1346
|
+
|
|
1347
|
+
function findMockData(repoRoot) {
|
|
1348
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1349
|
+
const findings = [];
|
|
1350
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1351
|
+
cwd: repoRoot,
|
|
1352
|
+
absolute: true,
|
|
1353
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
for (const fileAbs of files) {
|
|
1357
|
+
try {
|
|
1358
|
+
const code = readFileCached(fileAbs);
|
|
1359
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1360
|
+
|
|
1361
|
+
// Use unified engines
|
|
1362
|
+
const engineFindings = engines.analyzeMockData(code, fileRel);
|
|
1363
|
+
|
|
1364
|
+
// Convert engine findings to analyzer format
|
|
1365
|
+
for (const finding of engineFindings) {
|
|
1366
|
+
findings.push({
|
|
1367
|
+
id: stableId("F_MOCK_DATA", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1368
|
+
severity: finding.severity,
|
|
1369
|
+
category: finding.category,
|
|
1370
|
+
title: finding.title,
|
|
1371
|
+
message: finding.message,
|
|
1372
|
+
file: finding.file,
|
|
1373
|
+
line: finding.line,
|
|
1374
|
+
why: "Mock/fake data in production causes embarrassing bugs and makes your app look unfinished.",
|
|
1375
|
+
confidence: finding.confidence,
|
|
1376
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1377
|
+
fixHints: [
|
|
1378
|
+
"Replace mock data with real API calls or database queries.",
|
|
1379
|
+
"If intentional sample data, move to a clearly marked demo mode.",
|
|
1380
|
+
],
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
// Skip files that can't be analyzed
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return findings;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/* ============================================================================
|
|
1393
|
+
* TODO/FIXME DETECTOR (fixed /g+.test() bug)
|
|
1394
|
+
* ========================================================================== */
|
|
1395
|
+
|
|
1396
|
+
function findTodoFixme(repoRoot) {
|
|
1397
|
+
// TODO/FIXME engine not in unified engines yet - keep using existing
|
|
1398
|
+
const { analyzeTodoFixme } = require("./engines/todo-fixme-engine");
|
|
1399
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1400
|
+
const findings = [];
|
|
1401
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1402
|
+
cwd: repoRoot,
|
|
1403
|
+
absolute: true,
|
|
1404
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
for (const fileAbs of files) {
|
|
1408
|
+
try {
|
|
1409
|
+
const code = readFileCached(fileAbs);
|
|
1410
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1411
|
+
|
|
1412
|
+
// Use AST-based engine (not in unified engines yet)
|
|
1413
|
+
const engineFindings = analyzeTodoFixme(code, fileRel);
|
|
1414
|
+
|
|
1415
|
+
// Convert engine findings to analyzer format
|
|
1416
|
+
for (const finding of engineFindings) {
|
|
1417
|
+
if (finding.type === "summary") {
|
|
1418
|
+
findings.push({
|
|
1419
|
+
id: "F_TODO_SUMMARY",
|
|
1420
|
+
severity: finding.severity,
|
|
1421
|
+
category: finding.category,
|
|
1422
|
+
title: finding.title,
|
|
1423
|
+
why: "Large numbers of TODO comments indicate significant unfinished work.",
|
|
1424
|
+
confidence: finding.confidence,
|
|
1425
|
+
evidence: [],
|
|
1426
|
+
fixHints: [
|
|
1427
|
+
"Review and address high-priority TODOs before shipping.",
|
|
1428
|
+
`Run: vibecheck scan --json | jq '.findings[] | select(.category == "TODO")'`,
|
|
1429
|
+
],
|
|
1430
|
+
});
|
|
1431
|
+
} else {
|
|
1432
|
+
findings.push({
|
|
1433
|
+
id: stableId("F_TODO", `${fileRel}:${finding.line}:${finding.type}`),
|
|
1434
|
+
severity: finding.severity,
|
|
1435
|
+
category: finding.category,
|
|
1436
|
+
title: finding.title,
|
|
1437
|
+
message: finding.message,
|
|
1438
|
+
file: finding.file,
|
|
1439
|
+
line: finding.line,
|
|
1440
|
+
why: finding.severity === "BLOCK"
|
|
1441
|
+
? "This comment indicates a known critical issue that must be addressed before shipping."
|
|
1442
|
+
: "Unfinished work markers suggest the code isn't production-ready.",
|
|
1443
|
+
confidence: finding.confidence,
|
|
1444
|
+
evidence: [{ file: fileRel, lines: `${finding.line}`, reason: finding.title }],
|
|
1445
|
+
fixHints: [
|
|
1446
|
+
"Complete the TODO or remove it if already done.",
|
|
1447
|
+
"If deferring, create a tracked issue and reference it in the comment.",
|
|
1448
|
+
],
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
// Skip files that can't be analyzed
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return findings;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/* ============================================================================
|
|
1462
|
+
* CONSOLE.LOG DETECTOR (kept)
|
|
1463
|
+
* ========================================================================== */
|
|
1464
|
+
|
|
1465
|
+
function findConsoleLogs(repoRoot) {
|
|
1466
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1467
|
+
const findings = [];
|
|
1468
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1469
|
+
cwd: repoRoot,
|
|
1470
|
+
absolute: true,
|
|
1471
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
for (const fileAbs of files) {
|
|
1475
|
+
try {
|
|
1476
|
+
const code = readFileCached(fileAbs);
|
|
1477
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1478
|
+
|
|
1479
|
+
// Use unified engines
|
|
1480
|
+
const engineFindings = engines.analyzeConsoleLogs(code, fileRel);
|
|
1481
|
+
|
|
1482
|
+
// Convert engine findings to analyzer format
|
|
1483
|
+
for (const finding of engineFindings) {
|
|
1484
|
+
findings.push({
|
|
1485
|
+
id: stableId("F_CONSOLE_LOG", `${fileRel}:${finding.line}:${finding.type}`),
|
|
1486
|
+
severity: finding.severity,
|
|
1487
|
+
category: finding.category,
|
|
1488
|
+
title: finding.title,
|
|
1489
|
+
message: finding.message,
|
|
1490
|
+
file: finding.file,
|
|
1491
|
+
line: finding.line,
|
|
1492
|
+
why: "Console statements leak debugging info and clutter logs/console.",
|
|
1493
|
+
confidence: finding.confidence,
|
|
1494
|
+
evidence: [{ file: fileRel, lines: `${finding.line}`, reason: finding.codeSnippet || finding.title }],
|
|
1495
|
+
fixHints: ["Remove console.log or replace with a proper logger.", "Use a logger that can be silenced in production."],
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
// Skip files that can't be analyzed
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return findings;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/* ============================================================================
|
|
1508
|
+
* HARDCODED SECRETS DETECTOR (kept)
|
|
1509
|
+
* ========================================================================== */
|
|
1510
|
+
|
|
1511
|
+
function findHardcodedSecrets(repoRoot) {
|
|
1512
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1513
|
+
const findings = [];
|
|
1514
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx,json}"], {
|
|
1515
|
+
cwd: repoRoot,
|
|
1516
|
+
absolute: true,
|
|
1517
|
+
ignore: [...STANDARD_IGNORE_PATTERNS, "**/package*.json"],
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
for (const fileAbs of files) {
|
|
1521
|
+
try {
|
|
1522
|
+
const code = readFileCached(fileAbs);
|
|
1523
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1524
|
+
|
|
1525
|
+
// Use unified engines
|
|
1526
|
+
const engineFindings = engines.analyzeHardcodedSecrets(code, fileRel);
|
|
1527
|
+
|
|
1528
|
+
// Convert engine findings to analyzer format
|
|
1529
|
+
for (const finding of engineFindings) {
|
|
1530
|
+
findings.push({
|
|
1531
|
+
id: stableId("F_SECRET", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1532
|
+
severity: finding.severity,
|
|
1533
|
+
category: finding.category,
|
|
1534
|
+
title: finding.title,
|
|
1535
|
+
message: finding.message,
|
|
1536
|
+
file: finding.file,
|
|
1537
|
+
line: finding.line,
|
|
1538
|
+
why: finding.severity === "BLOCK"
|
|
1539
|
+
? "Hardcoded secrets get committed and leaked. This is critical."
|
|
1540
|
+
: "This string looks mathematically random, which usually indicates a hardcoded secret key.",
|
|
1541
|
+
confidence: finding.confidence,
|
|
1542
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1543
|
+
fixHints: finding.severity === "BLOCK"
|
|
1544
|
+
? [
|
|
1545
|
+
"Move the secret to environment variables.",
|
|
1546
|
+
"Rotate the compromised secret immediately.",
|
|
1547
|
+
"Add the file to .gitignore if it shouldn't be committed.",
|
|
1548
|
+
]
|
|
1549
|
+
: [
|
|
1550
|
+
"Move the secret to environment variables.",
|
|
1551
|
+
"If this is not a secret, consider using a more descriptive variable name.",
|
|
1552
|
+
],
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
// Skip files that can't be analyzed
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
return findings;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/* ============================================================================
|
|
1565
|
+
* DEAD CODE / UNUSED EXPORTS DETECTOR (AST-based engine)
|
|
1566
|
+
* ========================================================================== */
|
|
1567
|
+
|
|
1568
|
+
function findDeadCode(repoRoot) {
|
|
1569
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1570
|
+
const findings = [];
|
|
1571
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1572
|
+
cwd: repoRoot,
|
|
1573
|
+
absolute: true,
|
|
1574
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
for (const fileAbs of files) {
|
|
1578
|
+
try {
|
|
1579
|
+
const code = readFileCached(fileAbs);
|
|
1580
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1581
|
+
|
|
1582
|
+
// Use unified engines
|
|
1583
|
+
const engineFindings = engines.analyzeDeadCode(code, fileRel);
|
|
1584
|
+
|
|
1585
|
+
// Convert engine findings to analyzer format
|
|
1586
|
+
for (const finding of engineFindings) {
|
|
1587
|
+
findings.push({
|
|
1588
|
+
id: stableId("F_DEAD_CODE", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1589
|
+
severity: finding.severity,
|
|
1590
|
+
category: finding.category,
|
|
1591
|
+
title: finding.title,
|
|
1592
|
+
message: finding.message,
|
|
1593
|
+
file: finding.file,
|
|
1594
|
+
line: finding.line,
|
|
1595
|
+
why: "Dead code adds confusion and maintenance burden and usually indicates incomplete refactoring.",
|
|
1596
|
+
confidence: finding.confidence,
|
|
1597
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1598
|
+
fixHints: ["Remove the dead code entirely.", "If needed for reference, use git history instead of commenting."],
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
// Skip files that can't be analyzed
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return findings;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/* ============================================================================
|
|
1611
|
+
* DEPRECATED API USAGE DETECTOR (kept; deterministic)
|
|
1612
|
+
* ========================================================================== */
|
|
1613
|
+
|
|
1614
|
+
function findDeprecatedApis(repoRoot) {
|
|
1615
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1616
|
+
const findings = [];
|
|
1617
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1618
|
+
cwd: repoRoot,
|
|
1619
|
+
absolute: true,
|
|
1620
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
for (const fileAbs of files) {
|
|
1624
|
+
try {
|
|
1625
|
+
const code = readFileCached(fileAbs);
|
|
1626
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1627
|
+
|
|
1628
|
+
// Use unified engines
|
|
1629
|
+
const engineFindings = engines.analyzeDeprecatedApi(code, fileRel);
|
|
1630
|
+
|
|
1631
|
+
// Convert engine findings to analyzer format
|
|
1632
|
+
for (const finding of engineFindings) {
|
|
1633
|
+
findings.push({
|
|
1634
|
+
id: stableId("F_DEPRECATED", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1635
|
+
severity: finding.severity,
|
|
1636
|
+
category: finding.category,
|
|
1637
|
+
title: finding.title,
|
|
1638
|
+
message: finding.message,
|
|
1639
|
+
file: finding.file,
|
|
1640
|
+
line: finding.line,
|
|
1641
|
+
why: "Deprecated APIs may break in future versions and sometimes carry security issues.",
|
|
1642
|
+
confidence: finding.confidence,
|
|
1643
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1644
|
+
fixHints: finding.replacement ? [`Use ${finding.replacement} instead.`, "Check migration guides for the specific deprecation."] : ["Update to the modern API equivalent.", "Check migration guides for the specific deprecation."],
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
// Skip files that can't be analyzed
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
return findings;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/* ============================================================================
|
|
1657
|
+
* EMPTY CATCH BLOCKS DETECTOR (AST-based engine)
|
|
1658
|
+
* ========================================================================== */
|
|
1659
|
+
|
|
1660
|
+
function findEmptyCatch(repoRoot) {
|
|
1661
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1662
|
+
const findings = [];
|
|
1663
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1664
|
+
cwd: repoRoot,
|
|
1665
|
+
absolute: true,
|
|
1666
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
for (const fileAbs of files) {
|
|
1670
|
+
try {
|
|
1671
|
+
const code = readFileCached(fileAbs);
|
|
1672
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1673
|
+
|
|
1674
|
+
// Use unified engines
|
|
1675
|
+
const engineFindings = engines.analyzeEmptyCatch(code, fileRel);
|
|
1676
|
+
|
|
1677
|
+
// Convert engine findings to analyzer format
|
|
1678
|
+
for (const finding of engineFindings) {
|
|
1679
|
+
findings.push({
|
|
1680
|
+
id: stableId("F_EMPTY_CATCH", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1681
|
+
severity: finding.severity,
|
|
1682
|
+
category: finding.category,
|
|
1683
|
+
title: finding.title,
|
|
1684
|
+
message: finding.message,
|
|
1685
|
+
file: finding.file,
|
|
1686
|
+
line: finding.line,
|
|
1687
|
+
why: "Empty catch blocks swallow errors and make debugging impossible.",
|
|
1688
|
+
confidence: finding.confidence,
|
|
1689
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1690
|
+
fixHints: ["Log the error or handle it appropriately.", "If intentionally ignoring, add a comment explaining why."],
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
// Skip files that can't be analyzed
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
return findings;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/* ============================================================================
|
|
1703
|
+
* UNSAFE REGEX DETECTOR (fixed /g+.test() bug)
|
|
1704
|
+
* ========================================================================== */
|
|
1705
|
+
|
|
1706
|
+
function findUnsafeRegex(repoRoot) {
|
|
1707
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1708
|
+
const findings = [];
|
|
1709
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1710
|
+
cwd: repoRoot,
|
|
1711
|
+
absolute: true,
|
|
1712
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
for (const fileAbs of files) {
|
|
1716
|
+
try {
|
|
1717
|
+
const code = readFileCached(fileAbs);
|
|
1718
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1719
|
+
|
|
1720
|
+
// Use unified engines
|
|
1721
|
+
const engineFindings = engines.analyzeUnsafeRegex(code, fileRel);
|
|
1722
|
+
|
|
1723
|
+
// Convert engine findings to analyzer format
|
|
1724
|
+
for (const finding of engineFindings) {
|
|
1725
|
+
findings.push({
|
|
1726
|
+
id: stableId("F_UNSAFE_REGEX", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1727
|
+
severity: finding.severity,
|
|
1728
|
+
category: finding.category,
|
|
1729
|
+
title: finding.title,
|
|
1730
|
+
message: finding.message,
|
|
1731
|
+
file: finding.file,
|
|
1732
|
+
line: finding.line,
|
|
1733
|
+
why: "Unsafe regex patterns can cause denial of service via catastrophic backtracking.",
|
|
1734
|
+
confidence: finding.confidence,
|
|
1735
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1736
|
+
fixHints: ["Validate input length before applying regex.", "Consider safer parsing or a regex linter.", "Avoid nested quantifiers."],
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
// Skip files that can't be analyzed
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
return findings;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/* ============================================================================
|
|
1749
|
+
* NEW SECURITY & PERFORMANCE ANALYZERS
|
|
1750
|
+
* ========================================================================== */
|
|
1751
|
+
|
|
1752
|
+
function findSecurityVulnerabilities(repoRoot) {
|
|
1753
|
+
// Note: Security vulnerabilities engine not in unified engines yet
|
|
1754
|
+
// Keep using the existing engine for now
|
|
1755
|
+
const { analyzeSecurityVulnerabilities } = require("./engines/security-vulnerabilities-engine");
|
|
1756
|
+
const findings = [];
|
|
1757
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1758
|
+
cwd: repoRoot,
|
|
1759
|
+
absolute: true,
|
|
1760
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
for (const fileAbs of files) {
|
|
1764
|
+
try {
|
|
1765
|
+
const code = readFileCached(fileAbs);
|
|
1766
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1767
|
+
|
|
1768
|
+
const engineFindings = analyzeSecurityVulnerabilities(code, fileRel);
|
|
1769
|
+
|
|
1770
|
+
for (const finding of engineFindings) {
|
|
1771
|
+
findings.push({
|
|
1772
|
+
id: stableId("F_SECURITY", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1773
|
+
severity: finding.severity,
|
|
1774
|
+
category: finding.category,
|
|
1775
|
+
title: finding.title,
|
|
1776
|
+
message: finding.message,
|
|
1777
|
+
file: finding.file,
|
|
1778
|
+
line: finding.line,
|
|
1779
|
+
why: "Security vulnerabilities can lead to data breaches, unauthorized access, or system compromise.",
|
|
1780
|
+
confidence: finding.confidence,
|
|
1781
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1782
|
+
fixHints: [
|
|
1783
|
+
"Review security best practices for the detected vulnerability type.",
|
|
1784
|
+
"Use parameterized queries for SQL operations.",
|
|
1785
|
+
"Sanitize and validate all user inputs.",
|
|
1786
|
+
"Use Content Security Policy (CSP) headers for XSS protection.",
|
|
1787
|
+
],
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
return findings;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function findPerformanceIssues(repoRoot) {
|
|
1799
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1800
|
+
const findings = [];
|
|
1801
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1802
|
+
cwd: repoRoot,
|
|
1803
|
+
absolute: true,
|
|
1804
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
for (const fileAbs of files) {
|
|
1808
|
+
try {
|
|
1809
|
+
const code = readFileCached(fileAbs);
|
|
1810
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1811
|
+
|
|
1812
|
+
// Use unified engines
|
|
1813
|
+
const engineFindings = engines.analyzePerformanceIssues(code, fileRel);
|
|
1814
|
+
|
|
1815
|
+
for (const finding of engineFindings) {
|
|
1816
|
+
findings.push({
|
|
1817
|
+
id: stableId("F_PERF", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1818
|
+
severity: finding.severity,
|
|
1819
|
+
category: finding.category,
|
|
1820
|
+
title: finding.title,
|
|
1821
|
+
message: finding.message,
|
|
1822
|
+
file: finding.file,
|
|
1823
|
+
line: finding.line,
|
|
1824
|
+
why: "Performance issues can degrade user experience and increase server costs.",
|
|
1825
|
+
confidence: finding.confidence,
|
|
1826
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1827
|
+
fixHints: [
|
|
1828
|
+
"Optimize algorithms and data structures.",
|
|
1829
|
+
"Use pagination for large datasets.",
|
|
1830
|
+
"Remove unnecessary re-renders.",
|
|
1831
|
+
"Use async/await for I/O operations.",
|
|
1832
|
+
],
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return findings;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function findCodeQualityIssues(repoRoot) {
|
|
1844
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1845
|
+
const findings = [];
|
|
1846
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1847
|
+
cwd: repoRoot,
|
|
1848
|
+
absolute: true,
|
|
1849
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
for (const fileAbs of files) {
|
|
1853
|
+
try {
|
|
1854
|
+
const code = readFileCached(fileAbs);
|
|
1855
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1856
|
+
|
|
1857
|
+
// Use unified engines
|
|
1858
|
+
const engineFindings = engines.analyzeCodeQuality(code, fileRel);
|
|
1859
|
+
|
|
1860
|
+
for (const finding of engineFindings) {
|
|
1861
|
+
findings.push({
|
|
1862
|
+
id: stableId("F_QUALITY", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1863
|
+
severity: finding.severity,
|
|
1864
|
+
category: finding.category,
|
|
1865
|
+
title: finding.title,
|
|
1866
|
+
message: finding.message,
|
|
1867
|
+
file: finding.file,
|
|
1868
|
+
line: finding.line,
|
|
1869
|
+
why: "Code quality issues make code harder to maintain, test, and extend.",
|
|
1870
|
+
confidence: finding.confidence,
|
|
1871
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1872
|
+
fixHints: [
|
|
1873
|
+
"Break down complex functions into smaller, focused functions.",
|
|
1874
|
+
"Extract magic numbers into named constants.",
|
|
1875
|
+
"Reduce nesting depth with early returns.",
|
|
1876
|
+
"Consider design patterns for common problems.",
|
|
1877
|
+
],
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
return findings;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function findCrossFileIssues(repoRoot) {
|
|
1889
|
+
const { analyzeCrossFile } = require("./engines/cross-file-analysis-engine");
|
|
1890
|
+
const fg = require("fast-glob");
|
|
1891
|
+
|
|
1892
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1893
|
+
cwd: repoRoot,
|
|
1894
|
+
absolute: true,
|
|
1895
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
const engineFindings = analyzeCrossFile(files, repoRoot);
|
|
1899
|
+
const findings = [];
|
|
1900
|
+
|
|
1901
|
+
for (const finding of engineFindings) {
|
|
1902
|
+
findings.push({
|
|
1903
|
+
id: stableId("F_CROSSFILE", `${finding.file}:${finding.type}`),
|
|
1904
|
+
severity: finding.severity,
|
|
1905
|
+
category: finding.category,
|
|
1906
|
+
title: finding.title,
|
|
1907
|
+
message: finding.message,
|
|
1908
|
+
file: finding.file,
|
|
1909
|
+
line: finding.line,
|
|
1910
|
+
why: "Cross-file issues indicate architectural problems that can cause maintenance difficulties.",
|
|
1911
|
+
confidence: finding.confidence,
|
|
1912
|
+
evidence: [{ file: finding.file, reason: finding.title }],
|
|
1913
|
+
fixHints: [
|
|
1914
|
+
"Remove unused exports or mark them as internal.",
|
|
1915
|
+
"Refactor to break circular dependencies.",
|
|
1916
|
+
"Standardize import paths across the codebase.",
|
|
1917
|
+
],
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
return findings;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function findTypeSafetyIssues(repoRoot) {
|
|
1925
|
+
const engines = require("./engines/vibecheck-engines");
|
|
1926
|
+
const findings = [];
|
|
1927
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
1928
|
+
cwd: repoRoot,
|
|
1929
|
+
absolute: true,
|
|
1930
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
for (const fileAbs of files) {
|
|
1934
|
+
try {
|
|
1935
|
+
const code = readFileCached(fileAbs);
|
|
1936
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1937
|
+
|
|
1938
|
+
// Use unified engines
|
|
1939
|
+
const engineFindings = engines.analyzeTypeAware(code, fileRel);
|
|
1940
|
+
|
|
1941
|
+
for (const finding of engineFindings) {
|
|
1942
|
+
findings.push({
|
|
1943
|
+
id: stableId("F_TYPE", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1944
|
+
severity: finding.severity,
|
|
1945
|
+
category: finding.category,
|
|
1946
|
+
title: finding.title,
|
|
1947
|
+
message: finding.message,
|
|
1948
|
+
file: finding.file,
|
|
1949
|
+
line: finding.line,
|
|
1950
|
+
why: "Type safety issues can lead to runtime errors and make code harder to maintain.",
|
|
1951
|
+
confidence: finding.confidence,
|
|
1952
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1953
|
+
fixHints: [
|
|
1954
|
+
"Use proper TypeScript types instead of 'any'.",
|
|
1955
|
+
"Fix underlying type errors instead of suppressing them.",
|
|
1956
|
+
"Add explicit return type annotations.",
|
|
1957
|
+
],
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
return findings;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function findAccessibilityIssues(repoRoot) {
|
|
1969
|
+
const { analyzeAccessibility } = require("./engines/accessibility-engine");
|
|
1970
|
+
const findings = [];
|
|
1971
|
+
const files = fg.sync(["**/*.{tsx,jsx}"], {
|
|
1972
|
+
cwd: repoRoot,
|
|
1973
|
+
absolute: true,
|
|
1974
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
for (const fileAbs of files) {
|
|
1978
|
+
try {
|
|
1979
|
+
const code = readFileCached(fileAbs);
|
|
1980
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
1981
|
+
|
|
1982
|
+
const engineFindings = analyzeAccessibility(code, fileRel);
|
|
1983
|
+
|
|
1984
|
+
for (const finding of engineFindings) {
|
|
1985
|
+
findings.push({
|
|
1986
|
+
id: stableId("F_A11Y", `${fileRel}:${finding.type}:${finding.line}`),
|
|
1987
|
+
severity: finding.severity,
|
|
1988
|
+
category: finding.category,
|
|
1989
|
+
title: finding.title,
|
|
1990
|
+
message: finding.message,
|
|
1991
|
+
file: finding.file,
|
|
1992
|
+
line: finding.line,
|
|
1993
|
+
why: "Accessibility issues prevent users with disabilities from using your application.",
|
|
1994
|
+
confidence: finding.confidence,
|
|
1995
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
1996
|
+
fixHints: [
|
|
1997
|
+
"Add alt text to all images.",
|
|
1998
|
+
"Ensure all interactive elements have accessible labels.",
|
|
1999
|
+
"Add keyboard handlers for custom interactive elements.",
|
|
2000
|
+
"Test with screen readers.",
|
|
2001
|
+
],
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
return findings;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function findAPIConsistencyIssues(repoRoot) {
|
|
2013
|
+
const { analyzeAPIConsistency } = require("./engines/api-consistency-engine");
|
|
2014
|
+
const findings = [];
|
|
2015
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
2016
|
+
cwd: repoRoot,
|
|
2017
|
+
absolute: true,
|
|
2018
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
for (const fileAbs of files) {
|
|
2022
|
+
try {
|
|
2023
|
+
const code = readFileCached(fileAbs);
|
|
2024
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
2025
|
+
|
|
2026
|
+
const engineFindings = analyzeAPIConsistency(code, fileRel);
|
|
2027
|
+
|
|
2028
|
+
for (const finding of engineFindings) {
|
|
2029
|
+
findings.push({
|
|
2030
|
+
id: stableId("F_API", `${fileRel}:${finding.type}:${finding.line}`),
|
|
2031
|
+
severity: finding.severity,
|
|
2032
|
+
category: finding.category,
|
|
2033
|
+
title: finding.title,
|
|
2034
|
+
message: finding.message,
|
|
2035
|
+
file: finding.file,
|
|
2036
|
+
line: finding.line,
|
|
2037
|
+
why: "API consistency issues make APIs harder to use and maintain.",
|
|
2038
|
+
confidence: finding.confidence,
|
|
2039
|
+
evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
|
|
2040
|
+
fixHints: [
|
|
2041
|
+
"Standardize response formats across all API routes.",
|
|
2042
|
+
"Add consistent error handling.",
|
|
2043
|
+
"Always return explicit HTTP status codes.",
|
|
2044
|
+
],
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
return findings;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/* ============================================================================
|
|
2056
|
+
* OPTIMISTIC NO ROLLBACK DETECTOR
|
|
2057
|
+
* Finds optimistic UI updates that don't rollback on failure
|
|
2058
|
+
* ========================================================================== */
|
|
2059
|
+
|
|
2060
|
+
function findOptimisticNoRollback(repoRoot) {
|
|
2061
|
+
const findings = [];
|
|
2062
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
2063
|
+
cwd: repoRoot,
|
|
2064
|
+
absolute: true,
|
|
2065
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
for (const fileAbs of files) {
|
|
2069
|
+
const code = readFileCached(fileAbs);
|
|
2070
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
2071
|
+
|
|
2072
|
+
// Fast path: skip files without optimistic patterns
|
|
2073
|
+
const hasOptimisticUpdate = /\b(setOptimistic|optimisticUpdate|setState.*\bfetch|useMutation.*onMutate)\b/i.test(code);
|
|
2074
|
+
const hasStateUpdate = /\b(setState|set[A-Z]\w*|dispatch|update[A-Z])\b/.test(code);
|
|
2075
|
+
const hasNetworkCall = /\b(fetch|axios|useMutation|mutate)\b/.test(code);
|
|
2076
|
+
|
|
2077
|
+
if (!hasStateUpdate || !hasNetworkCall) continue;
|
|
2078
|
+
|
|
2079
|
+
let ast;
|
|
2080
|
+
try {
|
|
2081
|
+
ast = parseFile(code, fileAbs);
|
|
2082
|
+
} catch {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
try {
|
|
2087
|
+
traverse(ast, {
|
|
2088
|
+
CallExpression(pathNode) {
|
|
2089
|
+
const n = pathNode.node;
|
|
2090
|
+
|
|
2091
|
+
// Look for setState/dispatch calls followed by fetch without catch/finally rollback
|
|
2092
|
+
if (!t.isMemberExpression(n.callee)) return;
|
|
2093
|
+
|
|
2094
|
+
const methodName = n.callee.property?.name || "";
|
|
2095
|
+
const isStateUpdate = /^(setState|set[A-Z]|dispatch|update)/.test(methodName);
|
|
2096
|
+
|
|
2097
|
+
if (!isStateUpdate) return;
|
|
2098
|
+
|
|
2099
|
+
// Check if parent function has a try-catch with rollback
|
|
2100
|
+
let parentFn = pathNode.findParent(p => p.isFunction());
|
|
2101
|
+
if (!parentFn) return;
|
|
2102
|
+
|
|
2103
|
+
let hasRollback = false;
|
|
2104
|
+
let hasNetworkInSameBlock = false;
|
|
2105
|
+
|
|
2106
|
+
parentFn.traverse({
|
|
2107
|
+
CallExpression(inner) {
|
|
2108
|
+
const callee = inner.node.callee;
|
|
2109
|
+
if (isFetchCall(inner.node) || isAxiosCall(inner.node)) {
|
|
2110
|
+
hasNetworkInSameBlock = true;
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
CatchClause(catchPath) {
|
|
2114
|
+
// Check if catch has a state update (rollback)
|
|
2115
|
+
catchPath.traverse({
|
|
2116
|
+
CallExpression(rollbackCall) {
|
|
2117
|
+
if (t.isMemberExpression(rollbackCall.node.callee)) {
|
|
2118
|
+
const name = rollbackCall.node.callee.property?.name || "";
|
|
2119
|
+
if (/^(setState|set[A-Z]|dispatch|update)/.test(name)) {
|
|
2120
|
+
hasRollback = true;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
// If there's a state update, network call, but no rollback in catch
|
|
2129
|
+
if (hasNetworkInSameBlock && !hasRollback) {
|
|
2130
|
+
const loc = n.loc;
|
|
2131
|
+
findings.push({
|
|
2132
|
+
id: stableId("F_OPTIMISTIC_NO_ROLLBACK", `${fileRel}:${loc?.start?.line || 0}`),
|
|
2133
|
+
severity: "WARN",
|
|
2134
|
+
category: "OptimisticNoRollback",
|
|
2135
|
+
title: "Optimistic update without rollback on failure",
|
|
2136
|
+
why: "State is updated before network call completes, but there's no rollback if the request fails. Users see stale/incorrect data.",
|
|
2137
|
+
confidence: "med",
|
|
2138
|
+
evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Optimistic state update")].filter(Boolean),
|
|
2139
|
+
fixHints: [
|
|
2140
|
+
"Add a catch block that reverts the state to previous value on failure.",
|
|
2141
|
+
"Use react-query's onMutate/onError for automatic rollback.",
|
|
2142
|
+
"Store previous state before update and restore it on error.",
|
|
2143
|
+
],
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
} catch {
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
return findings;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/* ============================================================================
|
|
2157
|
+
* SILENT CATCH DETECTOR (Enhanced)
|
|
2158
|
+
* Finds catch blocks that swallow errors without logging or re-throwing
|
|
2159
|
+
* ========================================================================== */
|
|
2160
|
+
|
|
2161
|
+
function findSilentCatch(repoRoot) {
|
|
2162
|
+
const findings = [];
|
|
2163
|
+
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
2164
|
+
cwd: repoRoot,
|
|
2165
|
+
absolute: true,
|
|
2166
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
for (const fileAbs of files) {
|
|
2170
|
+
const code = readFileCached(fileAbs);
|
|
2171
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
2172
|
+
|
|
2173
|
+
// Fast path: skip files without try-catch
|
|
2174
|
+
if (!/\bcatch\s*\(/.test(code)) continue;
|
|
2175
|
+
|
|
2176
|
+
let ast;
|
|
2177
|
+
try {
|
|
2178
|
+
ast = parseFile(code, fileAbs);
|
|
2179
|
+
} catch {
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
try {
|
|
2184
|
+
traverse(ast, {
|
|
2185
|
+
CatchClause(pathNode) {
|
|
2186
|
+
const catchBody = pathNode.node.body;
|
|
2187
|
+
const catchParam = pathNode.node.param?.name || "error";
|
|
2188
|
+
|
|
2189
|
+
// Check if catch body is empty or only has comments
|
|
2190
|
+
if (!catchBody.body || catchBody.body.length === 0) {
|
|
2191
|
+
// Empty catch - already covered by findEmptyCatch
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// Check if catch actually handles the error
|
|
2196
|
+
let logsError = false;
|
|
2197
|
+
let rethrowsError = false;
|
|
2198
|
+
let showsUserError = false;
|
|
2199
|
+
let hasConditionalReturn = false;
|
|
2200
|
+
|
|
2201
|
+
pathNode.traverse({
|
|
2202
|
+
CallExpression(inner) {
|
|
2203
|
+
const callee = inner.node.callee;
|
|
2204
|
+
const args = inner.node.arguments;
|
|
2205
|
+
|
|
2206
|
+
// Check for console.error, console.log, logger.error, etc.
|
|
2207
|
+
if (t.isMemberExpression(callee)) {
|
|
2208
|
+
const obj = callee.object?.name || "";
|
|
2209
|
+
const prop = callee.property?.name || "";
|
|
2210
|
+
if ((obj === "console" || /logger|log/i.test(obj)) && /error|warn|log/.test(prop)) {
|
|
2211
|
+
// Check if it logs the error variable
|
|
2212
|
+
const argsStr = args.map(a => code.slice(a.start, a.end)).join(",");
|
|
2213
|
+
if (argsStr.includes(catchParam) || argsStr.includes("error") || argsStr.includes("err")) {
|
|
2214
|
+
logsError = true;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
// Check for toast.error, notification.error, etc.
|
|
2218
|
+
if (/toast|notification|alert|message/i.test(obj) && /error|warn|fail/.test(prop)) {
|
|
2219
|
+
showsUserError = true;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
},
|
|
2223
|
+
ThrowStatement() {
|
|
2224
|
+
rethrowsError = true;
|
|
2225
|
+
},
|
|
2226
|
+
ReturnStatement(ret) {
|
|
2227
|
+
// Returning early might be intentional error handling
|
|
2228
|
+
const ifParent = ret.findParent(p => p.isIfStatement());
|
|
2229
|
+
if (ifParent) hasConditionalReturn = true;
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
// Silent catch: doesn't log, doesn't rethrow, doesn't show user error
|
|
2234
|
+
if (!logsError && !rethrowsError && !showsUserError && !hasConditionalReturn) {
|
|
2235
|
+
const loc = pathNode.node.loc;
|
|
2236
|
+
findings.push({
|
|
2237
|
+
id: stableId("F_SILENT_CATCH", `${fileRel}:${loc?.start?.line || 0}`),
|
|
2238
|
+
severity: "WARN",
|
|
2239
|
+
category: "SilentCatch",
|
|
2240
|
+
title: "Catch block swallows error silently",
|
|
2241
|
+
why: "Errors are caught but not logged, reported, or shown to users. This makes debugging nearly impossible and hides failures.",
|
|
2242
|
+
confidence: "med",
|
|
2243
|
+
evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Silent catch block")].filter(Boolean),
|
|
2244
|
+
fixHints: [
|
|
2245
|
+
`Add console.error(${catchParam}) or a logger call.`,
|
|
2246
|
+
"Show user-friendly error message (toast, alert, etc.).",
|
|
2247
|
+
"Re-throw the error if it should propagate.",
|
|
2248
|
+
"If intentionally ignoring, add a comment explaining why.",
|
|
2249
|
+
],
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
} catch {
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
return findings;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/* ============================================================================
|
|
2263
|
+
* METHOD MISMATCH DETECTOR
|
|
2264
|
+
* Finds client-side GET requests to POST-only endpoints and vice versa
|
|
2265
|
+
* ========================================================================== */
|
|
2266
|
+
|
|
2267
|
+
function findMethodMismatch(truthpack) {
|
|
2268
|
+
if (!truthpack?.routes) return [];
|
|
2269
|
+
|
|
2270
|
+
const findings = [];
|
|
2271
|
+
const serverRoutes = truthpack.routes.server || [];
|
|
2272
|
+
const clientRefs = truthpack.routes.clientRefs || [];
|
|
2273
|
+
|
|
2274
|
+
// Build a map of route -> allowed methods
|
|
2275
|
+
const routeMethodMap = new Map();
|
|
2276
|
+
for (const route of serverRoutes) {
|
|
2277
|
+
const key = normalizeRoutePath(route.path);
|
|
2278
|
+
if (!routeMethodMap.has(key)) {
|
|
2279
|
+
routeMethodMap.set(key, new Set());
|
|
2280
|
+
}
|
|
2281
|
+
if (route.method) {
|
|
2282
|
+
routeMethodMap.get(key).add(route.method.toUpperCase());
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// Check client refs for method mismatches
|
|
2287
|
+
for (const ref of clientRefs) {
|
|
2288
|
+
if (!ref.method || !ref.path) continue;
|
|
2289
|
+
|
|
2290
|
+
const clientMethod = ref.method.toUpperCase();
|
|
2291
|
+
const normalizedPath = normalizeRoutePath(ref.path);
|
|
2292
|
+
|
|
2293
|
+
// Find matching server route
|
|
2294
|
+
const allowedMethods = routeMethodMap.get(normalizedPath);
|
|
2295
|
+
|
|
2296
|
+
if (allowedMethods && allowedMethods.size > 0 && !allowedMethods.has(clientMethod)) {
|
|
2297
|
+
// Method mismatch found
|
|
2298
|
+
const allowed = Array.from(allowedMethods).join(", ");
|
|
2299
|
+
findings.push({
|
|
2300
|
+
id: stableId("F_METHOD_MISMATCH", `${ref.file || "unknown"}:${ref.line || 0}:${ref.path}`),
|
|
2301
|
+
severity: "BLOCK",
|
|
2302
|
+
category: "MethodMismatch",
|
|
2303
|
+
title: `${clientMethod} request to ${allowed}-only endpoint: ${ref.path}`,
|
|
2304
|
+
why: `Client makes ${clientMethod} request but server only accepts ${allowed}. This will fail with 405 Method Not Allowed.`,
|
|
2305
|
+
confidence: "high",
|
|
2306
|
+
evidence: ref.file ? [{
|
|
2307
|
+
file: ref.file,
|
|
2308
|
+
line: ref.line,
|
|
2309
|
+
reason: `Client ${clientMethod} to ${allowed}-only route`,
|
|
2310
|
+
}] : [],
|
|
2311
|
+
fixHints: [
|
|
2312
|
+
`Change client request method from ${clientMethod} to ${allowed}.`,
|
|
2313
|
+
`Add ${clientMethod} handler to the server route.`,
|
|
2314
|
+
"Verify the API contract matches documentation.",
|
|
2315
|
+
],
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
return findings;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// Helper to normalize route paths for comparison
|
|
2324
|
+
function normalizeRoutePath(routePath) {
|
|
2325
|
+
if (!routePath) return "";
|
|
2326
|
+
return routePath
|
|
2327
|
+
.replace(/\[([^\]]+)\]/g, ":$1") // [id] -> :id
|
|
2328
|
+
.replace(/\/+/g, "/") // multiple slashes -> single
|
|
2329
|
+
.replace(/\/$/, "") // remove trailing slash
|
|
2330
|
+
.toLowerCase();
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/* ============================================================================
|
|
2334
|
+
* DEAD UI DETECTOR (Enhanced)
|
|
2335
|
+
* Finds buttons, forms, and links that do nothing
|
|
2336
|
+
* ========================================================================== */
|
|
2337
|
+
|
|
2338
|
+
function findDeadUI(repoRoot) {
|
|
2339
|
+
const findings = [];
|
|
2340
|
+
const files = fg.sync(["**/*.{tsx,jsx}"], {
|
|
2341
|
+
cwd: repoRoot,
|
|
2342
|
+
absolute: true,
|
|
2343
|
+
ignore: STANDARD_IGNORE_PATTERNS,
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
for (const fileAbs of files) {
|
|
2347
|
+
const code = readFileCached(fileAbs);
|
|
2348
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
2349
|
+
|
|
2350
|
+
// Fast path: skip files without interactive elements
|
|
2351
|
+
if (!/<(button|Button|form|Form|a|Link)\b/.test(code)) continue;
|
|
2352
|
+
|
|
2353
|
+
let ast;
|
|
2354
|
+
try {
|
|
2355
|
+
ast = parseFile(code, fileAbs);
|
|
2356
|
+
} catch {
|
|
2357
|
+
continue;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
try {
|
|
2361
|
+
traverse(ast, {
|
|
2362
|
+
JSXElement(pathNode) {
|
|
2363
|
+
const opening = pathNode.node.openingElement;
|
|
2364
|
+
const tagName = opening.name?.name || opening.name?.property?.name || "";
|
|
2365
|
+
|
|
2366
|
+
// Check for buttons, forms, links
|
|
2367
|
+
if (!/^(button|Button|form|Form|a|Link)$/i.test(tagName)) return;
|
|
2368
|
+
|
|
2369
|
+
const attrs = opening.attributes || [];
|
|
2370
|
+
let hasOnClick = false;
|
|
2371
|
+
let hasOnSubmit = false;
|
|
2372
|
+
let hasHref = false;
|
|
2373
|
+
let hasAction = false;
|
|
2374
|
+
let hasType = false;
|
|
2375
|
+
let isDisabled = false;
|
|
2376
|
+
|
|
2377
|
+
for (const attr of attrs) {
|
|
2378
|
+
if (!t.isJSXAttribute(attr)) continue;
|
|
2379
|
+
const name = attr.name?.name || "";
|
|
2380
|
+
|
|
2381
|
+
if (name === "onClick" || name === "onPress") hasOnClick = true;
|
|
2382
|
+
if (name === "onSubmit") hasOnSubmit = true;
|
|
2383
|
+
if (name === "href" || name === "to") hasHref = true;
|
|
2384
|
+
if (name === "action") hasAction = true;
|
|
2385
|
+
if (name === "type") hasType = true;
|
|
2386
|
+
if (name === "disabled") isDisabled = true;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// Button without onClick (unless it's a submit button or disabled)
|
|
2390
|
+
if (/button/i.test(tagName) && !hasOnClick && !isDisabled) {
|
|
2391
|
+
const isSubmitType = attrs.some(a =>
|
|
2392
|
+
t.isJSXAttribute(a) &&
|
|
2393
|
+
a.name?.name === "type" &&
|
|
2394
|
+
t.isStringLiteral(a.value) &&
|
|
2395
|
+
a.value.value === "submit"
|
|
2396
|
+
);
|
|
2397
|
+
|
|
2398
|
+
if (!isSubmitType) {
|
|
2399
|
+
const loc = opening.loc;
|
|
2400
|
+
findings.push({
|
|
2401
|
+
id: stableId("F_DEAD_UI", `${fileRel}:${loc?.start?.line || 0}:button`),
|
|
2402
|
+
severity: "WARN",
|
|
2403
|
+
category: "DeadUI",
|
|
2404
|
+
title: "Button without onClick handler",
|
|
2405
|
+
why: "This button does nothing when clicked. Users expect buttons to perform actions.",
|
|
2406
|
+
confidence: "med",
|
|
2407
|
+
evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Dead button")].filter(Boolean),
|
|
2408
|
+
fixHints: [
|
|
2409
|
+
"<Button onClick={() => handleAction()}>Click Me</Button>",
|
|
2410
|
+
"For submit: <Button type=\"submit\">Submit</Button>",
|
|
2411
|
+
"For disabled: <Button disabled>Coming Soon</Button>",
|
|
2412
|
+
],
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// Form without onSubmit or action
|
|
2418
|
+
if (/form/i.test(tagName) && !hasOnSubmit && !hasAction) {
|
|
2419
|
+
const loc = opening.loc;
|
|
2420
|
+
findings.push({
|
|
2421
|
+
id: stableId("F_DEAD_UI", `${fileRel}:${loc?.start?.line || 0}:form`),
|
|
2422
|
+
severity: "WARN",
|
|
2423
|
+
category: "DeadUI",
|
|
2424
|
+
title: "Form without onSubmit or action",
|
|
2425
|
+
why: "This form does nothing when submitted. Form data won't be processed.",
|
|
2426
|
+
confidence: "med",
|
|
2427
|
+
evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Dead form")].filter(Boolean),
|
|
2428
|
+
fixHints: [
|
|
2429
|
+
"Add an onSubmit handler to process form data.",
|
|
2430
|
+
"Or add an action attribute for server-side submission.",
|
|
2431
|
+
],
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Link without href
|
|
2436
|
+
if (/^(a|Link)$/i.test(tagName) && !hasHref) {
|
|
2437
|
+
const loc = opening.loc;
|
|
2438
|
+
findings.push({
|
|
2439
|
+
id: stableId("F_DEAD_UI", `${fileRel}:${loc?.start?.line || 0}:link`),
|
|
2440
|
+
severity: "WARN",
|
|
2441
|
+
category: "DeadUI",
|
|
2442
|
+
title: "Link without href or to prop",
|
|
2443
|
+
why: "This link goes nowhere. Users expect links to navigate somewhere.",
|
|
2444
|
+
confidence: "med",
|
|
2445
|
+
evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Dead link")].filter(Boolean),
|
|
2446
|
+
fixHints: [
|
|
2447
|
+
"Add href prop with the target URL.",
|
|
2448
|
+
"If using Next.js Link, ensure href is provided.",
|
|
2449
|
+
"If it's a button styled as link, use a button element instead.",
|
|
2450
|
+
],
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
} catch {
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
return findings;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
module.exports = {
|
|
2464
|
+
// V3: Cache management - call after scan completes to prevent memory leaks
|
|
2465
|
+
clearFileCache,
|
|
2466
|
+
|
|
2467
|
+
// V3: Entropy helper - exported for testing/reuse
|
|
2468
|
+
getShannonEntropy,
|
|
2469
|
+
|
|
2470
|
+
// Analyzers
|
|
2471
|
+
findMissingRoutes,
|
|
2472
|
+
findEnvGaps,
|
|
2473
|
+
findFakeSuccess,
|
|
2474
|
+
findGhostAuth,
|
|
2475
|
+
findStripeWebhookViolations,
|
|
2476
|
+
findPaidSurfaceNotEnforced,
|
|
2477
|
+
findOwnerModeBypass,
|
|
2478
|
+
findMockData,
|
|
2479
|
+
findTodoFixme,
|
|
2480
|
+
findConsoleLogs,
|
|
2481
|
+
findHardcodedSecrets,
|
|
2482
|
+
findDeadCode,
|
|
2483
|
+
findDeprecatedApis,
|
|
2484
|
+
findEmptyCatch,
|
|
2485
|
+
findUnsafeRegex,
|
|
2486
|
+
// Enhanced analyzers
|
|
2487
|
+
findSecurityVulnerabilities,
|
|
2488
|
+
findPerformanceIssues,
|
|
2489
|
+
findCodeQualityIssues,
|
|
2490
|
+
// Advanced analyzers
|
|
2491
|
+
findCrossFileIssues,
|
|
2492
|
+
findTypeSafetyIssues,
|
|
2493
|
+
findAccessibilityIssues,
|
|
2494
|
+
findAPIConsistencyIssues,
|
|
2495
|
+
// NEW: AI Hallucination Detectors
|
|
2496
|
+
findOptimisticNoRollback,
|
|
2497
|
+
findSilentCatch,
|
|
2498
|
+
findMethodMismatch,
|
|
2499
|
+
findDeadUI,
|
|
2500
|
+
};
|