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,2260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reality Mode v2 - Two-Pass Auth Verification + Dead UI Crawler + Fake Data Detection
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* ENTERPRISE EDITION - World-Class Terminal Experience
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* TIER ENFORCEMENT:
|
|
9
|
+
* - FREE: Preview mode (5 pages, 20 clicks, no auth boundary)
|
|
10
|
+
* - STARTER: Full budgets + basic auth verification
|
|
11
|
+
* - PRO: Advanced auth boundary (multi-role, 2-pass) + fake data detection
|
|
12
|
+
*
|
|
13
|
+
* Pass A (anon): crawl + click, record which routes look protected
|
|
14
|
+
* Pass B (auth): crawl same routes using storageState, verify protected routes accessible
|
|
15
|
+
*
|
|
16
|
+
* Findings:
|
|
17
|
+
* - Dead UI (clicks that do nothing)
|
|
18
|
+
* - HTTP errors (4xx/5xx)
|
|
19
|
+
* - Auth coverage (protected route reachable anonymously = BLOCK)
|
|
20
|
+
* - Fake domain detection (localhost, jsonplaceholder, ngrok, mockapi.io)
|
|
21
|
+
* - Fake response detection (demo IDs, test keys, placeholder data)
|
|
22
|
+
* - Mock status codes (418, 999, etc.)
|
|
23
|
+
* - Route coverage stats
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
const fs = require("fs");
|
|
29
|
+
const path = require("path");
|
|
30
|
+
const crypto = require("crypto");
|
|
31
|
+
const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
|
|
32
|
+
|
|
33
|
+
// Entitlements enforcement
|
|
34
|
+
const entitlements = require("./lib/entitlements-v2");
|
|
35
|
+
const upsell = require("./lib/upsell");
|
|
36
|
+
|
|
37
|
+
let chromium;
|
|
38
|
+
let playwrightError = null;
|
|
39
|
+
try {
|
|
40
|
+
chromium = require("playwright").chromium;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
chromium = null;
|
|
43
|
+
playwrightError = e.message;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
47
|
+
// ADVANCED TERMINAL - ANSI CODES & UTILITIES
|
|
48
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
49
|
+
|
|
50
|
+
const c = {
|
|
51
|
+
reset: '\x1b[0m',
|
|
52
|
+
bold: '\x1b[1m',
|
|
53
|
+
dim: '\x1b[2m',
|
|
54
|
+
italic: '\x1b[3m',
|
|
55
|
+
underline: '\x1b[4m',
|
|
56
|
+
blink: '\x1b[5m',
|
|
57
|
+
inverse: '\x1b[7m',
|
|
58
|
+
hidden: '\x1b[8m',
|
|
59
|
+
strike: '\x1b[9m',
|
|
60
|
+
// Colors
|
|
61
|
+
black: '\x1b[30m',
|
|
62
|
+
red: '\x1b[31m',
|
|
63
|
+
green: '\x1b[32m',
|
|
64
|
+
yellow: '\x1b[33m',
|
|
65
|
+
blue: '\x1b[34m',
|
|
66
|
+
magenta: '\x1b[35m',
|
|
67
|
+
cyan: '\x1b[36m',
|
|
68
|
+
white: '\x1b[37m',
|
|
69
|
+
// Bright colors
|
|
70
|
+
gray: '\x1b[90m',
|
|
71
|
+
brightRed: '\x1b[91m',
|
|
72
|
+
brightGreen: '\x1b[92m',
|
|
73
|
+
brightYellow: '\x1b[93m',
|
|
74
|
+
brightBlue: '\x1b[94m',
|
|
75
|
+
brightMagenta: '\x1b[95m',
|
|
76
|
+
brightCyan: '\x1b[96m',
|
|
77
|
+
brightWhite: '\x1b[97m',
|
|
78
|
+
// Background
|
|
79
|
+
bgBlack: '\x1b[40m',
|
|
80
|
+
bgRed: '\x1b[41m',
|
|
81
|
+
bgGreen: '\x1b[42m',
|
|
82
|
+
bgYellow: '\x1b[43m',
|
|
83
|
+
bgBlue: '\x1b[44m',
|
|
84
|
+
bgMagenta: '\x1b[45m',
|
|
85
|
+
bgCyan: '\x1b[46m',
|
|
86
|
+
bgWhite: '\x1b[47m',
|
|
87
|
+
// Cursor control
|
|
88
|
+
cursorUp: (n = 1) => `\x1b[${n}A`,
|
|
89
|
+
cursorDown: (n = 1) => `\x1b[${n}B`,
|
|
90
|
+
cursorRight: (n = 1) => `\x1b[${n}C`,
|
|
91
|
+
cursorLeft: (n = 1) => `\x1b[${n}D`,
|
|
92
|
+
clearLine: '\x1b[2K',
|
|
93
|
+
clearScreen: '\x1b[2J',
|
|
94
|
+
saveCursor: '\x1b[s',
|
|
95
|
+
restoreCursor: '\x1b[u',
|
|
96
|
+
hideCursor: '\x1b[?25l',
|
|
97
|
+
showCursor: '\x1b[?25h',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// True color support
|
|
101
|
+
const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
|
|
102
|
+
const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
|
|
103
|
+
|
|
104
|
+
// Premium color palette (orange/coral theme for "reality" - testing/verification)
|
|
105
|
+
const colors = {
|
|
106
|
+
// Gradient for banner
|
|
107
|
+
gradient1: rgb(255, 150, 100), // Light coral
|
|
108
|
+
gradient2: rgb(255, 130, 80), // Coral
|
|
109
|
+
gradient3: rgb(255, 110, 60), // Orange-coral
|
|
110
|
+
gradient4: rgb(255, 90, 50), // Orange
|
|
111
|
+
gradient5: rgb(255, 70, 40), // Deep orange
|
|
112
|
+
gradient6: rgb(255, 50, 30), // Red-orange
|
|
113
|
+
|
|
114
|
+
// Pass colors
|
|
115
|
+
anon: rgb(150, 200, 255), // Blue for anonymous
|
|
116
|
+
auth: rgb(100, 255, 180), // Green for authenticated
|
|
117
|
+
|
|
118
|
+
// Category colors
|
|
119
|
+
deadUI: rgb(255, 100, 100), // Red for dead UI
|
|
120
|
+
authCoverage: rgb(255, 180, 100), // Orange for auth issues
|
|
121
|
+
httpError: rgb(255, 150, 50), // Amber for HTTP errors
|
|
122
|
+
coverage: rgb(100, 200, 255), // Blue for coverage
|
|
123
|
+
|
|
124
|
+
// Status colors
|
|
125
|
+
success: rgb(0, 255, 150),
|
|
126
|
+
warning: rgb(255, 200, 0),
|
|
127
|
+
error: rgb(255, 80, 80),
|
|
128
|
+
info: rgb(100, 200, 255),
|
|
129
|
+
|
|
130
|
+
// UI colors
|
|
131
|
+
accent: rgb(255, 150, 100),
|
|
132
|
+
muted: rgb(140, 120, 100),
|
|
133
|
+
subtle: rgb(100, 80, 60),
|
|
134
|
+
highlight: rgb(255, 255, 255),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
// PREMIUM BANNER
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
const REALITY_BANNER = `
|
|
142
|
+
${rgb(255, 160, 120)} ██████╗ ███████╗ █████╗ ██╗ ██╗████████╗██╗ ██╗${c.reset}
|
|
143
|
+
${rgb(255, 140, 100)} ██╔══██╗██╔════╝██╔══██╗██║ ██║╚══██╔══╝╚██╗ ██╔╝${c.reset}
|
|
144
|
+
${rgb(255, 120, 80)} ██████╔╝█████╗ ███████║██║ ██║ ██║ ╚████╔╝ ${c.reset}
|
|
145
|
+
${rgb(255, 100, 60)} ██╔══██╗██╔══╝ ██╔══██║██║ ██║ ██║ ╚██╔╝ ${c.reset}
|
|
146
|
+
${rgb(255, 80, 40)} ██║ ██║███████╗██║ ██║███████╗██║ ██║ ██║ ${c.reset}
|
|
147
|
+
${rgb(255, 60, 20)} ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ${c.reset}
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const BANNER_FULL = `
|
|
151
|
+
${rgb(255, 160, 120)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
|
|
152
|
+
${rgb(255, 140, 100)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
|
|
153
|
+
${rgb(255, 120, 80)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
|
|
154
|
+
${rgb(255, 100, 60)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
|
|
155
|
+
${rgb(255, 80, 40)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
|
|
156
|
+
${rgb(255, 60, 20)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
|
|
157
|
+
|
|
158
|
+
${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
|
|
159
|
+
${c.dim} │${c.reset} ${rgb(255, 150, 100)}🎭${c.reset} ${c.bold}REALITY${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}Dead UI${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Fake Data${c.reset} ${c.dim}•${c.reset} ${rgb(100, 200, 255)}Auth Coverage${c.reset} ${c.dim}│${c.reset}
|
|
160
|
+
${c.dim} └─────────────────────────────────────────────────────────────────────┘${c.reset}
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
164
|
+
// FAKE DATA DETECTION PATTERNS (from reality-mode/reality-scanner.ts)
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
168
|
+
// FAKE DETECTION PATTERNS WITH CONFIDENCE SCORING
|
|
169
|
+
// Each pattern has a confidence level to reduce false positives
|
|
170
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
171
|
+
|
|
172
|
+
const FAKE_DOMAIN_PATTERNS = [
|
|
173
|
+
// CRITICAL: These are almost certainly fake backends (confidence: 0.95+)
|
|
174
|
+
{ pattern: /jsonplaceholder\.typicode\.com/i, name: "JSONPlaceholder mock API", confidence: 0.99, severity: 'BLOCK' },
|
|
175
|
+
{ pattern: /reqres\.in/i, name: "ReqRes mock API", confidence: 0.99, severity: 'BLOCK' },
|
|
176
|
+
{ pattern: /mockapi\.io/i, name: "MockAPI.io", confidence: 0.99, severity: 'BLOCK' },
|
|
177
|
+
{ pattern: /mocky\.io/i, name: "Mocky.io", confidence: 0.99, severity: 'BLOCK' },
|
|
178
|
+
{ pattern: /httpbin\.org/i, name: "HTTPBin testing API", confidence: 0.95, severity: 'BLOCK' },
|
|
179
|
+
{ pattern: /api\.example\.com/i, name: "Example.com API", confidence: 0.95, severity: 'BLOCK' },
|
|
180
|
+
{ pattern: /fake\.api/i, name: "Fake API pattern", confidence: 0.95, severity: 'BLOCK' },
|
|
181
|
+
{ pattern: /demo\.api/i, name: "Demo API pattern", confidence: 0.90, severity: 'BLOCK' },
|
|
182
|
+
|
|
183
|
+
// HIGH: Likely development/testing (confidence: 0.7-0.9)
|
|
184
|
+
// NOTE: These could be legitimate in dev/CI contexts
|
|
185
|
+
{ pattern: /localhost:\d+/i, name: "Localhost API", confidence: 0.75, severity: 'WARN', devContextOk: true },
|
|
186
|
+
{ pattern: /127\.0\.0\.1:\d+/i, name: "Loopback API", confidence: 0.75, severity: 'WARN', devContextOk: true },
|
|
187
|
+
{ pattern: /\.ngrok\.io/i, name: "Ngrok tunnel", confidence: 0.80, severity: 'WARN', devContextOk: true },
|
|
188
|
+
{ pattern: /\.ngrok-free\.app/i, name: "Ngrok free tunnel", confidence: 0.80, severity: 'WARN', devContextOk: true },
|
|
189
|
+
|
|
190
|
+
// MEDIUM: Could be legitimate staging (confidence: 0.5-0.7)
|
|
191
|
+
// NOTE: Many organizations have legitimate staging environments
|
|
192
|
+
{ pattern: /staging\.[^/]+\/api/i, name: "Staging API endpoint", confidence: 0.60, severity: 'WARN', stagingContextOk: true },
|
|
193
|
+
{ pattern: /\.local\//i, name: "Local domain", confidence: 0.50, severity: 'WARN', devContextOk: true },
|
|
194
|
+
{ pattern: /\.test\//i, name: "Test domain", confidence: 0.50, severity: 'WARN', devContextOk: true },
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const FAKE_RESPONSE_PATTERNS = [
|
|
198
|
+
// CRITICAL: Test API keys exposed (security issue)
|
|
199
|
+
{ pattern: /sk_test_[a-zA-Z0-9]{20,}/i, name: "Test Stripe secret key", confidence: 0.99, severity: 'BLOCK' },
|
|
200
|
+
{ pattern: /pk_test_[a-zA-Z0-9]{20,}/i, name: "Test Stripe public key", confidence: 0.95, severity: 'WARN' },
|
|
201
|
+
|
|
202
|
+
// HIGH: Clearly fake IDs/data
|
|
203
|
+
{ pattern: /inv_demo_[a-zA-Z0-9]+/i, name: "Demo invoice ID", confidence: 0.95, severity: 'BLOCK' },
|
|
204
|
+
{ pattern: /user_demo_[a-zA-Z0-9]+/i, name: "Demo user ID", confidence: 0.95, severity: 'BLOCK' },
|
|
205
|
+
{ pattern: /cus_demo_[a-zA-Z0-9]+/i, name: "Demo customer ID", confidence: 0.95, severity: 'BLOCK' },
|
|
206
|
+
{ pattern: /sub_demo_[a-zA-Z0-9]+/i, name: "Demo subscription ID", confidence: 0.95, severity: 'BLOCK' },
|
|
207
|
+
{ pattern: /"mock":\s*true/i, name: "Mock flag enabled", confidence: 0.95, severity: 'BLOCK' },
|
|
208
|
+
{ pattern: /"isDemo":\s*true/i, name: "Demo mode flag", confidence: 0.95, severity: 'BLOCK' },
|
|
209
|
+
{ pattern: /"status":\s*"simulated"/i, name: "Simulated status", confidence: 0.90, severity: 'BLOCK' },
|
|
210
|
+
|
|
211
|
+
// MEDIUM: Placeholder content (could be legitimate in docs/examples)
|
|
212
|
+
// NOTE: Need context awareness - these are fine in documentation/help pages
|
|
213
|
+
{ pattern: /lorem\s+ipsum\s+dolor/i, name: "Lorem ipsum placeholder", confidence: 0.70, severity: 'WARN', docsContextOk: true },
|
|
214
|
+
{ pattern: /john\.doe@/i, name: "John Doe placeholder email", confidence: 0.65, severity: 'WARN', docsContextOk: true },
|
|
215
|
+
{ pattern: /jane\.doe@/i, name: "Jane Doe placeholder email", confidence: 0.65, severity: 'WARN', docsContextOk: true },
|
|
216
|
+
{ pattern: /user@example\.com/i, name: "Example.com email", confidence: 0.50, severity: 'WARN', docsContextOk: true },
|
|
217
|
+
{ pattern: /placeholder\.(com|jpg|png)/i, name: "Placeholder domain/image", confidence: 0.60, severity: 'WARN', docsContextOk: true },
|
|
218
|
+
|
|
219
|
+
// LOWER: Could have many false positives
|
|
220
|
+
{ pattern: /"id":\s*"demo"/i, name: "Demo ID value", confidence: 0.70, severity: 'WARN' },
|
|
221
|
+
{ pattern: /"id":\s*"test"/i, name: "Test ID value", confidence: 0.60, severity: 'WARN' },
|
|
222
|
+
{ pattern: /"success":\s*true[^}]*"demo"/i, name: "Demo success response", confidence: 0.75, severity: 'WARN' },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// URLs that are allowed and should skip detection
|
|
226
|
+
const FAKE_DETECTION_ALLOWLIST = [
|
|
227
|
+
/\/docs?\//i, // Documentation pages
|
|
228
|
+
/\/help\//i, // Help pages
|
|
229
|
+
/\/examples?\//i, // Example pages
|
|
230
|
+
/\/demo\//i, // Demo pages (intentional)
|
|
231
|
+
/\/playground\//i, // Playground/sandbox
|
|
232
|
+
/\/api-docs?\//i, // API documentation
|
|
233
|
+
/\/swagger/i, // Swagger docs
|
|
234
|
+
/\/openapi/i, // OpenAPI docs
|
|
235
|
+
/readme/i, // README content
|
|
236
|
+
/changelog/i, // Changelog
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Classify a network request/response for fake data patterns
|
|
241
|
+
* Returns null if clean, or an object with detection details
|
|
242
|
+
*
|
|
243
|
+
* Enhanced with:
|
|
244
|
+
* - Confidence scoring to reduce false positives
|
|
245
|
+
* - Context awareness (dev, staging, docs)
|
|
246
|
+
* - Allowlist for legitimate use cases
|
|
247
|
+
*/
|
|
248
|
+
function classifyNetworkTraffic(url, responseBody, status, context = {}) {
|
|
249
|
+
// Skip static assets (images, fonts, stylesheets, scripts)
|
|
250
|
+
if (/\.(js|css|png|jpg|jpeg|svg|ico|woff|woff2|ttf|eot|gif|webp|mp4|webm|pdf)(\?|$)/i.test(url)) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check allowlist - skip detection for documentation/example URLs
|
|
255
|
+
for (const allowPattern of FAKE_DETECTION_ALLOWLIST) {
|
|
256
|
+
if (allowPattern.test(url)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const detections = [];
|
|
262
|
+
const isDev = context.isDev || process.env.NODE_ENV === 'development';
|
|
263
|
+
const isStaging = context.isStaging || /staging|stg|preprod/i.test(url);
|
|
264
|
+
const isDocsPage = context.isDocsPage || /docs?|help|example|readme/i.test(url);
|
|
265
|
+
|
|
266
|
+
// Check for fake domain patterns
|
|
267
|
+
for (const { pattern, name, confidence, severity, devContextOk, stagingContextOk } of FAKE_DOMAIN_PATTERNS) {
|
|
268
|
+
if (pattern.test(url)) {
|
|
269
|
+
// Skip if this pattern is OK in current context
|
|
270
|
+
if (devContextOk && isDev) continue;
|
|
271
|
+
if (stagingContextOk && isStaging) continue;
|
|
272
|
+
|
|
273
|
+
detections.push({
|
|
274
|
+
type: 'fake-domain',
|
|
275
|
+
severity,
|
|
276
|
+
evidence: `URL matches fake domain pattern: ${name}`,
|
|
277
|
+
url,
|
|
278
|
+
confidence,
|
|
279
|
+
pattern: pattern.source
|
|
280
|
+
});
|
|
281
|
+
break; // One domain match is enough
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check response body for fake data patterns
|
|
286
|
+
if (responseBody && typeof responseBody === 'string') {
|
|
287
|
+
// Skip very short responses (likely not meaningful data)
|
|
288
|
+
if (responseBody.length < 20) {
|
|
289
|
+
return detections.length > 0 ? detections : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const { pattern, name, confidence, severity, docsContextOk } of FAKE_RESPONSE_PATTERNS) {
|
|
293
|
+
// Skip patterns that are OK in docs context
|
|
294
|
+
if (docsContextOk && isDocsPage) continue;
|
|
295
|
+
|
|
296
|
+
if (pattern.test(responseBody)) {
|
|
297
|
+
detections.push({
|
|
298
|
+
type: 'fake-response',
|
|
299
|
+
severity,
|
|
300
|
+
evidence: `Response contains ${name}`,
|
|
301
|
+
url,
|
|
302
|
+
confidence,
|
|
303
|
+
pattern: pattern.source
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check for suspicious status codes (with lower confidence)
|
|
310
|
+
if (status === 418 || status === 999 || status === 0) {
|
|
311
|
+
detections.push({
|
|
312
|
+
type: 'mock-status',
|
|
313
|
+
severity: 'WARN',
|
|
314
|
+
evidence: `Suspicious HTTP status code: ${status}`,
|
|
315
|
+
url,
|
|
316
|
+
confidence: 0.60 // Lower confidence - could be legitimate
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Filter out low-confidence detections if we have high-confidence ones
|
|
321
|
+
const highConfidence = detections.filter(d => d.confidence >= 0.80);
|
|
322
|
+
if (highConfidence.length > 0 && detections.length > highConfidence.length) {
|
|
323
|
+
// Return only high-confidence detections to reduce noise
|
|
324
|
+
return highConfidence;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return detections.length > 0 ? detections : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
331
|
+
// ICONS & SYMBOLS
|
|
332
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
333
|
+
|
|
334
|
+
const ICONS = {
|
|
335
|
+
// Main
|
|
336
|
+
reality: '🎭',
|
|
337
|
+
browser: '🌐',
|
|
338
|
+
crawl: '🕷️',
|
|
339
|
+
|
|
340
|
+
// Status
|
|
341
|
+
check: '✓',
|
|
342
|
+
cross: '✗',
|
|
343
|
+
warning: '⚠',
|
|
344
|
+
info: 'ℹ',
|
|
345
|
+
arrow: '→',
|
|
346
|
+
bullet: '•',
|
|
347
|
+
|
|
348
|
+
// Passes
|
|
349
|
+
anon: '👤',
|
|
350
|
+
auth: '🔑',
|
|
351
|
+
pass: '✅',
|
|
352
|
+
fail: '❌',
|
|
353
|
+
|
|
354
|
+
// Categories
|
|
355
|
+
deadUI: '💀',
|
|
356
|
+
click: '👆',
|
|
357
|
+
link: '🔗',
|
|
358
|
+
http: '📡',
|
|
359
|
+
coverage: '📊',
|
|
360
|
+
shield: '🛡️',
|
|
361
|
+
|
|
362
|
+
// Actions
|
|
363
|
+
running: '▶',
|
|
364
|
+
complete: '●',
|
|
365
|
+
pending: '○',
|
|
366
|
+
skip: '◌',
|
|
367
|
+
|
|
368
|
+
// Objects
|
|
369
|
+
page: '📄',
|
|
370
|
+
screenshot: '📸',
|
|
371
|
+
clock: '⏱',
|
|
372
|
+
lightning: '⚡',
|
|
373
|
+
sparkle: '✨',
|
|
374
|
+
target: '🎯',
|
|
375
|
+
eye: '👁️',
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
379
|
+
// BOX DRAWING
|
|
380
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
381
|
+
|
|
382
|
+
const BOX = {
|
|
383
|
+
topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
|
|
384
|
+
horizontal: '─', vertical: '│',
|
|
385
|
+
teeRight: '├', teeLeft: '┤', teeDown: '┬', teeUp: '┴',
|
|
386
|
+
cross: '┼',
|
|
387
|
+
// Double line
|
|
388
|
+
dTopLeft: '╔', dTopRight: '╗', dBottomLeft: '╚', dBottomRight: '╝',
|
|
389
|
+
dHorizontal: '═', dVertical: '║',
|
|
390
|
+
// Heavy
|
|
391
|
+
hTopLeft: '┏', hTopRight: '┓', hBottomLeft: '┗', hBottomRight: '┛',
|
|
392
|
+
hHorizontal: '━', hVertical: '┃',
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
396
|
+
// SPINNER & PROGRESS
|
|
397
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
398
|
+
|
|
399
|
+
const SPINNER_DOTS = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
|
|
400
|
+
const SPINNER_CRAWL = ['🕷️ ', ' 🕷️', ' 🕷️', ' 🕷️', ' 🕷️', ' 🕷️'];
|
|
401
|
+
|
|
402
|
+
let spinnerIndex = 0;
|
|
403
|
+
let spinnerInterval = null;
|
|
404
|
+
let spinnerStartTime = null;
|
|
405
|
+
|
|
406
|
+
function formatDuration(ms) {
|
|
407
|
+
if (ms < 1000) return `${ms}ms`;
|
|
408
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
409
|
+
const mins = Math.floor(ms / 60000);
|
|
410
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
411
|
+
return `${mins}m ${secs}s`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function formatNumber(num) {
|
|
415
|
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function truncate(str, len) {
|
|
419
|
+
if (!str) return '';
|
|
420
|
+
if (str.length <= len) return str;
|
|
421
|
+
return str.slice(0, len - 3) + '...';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function padCenter(str, width) {
|
|
425
|
+
const padding = Math.max(0, width - str.length);
|
|
426
|
+
const left = Math.floor(padding / 2);
|
|
427
|
+
const right = padding - left;
|
|
428
|
+
return ' '.repeat(left) + str + ' '.repeat(right);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function progressBar(percent, width = 30, opts = {}) {
|
|
432
|
+
const filled = Math.round((percent / 100) * width);
|
|
433
|
+
const empty = width - filled;
|
|
434
|
+
|
|
435
|
+
let filledColor = opts.color || colors.accent;
|
|
436
|
+
if (!opts.color) {
|
|
437
|
+
if (percent >= 80) filledColor = colors.success;
|
|
438
|
+
else if (percent >= 50) filledColor = colors.warning;
|
|
439
|
+
else filledColor = colors.error;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const filledChar = opts.filled || '█';
|
|
443
|
+
const emptyChar = opts.empty || '░';
|
|
444
|
+
|
|
445
|
+
return `${filledColor}${filledChar.repeat(filled)}${c.dim}${emptyChar.repeat(empty)}${c.reset}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function startSpinner(message, color = colors.accent) {
|
|
449
|
+
spinnerStartTime = Date.now();
|
|
450
|
+
process.stdout.write(c.hideCursor);
|
|
451
|
+
|
|
452
|
+
spinnerInterval = setInterval(() => {
|
|
453
|
+
const elapsed = formatDuration(Date.now() - spinnerStartTime);
|
|
454
|
+
process.stdout.write(`\r${c.clearLine} ${color}${SPINNER_DOTS[spinnerIndex]}${c.reset} ${message} ${c.dim}${elapsed}${c.reset}`);
|
|
455
|
+
spinnerIndex = (spinnerIndex + 1) % SPINNER_DOTS.length;
|
|
456
|
+
}, 80);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function stopSpinner(message, success = true) {
|
|
460
|
+
if (spinnerInterval) {
|
|
461
|
+
clearInterval(spinnerInterval);
|
|
462
|
+
spinnerInterval = null;
|
|
463
|
+
}
|
|
464
|
+
const elapsed = spinnerStartTime ? formatDuration(Date.now() - spinnerStartTime) : '';
|
|
465
|
+
const icon = success ? `${colors.success}${ICONS.check}${c.reset}` : `${colors.error}${ICONS.cross}${c.reset}`;
|
|
466
|
+
process.stdout.write(`\r${c.clearLine} ${icon} ${message} ${c.dim}${elapsed}${c.reset}\n`);
|
|
467
|
+
process.stdout.write(c.showCursor);
|
|
468
|
+
spinnerStartTime = null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateSpinnerMessage(message) {
|
|
472
|
+
// Update message while spinner keeps running
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
476
|
+
// SECTION HEADERS
|
|
477
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
478
|
+
|
|
479
|
+
function printBanner() {
|
|
480
|
+
console.log(BANNER_FULL);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function printCompactBanner() {
|
|
484
|
+
console.log(REALITY_BANNER);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function printDivider(char = '─', width = 69, color = c.dim) {
|
|
488
|
+
console.log(`${color} ${char.repeat(width)}${c.reset}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function printSection(title, icon = '◆') {
|
|
492
|
+
console.log();
|
|
493
|
+
console.log(` ${colors.accent}${icon}${c.reset} ${c.bold}${title}${c.reset}`);
|
|
494
|
+
printDivider();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
498
|
+
// PASS DISPLAY - Two-Pass Visualization
|
|
499
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
500
|
+
|
|
501
|
+
function printPassHeader(passType, url = null) {
|
|
502
|
+
const isAnon = passType === 'anon' || passType === 'ANON';
|
|
503
|
+
const config = isAnon
|
|
504
|
+
? { icon: ICONS.anon, name: 'PASS A: ANONYMOUS', color: colors.anon, desc: 'Crawling without authentication' }
|
|
505
|
+
: { icon: ICONS.auth, name: 'PASS B: AUTHENTICATED', color: colors.auth, desc: 'Crawling with session state' };
|
|
506
|
+
|
|
507
|
+
console.log();
|
|
508
|
+
console.log(` ${config.color}${BOX.hTopLeft}${BOX.hHorizontal.repeat(3)}${c.reset} ${config.icon} ${c.bold}${config.name}${c.reset}`);
|
|
509
|
+
console.log(` ${config.color}${BOX.hVertical}${c.reset} ${c.dim}${config.desc}${c.reset}`);
|
|
510
|
+
if (url) {
|
|
511
|
+
console.log(` ${config.color}${BOX.hVertical}${c.reset} ${colors.accent}${url}${c.reset}`);
|
|
512
|
+
}
|
|
513
|
+
console.log(` ${config.color}${BOX.hBottomLeft}${BOX.hHorizontal.repeat(60)}${c.reset}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function printPassResult(passType, result) {
|
|
517
|
+
const isAnon = passType === 'anon' || passType === 'ANON';
|
|
518
|
+
const config = isAnon
|
|
519
|
+
? { icon: ICONS.anon, color: colors.anon }
|
|
520
|
+
: { icon: ICONS.auth, color: colors.auth };
|
|
521
|
+
|
|
522
|
+
const pages = result.pagesVisited?.length || 0;
|
|
523
|
+
const findings = result.findings?.length || 0;
|
|
524
|
+
const blocks = result.findings?.filter(f => f.severity === 'BLOCK').length || 0;
|
|
525
|
+
const warns = result.findings?.filter(f => f.severity === 'WARN').length || 0;
|
|
526
|
+
|
|
527
|
+
console.log();
|
|
528
|
+
console.log(` ${config.color}${config.icon}${c.reset} ${c.bold}${isAnon ? 'Anonymous' : 'Authenticated'} Pass Complete${c.reset}`);
|
|
529
|
+
console.log(` ${c.dim}Pages visited:${c.reset} ${colors.info}${pages}${c.reset}`);
|
|
530
|
+
console.log(` ${c.dim}Findings:${c.reset} ${findings} ${c.dim}(${c.reset}${blocks > 0 ? colors.error : colors.success}${blocks} blockers${c.reset}${c.dim},${c.reset} ${warns > 0 ? colors.warning : colors.success}${warns} warnings${c.reset}${c.dim})${c.reset}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
534
|
+
// COVERAGE DISPLAY
|
|
535
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
536
|
+
|
|
537
|
+
function printCoverageCard(coverage, anonPages, authPages, maxPages) {
|
|
538
|
+
if (!coverage) return;
|
|
539
|
+
|
|
540
|
+
printSection('COVERAGE', ICONS.coverage);
|
|
541
|
+
console.log();
|
|
542
|
+
|
|
543
|
+
// UI Path Coverage
|
|
544
|
+
const pct = coverage.percent || 0;
|
|
545
|
+
const pctColor = pct >= 80 ? colors.success : pct >= 50 ? colors.warning : colors.error;
|
|
546
|
+
|
|
547
|
+
console.log(` ${c.bold}UI Path Coverage${c.reset}`);
|
|
548
|
+
console.log(` ${progressBar(pct, 40)} ${pctColor}${c.bold}${pct}%${c.reset}`);
|
|
549
|
+
console.log(` ${c.dim}${coverage.hit}/${coverage.total} paths visited${c.reset}`);
|
|
550
|
+
|
|
551
|
+
// Pages visited breakdown
|
|
552
|
+
console.log();
|
|
553
|
+
console.log(` ${c.bold}Pages Crawled${c.reset}`);
|
|
554
|
+
|
|
555
|
+
const anonPct = Math.round((anonPages / maxPages) * 100);
|
|
556
|
+
console.log(` ${ICONS.anon} ${c.dim}Anonymous:${c.reset} ${progressBar(anonPct, 25, { color: colors.anon })} ${anonPages}/${maxPages}`);
|
|
557
|
+
|
|
558
|
+
if (authPages !== null && authPages !== undefined) {
|
|
559
|
+
const authPct = Math.round((authPages / maxPages) * 100);
|
|
560
|
+
console.log(` ${ICONS.auth} ${c.dim}Authenticated:${c.reset} ${progressBar(authPct, 25, { color: colors.auth })} ${authPages}/${maxPages}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Missed paths
|
|
564
|
+
if (coverage.missed && coverage.missed.length > 0) {
|
|
565
|
+
console.log();
|
|
566
|
+
console.log(` ${c.dim}Missed paths (${coverage.missed.length}):${c.reset}`);
|
|
567
|
+
for (const missed of coverage.missed.slice(0, 5)) {
|
|
568
|
+
console.log(` ${colors.warning}${ICONS.warning}${c.reset} ${c.dim}${truncate(missed, 50)}${c.reset}`);
|
|
569
|
+
}
|
|
570
|
+
if (coverage.missed.length > 5) {
|
|
571
|
+
console.log(` ${c.dim}... and ${coverage.missed.length - 5} more${c.reset}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
577
|
+
// FINDINGS DISPLAY
|
|
578
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
579
|
+
|
|
580
|
+
function getSeverityStyle(severity) {
|
|
581
|
+
const styles = {
|
|
582
|
+
BLOCK: { color: colors.error, bg: bgRgb(80, 20, 20), icon: '●', label: 'BLOCKER' },
|
|
583
|
+
WARN: { color: colors.warning, bg: bgRgb(80, 60, 0), icon: '◐', label: 'WARNING' },
|
|
584
|
+
INFO: { color: colors.info, bg: bgRgb(20, 40, 60), icon: '○', label: 'INFO' },
|
|
585
|
+
};
|
|
586
|
+
return styles[severity] || styles.INFO;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function getCategoryIcon(category) {
|
|
590
|
+
const icons = {
|
|
591
|
+
'DeadUI': ICONS.deadUI,
|
|
592
|
+
'AuthCoverage': ICONS.shield,
|
|
593
|
+
'HTTPError': ICONS.http,
|
|
594
|
+
// Fake data detection categories
|
|
595
|
+
'FakeDomain': '🔗',
|
|
596
|
+
'FakeResponse': '🎭',
|
|
597
|
+
'MockStatus': '📡',
|
|
598
|
+
};
|
|
599
|
+
return icons[category] || ICONS.bullet;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function getCategoryColor(category) {
|
|
603
|
+
const categoryColors = {
|
|
604
|
+
'DeadUI': colors.deadUI,
|
|
605
|
+
'AuthCoverage': colors.authCoverage,
|
|
606
|
+
'HTTPError': colors.httpError,
|
|
607
|
+
// Fake data detection categories - all critical (red/orange)
|
|
608
|
+
'FakeDomain': rgb(255, 80, 80), // Red - critical
|
|
609
|
+
'FakeResponse': rgb(255, 100, 60), // Orange-red
|
|
610
|
+
'MockStatus': rgb(255, 150, 50), // Amber
|
|
611
|
+
};
|
|
612
|
+
return categoryColors[category] || colors.accent;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function printFindingsBreakdown(findings) {
|
|
616
|
+
if (!findings || findings.length === 0) {
|
|
617
|
+
printSection('FINDINGS', ICONS.check);
|
|
618
|
+
console.log();
|
|
619
|
+
console.log(` ${colors.success}${c.bold}${ICONS.sparkle} No issues found! UI is responsive.${c.reset}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Group by category
|
|
624
|
+
const byCategory = {};
|
|
625
|
+
for (const f of findings) {
|
|
626
|
+
const cat = f.category || 'Other';
|
|
627
|
+
if (!byCategory[cat]) byCategory[cat] = [];
|
|
628
|
+
byCategory[cat].push(f);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const blocks = findings.filter(f => f.severity === 'BLOCK');
|
|
632
|
+
const warns = findings.filter(f => f.severity === 'WARN');
|
|
633
|
+
|
|
634
|
+
printSection(`FINDINGS (${blocks.length} blockers, ${warns.length} warnings)`, ICONS.target);
|
|
635
|
+
console.log();
|
|
636
|
+
|
|
637
|
+
// Summary by category
|
|
638
|
+
for (const [category, catFindings] of Object.entries(byCategory)) {
|
|
639
|
+
const catBlocks = catFindings.filter(f => f.severity === 'BLOCK').length;
|
|
640
|
+
const catWarns = catFindings.filter(f => f.severity === 'WARN').length;
|
|
641
|
+
const icon = getCategoryIcon(category);
|
|
642
|
+
const catColor = getCategoryColor(category);
|
|
643
|
+
|
|
644
|
+
const statusIcon = catBlocks > 0 ? ICONS.cross : catWarns > 0 ? ICONS.warning : ICONS.check;
|
|
645
|
+
const statusColor = catBlocks > 0 ? colors.error : catWarns > 0 ? colors.warning : colors.success;
|
|
646
|
+
|
|
647
|
+
console.log(` ${statusColor}${statusIcon}${c.reset} ${icon} ${c.bold}${category.padEnd(18)}${c.reset} ${catBlocks > 0 ? `${colors.error}${catBlocks} blockers${c.reset} ` : ''}${catWarns > 0 ? `${colors.warning}${catWarns} warnings${c.reset}` : ''}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function printBlockerDetails(findings, maxShow = 6) {
|
|
652
|
+
const blockers = findings.filter(f => f.severity === 'BLOCK');
|
|
653
|
+
|
|
654
|
+
if (blockers.length === 0) return;
|
|
655
|
+
|
|
656
|
+
printSection(`BLOCKERS (${blockers.length})`, '🚨');
|
|
657
|
+
console.log();
|
|
658
|
+
|
|
659
|
+
for (const blocker of blockers.slice(0, maxShow)) {
|
|
660
|
+
const style = getSeverityStyle(blocker.severity);
|
|
661
|
+
const icon = getCategoryIcon(blocker.category);
|
|
662
|
+
|
|
663
|
+
// Severity badge
|
|
664
|
+
console.log(` ${style.bg}${c.bold} ${style.label} ${c.reset} ${icon} ${c.bold}${truncate(blocker.title, 45)}${c.reset}`);
|
|
665
|
+
|
|
666
|
+
// Page URL
|
|
667
|
+
if (blocker.page) {
|
|
668
|
+
console.log(` ${' '.repeat(10)} ${colors.info}${ICONS.page} ${truncate(blocker.page, 50)}${c.reset}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Reason
|
|
672
|
+
if (blocker.reason) {
|
|
673
|
+
console.log(` ${' '.repeat(10)} ${c.dim}${truncate(blocker.reason, 50)}${c.reset}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Screenshot
|
|
677
|
+
if (blocker.screenshot) {
|
|
678
|
+
console.log(` ${' '.repeat(10)} ${colors.accent}${ICONS.screenshot} ${truncate(blocker.screenshot, 45)}${c.reset}`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
console.log();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (blockers.length > maxShow) {
|
|
685
|
+
console.log(` ${c.dim}... and ${blockers.length - maxShow} more blockers (see full report)${c.reset}`);
|
|
686
|
+
console.log();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
691
|
+
// VERDICT DISPLAY
|
|
692
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
693
|
+
|
|
694
|
+
function getVerdictConfig(blocks, warns) {
|
|
695
|
+
if (blocks === 0 && warns === 0) {
|
|
696
|
+
return {
|
|
697
|
+
verdict: 'CLEAN',
|
|
698
|
+
icon: '✅',
|
|
699
|
+
headline: 'REALITY VERIFIED',
|
|
700
|
+
tagline: 'All UI elements are responsive and functional!',
|
|
701
|
+
color: colors.success,
|
|
702
|
+
bgColor: bgRgb(0, 80, 50),
|
|
703
|
+
borderColor: rgb(0, 200, 120),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (blocks === 0) {
|
|
708
|
+
return {
|
|
709
|
+
verdict: 'WARN',
|
|
710
|
+
icon: '⚠️',
|
|
711
|
+
headline: 'MINOR ISSUES',
|
|
712
|
+
tagline: `${warns} warning${warns !== 1 ? 's' : ''} found - review recommended`,
|
|
713
|
+
color: colors.warning,
|
|
714
|
+
bgColor: bgRgb(80, 60, 0),
|
|
715
|
+
borderColor: rgb(200, 160, 0),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
verdict: 'BLOCK',
|
|
721
|
+
icon: '🛑',
|
|
722
|
+
headline: 'DEAD UI DETECTED',
|
|
723
|
+
tagline: `${blocks} blocker${blocks !== 1 ? 's' : ''} must be fixed`,
|
|
724
|
+
color: colors.error,
|
|
725
|
+
bgColor: bgRgb(80, 20, 20),
|
|
726
|
+
borderColor: rgb(200, 60, 60),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function printVerdictCard(blocks, warns, duration) {
|
|
731
|
+
const config = getVerdictConfig(blocks, warns);
|
|
732
|
+
const w = 68;
|
|
733
|
+
|
|
734
|
+
console.log();
|
|
735
|
+
console.log();
|
|
736
|
+
|
|
737
|
+
// Top border
|
|
738
|
+
console.log(` ${config.borderColor}${BOX.dTopLeft}${BOX.dHorizontal.repeat(w)}${BOX.dTopRight}${c.reset}`);
|
|
739
|
+
|
|
740
|
+
// Empty line
|
|
741
|
+
console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
|
|
742
|
+
|
|
743
|
+
// Icon and headline
|
|
744
|
+
const headlineText = `${config.icon} ${config.headline}`;
|
|
745
|
+
const headlinePadded = padCenter(headlineText, w);
|
|
746
|
+
console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${config.color}${c.bold}${headlinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
|
|
747
|
+
|
|
748
|
+
// Tagline
|
|
749
|
+
const taglinePadded = padCenter(config.tagline, w);
|
|
750
|
+
console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${c.dim}${taglinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
|
|
751
|
+
|
|
752
|
+
// Empty line
|
|
753
|
+
console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
|
|
754
|
+
|
|
755
|
+
// Stats row
|
|
756
|
+
const stats = `Blockers: ${blocks} • Warnings: ${warns} • Duration: ${formatDuration(duration)}`;
|
|
757
|
+
const statsPadded = padCenter(stats, w);
|
|
758
|
+
console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${c.dim}${statsPadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
|
|
759
|
+
|
|
760
|
+
// Empty line
|
|
761
|
+
console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
|
|
762
|
+
|
|
763
|
+
// Bottom border
|
|
764
|
+
console.log(` ${config.borderColor}${BOX.dBottomLeft}${BOX.dHorizontal.repeat(w)}${BOX.dBottomRight}${c.reset}`);
|
|
765
|
+
|
|
766
|
+
console.log();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
770
|
+
// TIER WARNING DISPLAY
|
|
771
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
772
|
+
|
|
773
|
+
function printTierWarning(tier, limits, originalMaxPages, appliedMaxPages, verifyAuthRequested, verifyAuthApplied) {
|
|
774
|
+
if (tier !== 'free') return;
|
|
775
|
+
|
|
776
|
+
console.log();
|
|
777
|
+
console.log(` ${colors.warning}${ICONS.warning}${c.reset} ${c.bold}FREE TIER: Preview Mode${c.reset}`);
|
|
778
|
+
|
|
779
|
+
if (originalMaxPages > appliedMaxPages) {
|
|
780
|
+
console.log(` ${c.dim}Pages capped:${c.reset} ${appliedMaxPages} ${c.dim}(requested ${originalMaxPages})${c.reset}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (verifyAuthRequested && !verifyAuthApplied) {
|
|
784
|
+
console.log(` ${c.dim}Auth boundary:${c.reset} ${colors.error}disabled${c.reset} ${c.dim}(requires STARTER+)${c.reset}`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
console.log(` ${colors.accent}Upgrade:${c.reset} ${c.dim}https://vibecheckai.dev/pricing${c.reset}`);
|
|
788
|
+
console.log();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
792
|
+
// HELP DISPLAY
|
|
793
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
794
|
+
|
|
795
|
+
function printHelp(opts = {}) {
|
|
796
|
+
if (shouldShowBanner(opts)) {
|
|
797
|
+
console.log(BANNER_FULL);
|
|
798
|
+
}
|
|
799
|
+
console.log(`
|
|
800
|
+
${c.bold}Usage:${c.reset} vibecheck reality --url <url> [options]
|
|
801
|
+
|
|
802
|
+
${c.bold}Runtime UI Verification${c.reset} — Prove your UI actually works.
|
|
803
|
+
|
|
804
|
+
${c.bold}Two-Pass Architecture:${c.reset}
|
|
805
|
+
${colors.anon}${ICONS.anon} Pass A (Anon)${c.reset} Crawl without auth, record protected routes
|
|
806
|
+
${colors.auth}${ICONS.auth} Pass B (Auth)${c.reset} Re-crawl with session, verify access
|
|
807
|
+
|
|
808
|
+
${c.bold}What It Detects:${c.reset}
|
|
809
|
+
${colors.deadUI}${ICONS.deadUI} Dead UI${c.reset} Clicks that do nothing
|
|
810
|
+
${colors.authCoverage}${ICONS.shield} Auth Gaps${c.reset} Protected routes accessible anonymously
|
|
811
|
+
${colors.httpError}${ICONS.http} HTTP Errors${c.reset} 4xx/5xx responses
|
|
812
|
+
|
|
813
|
+
${c.bold}Options:${c.reset}
|
|
814
|
+
${colors.accent}--url, -u <url>${c.reset} Base URL for testing ${c.dim}(required)${c.reset}
|
|
815
|
+
${colors.accent}--auth <email:pass>${c.reset} Login credentials for auth verification
|
|
816
|
+
${colors.accent}--storage-state <path>${c.reset} Playwright session state file
|
|
817
|
+
${colors.accent}--save-storage-state <p>${c.reset} Save session after login
|
|
818
|
+
${colors.accent}--truthpack <path>${c.reset} Custom truthpack path
|
|
819
|
+
${colors.accent}--verify-auth${c.reset} Enable two-pass auth verification
|
|
820
|
+
${colors.accent}--headed${c.reset} Run browser visible ${c.dim}(for debugging)${c.reset}
|
|
821
|
+
${colors.accent}--danger${c.reset} Allow clicking destructive elements
|
|
822
|
+
${colors.accent}--max-pages <n>${c.reset} Max pages to crawl ${c.dim}(default: 18)${c.reset}
|
|
823
|
+
${colors.accent}--max-depth <n>${c.reset} Max crawl depth ${c.dim}(default: 2)${c.reset}
|
|
824
|
+
${colors.accent}--timeout <ms>${c.reset} Page timeout ${c.dim}(default: 15000)${c.reset}
|
|
825
|
+
${colors.accent}--help, -h${c.reset} Show this help
|
|
826
|
+
|
|
827
|
+
${c.bold}Visual Artifacts:${c.reset}
|
|
828
|
+
${colors.accent}--video, --record-video${c.reset} Record video of browser sessions
|
|
829
|
+
${colors.accent}--trace, --record-trace${c.reset} Record Playwright trace (viewable in trace.playwright.dev)
|
|
830
|
+
${colors.accent}--har, --record-har${c.reset} Record HAR network traffic
|
|
831
|
+
|
|
832
|
+
${c.bold}Flakiness Reduction:${c.reset}
|
|
833
|
+
${colors.accent}--retries <n>${c.reset} Retry failed nav/clicks ${c.dim}(default: 2)${c.reset}
|
|
834
|
+
${colors.accent}--stable-wait <ms>${c.reset} Wait after actions ${c.dim}(default: 500ms)${c.reset}
|
|
835
|
+
${colors.accent}--stability-runs <n>${c.reset} Run N times for stability check ${c.dim}(default: 1)${c.reset}
|
|
836
|
+
${colors.accent}--flaky-threshold <f>${c.reset} Min occurrence rate to report ${c.dim}(default: 0.66)${c.reset}
|
|
837
|
+
|
|
838
|
+
${c.bold}Tier Limits:${c.reset}
|
|
839
|
+
${c.dim}FREE${c.reset} 5 pages, no auth boundary
|
|
840
|
+
${c.dim}STARTER${c.reset} Full budgets + basic auth
|
|
841
|
+
${c.dim}PRO${c.reset} Advanced auth (multi-role)
|
|
842
|
+
|
|
843
|
+
${c.bold}Exit Codes:${c.reset}
|
|
844
|
+
${colors.success}0${c.reset} CLEAN — No issues found
|
|
845
|
+
${colors.warning}1${c.reset} WARN — Warnings found
|
|
846
|
+
${colors.error}2${c.reset} BLOCK — Blockers found (dead UI, auth gaps)
|
|
847
|
+
|
|
848
|
+
${c.bold}Examples:${c.reset}
|
|
849
|
+
${c.dim}# Basic crawl${c.reset}
|
|
850
|
+
vibecheck reality --url http://localhost:3000
|
|
851
|
+
|
|
852
|
+
${c.dim}# With auth verification${c.reset}
|
|
853
|
+
vibecheck reality --url http://localhost:3000 --verify-auth --auth user@test.com:pass
|
|
854
|
+
|
|
855
|
+
${c.dim}# Debug mode (visible browser)${c.reset}
|
|
856
|
+
vibecheck reality --url http://localhost:3000 --headed
|
|
857
|
+
|
|
858
|
+
${c.dim}# Allow destructive actions${c.reset}
|
|
859
|
+
vibecheck reality --url http://localhost:3000 --danger
|
|
860
|
+
`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
864
|
+
// UTILITY FUNCTIONS (preserved from original)
|
|
865
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
866
|
+
|
|
867
|
+
function ensureDir(p) {
|
|
868
|
+
fs.mkdirSync(p, { recursive: true });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function stamp() {
|
|
872
|
+
const d = new Date();
|
|
873
|
+
const z = (n) => String(n).padStart(2, "0");
|
|
874
|
+
return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}_${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function sha1(s) {
|
|
878
|
+
return crypto.createHash("sha1").update(String(s)).digest("hex");
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function normalizeUrl(u) {
|
|
882
|
+
try {
|
|
883
|
+
const url = new URL(u);
|
|
884
|
+
url.hash = "";
|
|
885
|
+
return url.toString();
|
|
886
|
+
} catch {
|
|
887
|
+
return u;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function sameOrigin(a, b) {
|
|
892
|
+
try {
|
|
893
|
+
return new URL(a).origin === new URL(b).origin;
|
|
894
|
+
} catch {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function pathFromUrl(u) {
|
|
900
|
+
try {
|
|
901
|
+
return new URL(u).pathname || "/";
|
|
902
|
+
} catch {
|
|
903
|
+
return "/";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function looksRisky(text) {
|
|
908
|
+
const t = String(text || "").toLowerCase();
|
|
909
|
+
return /\b(delete|remove|destroy|wipe|purge|drop|cancel\s+plan|unsubscribe|terminate)\b/.test(t);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function looksAuthAction(text) {
|
|
913
|
+
const t = String(text || "").toLowerCase();
|
|
914
|
+
return /\b(logout|sign\s*out)\b/.test(t);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function loadTruthpack(repoRoot, truthpackRel) {
|
|
918
|
+
const rel = truthpackRel || path.join(".vibecheck", "truth", "truthpack.json");
|
|
919
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(repoRoot, rel);
|
|
920
|
+
if (!fs.existsSync(abs)) return null;
|
|
921
|
+
try {
|
|
922
|
+
return JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function compileNextMatcher(pattern) {
|
|
929
|
+
const p = String(pattern || "").trim();
|
|
930
|
+
if (!p) return null;
|
|
931
|
+
const norm = p.startsWith("/") ? p : `/${p}`;
|
|
932
|
+
const esc = norm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
933
|
+
if (norm.includes(":path*")) {
|
|
934
|
+
const base = esc.replace(/\\:path\\\*/g, "");
|
|
935
|
+
return new RegExp(`^${base}(\\/.*)?$`, "i");
|
|
936
|
+
}
|
|
937
|
+
return new RegExp(`^${esc}$`, "i");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function getProtectedMatchersFromTruthpack(truthpack) {
|
|
941
|
+
const patterns = truthpack?.auth?.nextMatcherPatterns || [];
|
|
942
|
+
const out = [];
|
|
943
|
+
for (const p of patterns) {
|
|
944
|
+
const rx = compileNextMatcher(p);
|
|
945
|
+
if (rx) out.push({ pattern: p, rx });
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function isLoginPage(page) {
|
|
951
|
+
try {
|
|
952
|
+
return await page.evaluate(() => {
|
|
953
|
+
const hasPass = !!document.querySelector("input[type='password']");
|
|
954
|
+
const txt = (document.body?.innerText || "").toLowerCase();
|
|
955
|
+
const signInWords = /(sign in|log in|login|welcome back|forgot password)/.test(txt);
|
|
956
|
+
const urlLooks = /(login|signin|auth)/.test(location.pathname.toLowerCase());
|
|
957
|
+
return hasPass || signInWords || urlLooks;
|
|
958
|
+
});
|
|
959
|
+
} catch {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function pageSignature(page) {
|
|
965
|
+
return page.evaluate(() => {
|
|
966
|
+
const c = document.querySelectorAll("a,button,input,select,textarea,[role='button']").length;
|
|
967
|
+
const t = document.body?.innerText?.length || 0;
|
|
968
|
+
return `${location.href}|${c}|${t}`;
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function clickOutcome(page, locator, opts = {}) {
|
|
973
|
+
const beforeSig = await pageSignature(page);
|
|
974
|
+
const beforeUrl = page.url();
|
|
975
|
+
const beforeReq = opts.reqCounter.value;
|
|
976
|
+
|
|
977
|
+
// Enhanced mutation observer that detects more changes including:
|
|
978
|
+
// - DOM structure changes (childList, subtree)
|
|
979
|
+
// - Attribute changes (class, style, aria-*, data-*)
|
|
980
|
+
// - CSS visibility/display changes
|
|
981
|
+
const domPromise = page.evaluate(() => {
|
|
982
|
+
return new Promise((resolve) => {
|
|
983
|
+
let changeCount = 0;
|
|
984
|
+
let attributeChanges = [];
|
|
985
|
+
|
|
986
|
+
const obs = new MutationObserver((mutations) => {
|
|
987
|
+
for (const mutation of mutations) {
|
|
988
|
+
changeCount++;
|
|
989
|
+
if (mutation.type === 'attributes') {
|
|
990
|
+
attributeChanges.push({
|
|
991
|
+
attr: mutation.attributeName,
|
|
992
|
+
target: mutation.target.tagName
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
obs.observe(document.documentElement, {
|
|
999
|
+
childList: true,
|
|
1000
|
+
subtree: true,
|
|
1001
|
+
attributes: true,
|
|
1002
|
+
attributeFilter: ['class', 'style', 'aria-expanded', 'aria-hidden', 'aria-selected',
|
|
1003
|
+
'data-state', 'hidden', 'open', 'data-open', 'data-closed']
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
setTimeout(() => {
|
|
1007
|
+
try { obs.disconnect(); } catch {}
|
|
1008
|
+
resolve({
|
|
1009
|
+
changed: changeCount > 0,
|
|
1010
|
+
changeCount,
|
|
1011
|
+
attributeChanges: attributeChanges.slice(0, 10) // Limit for performance
|
|
1012
|
+
});
|
|
1013
|
+
}, 900);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// Also track CSS visibility changes via computed styles
|
|
1018
|
+
const beforeVisibility = await page.evaluate(() => {
|
|
1019
|
+
const modals = document.querySelectorAll('[role="dialog"], .modal, .dropdown, .popover, [data-state]');
|
|
1020
|
+
return Array.from(modals).slice(0, 20).map(el => ({
|
|
1021
|
+
visible: getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden',
|
|
1022
|
+
state: el.getAttribute('data-state')
|
|
1023
|
+
}));
|
|
1024
|
+
}).catch(() => []);
|
|
1025
|
+
|
|
1026
|
+
const navPromise = page.waitForNavigation({ timeout: 1200 }).then(() => true).catch(() => false);
|
|
1027
|
+
const clickRes = await locator.click({ timeout: 1200 }).then(() => ({ ok: true })).catch((e) => ({ ok: false, error: String(e?.message || e) }));
|
|
1028
|
+
|
|
1029
|
+
const navRes = await navPromise;
|
|
1030
|
+
const domRes = await domPromise;
|
|
1031
|
+
await page.waitForTimeout(300); // Slightly longer wait for CSS transitions
|
|
1032
|
+
|
|
1033
|
+
const afterSig = await pageSignature(page);
|
|
1034
|
+
const afterUrl = page.url();
|
|
1035
|
+
|
|
1036
|
+
// Check CSS visibility changes
|
|
1037
|
+
const afterVisibility = await page.evaluate(() => {
|
|
1038
|
+
const modals = document.querySelectorAll('[role="dialog"], .modal, .dropdown, .popover, [data-state]');
|
|
1039
|
+
return Array.from(modals).slice(0, 20).map(el => ({
|
|
1040
|
+
visible: getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden',
|
|
1041
|
+
state: el.getAttribute('data-state')
|
|
1042
|
+
}));
|
|
1043
|
+
}).catch(() => []);
|
|
1044
|
+
|
|
1045
|
+
// Detect visibility state changes (modal open/close, dropdown toggle, etc.)
|
|
1046
|
+
const visibilityChanged = JSON.stringify(beforeVisibility) !== JSON.stringify(afterVisibility);
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
clickOk: clickRes.ok,
|
|
1050
|
+
clickError: clickRes.error || null,
|
|
1051
|
+
navHappened: !!navRes,
|
|
1052
|
+
urlChanged: normalizeUrl(afterUrl) !== normalizeUrl(beforeUrl),
|
|
1053
|
+
domChanged: !!domRes?.changed || afterSig !== beforeSig,
|
|
1054
|
+
visibilityChanged, // NEW: Tracks CSS visibility/state changes
|
|
1055
|
+
changeCount: domRes?.changeCount || 0, // NEW: Number of mutations detected
|
|
1056
|
+
reqDelta: Math.max(0, opts.reqCounter.value - beforeReq),
|
|
1057
|
+
beforeUrl,
|
|
1058
|
+
afterUrl
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function collectLinks(page, baseUrl) {
|
|
1063
|
+
const links = await page.evaluate(() => Array.from(document.querySelectorAll("a[href]")).map(a => a.getAttribute("href")).filter(Boolean));
|
|
1064
|
+
return links.map(href => { try { return new URL(href, baseUrl).toString(); } catch { return null; } }).filter(Boolean);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function collectInteractives(page) {
|
|
1068
|
+
return page.evaluate(() => {
|
|
1069
|
+
const nodes = Array.from(document.querySelectorAll("button, a[href], input[type='submit'], [role='button'], [onclick]"));
|
|
1070
|
+
return nodes.slice(0, 80).map((el, idx) => {
|
|
1071
|
+
const text = (el.getAttribute("aria-label") || el.innerText || "").trim().slice(0, 80).toLowerCase();
|
|
1072
|
+
const classList = Array.from(el.classList).join(' ').toLowerCase();
|
|
1073
|
+
const id = (el.id || "").toLowerCase();
|
|
1074
|
+
const dataTestId = el.getAttribute("data-testid") || "";
|
|
1075
|
+
|
|
1076
|
+
// Detect element context for false positive reduction
|
|
1077
|
+
const isInsideModal = !!el.closest('[role="dialog"], [role="alertdialog"], .modal, .dialog, [data-radix-dialog-content]');
|
|
1078
|
+
const isInsideDropdown = !!el.closest('[role="menu"], [role="listbox"], .dropdown, .popover, [data-radix-menu-content]');
|
|
1079
|
+
const isInsideAccordion = !!el.closest('[role="region"], .accordion, [data-state], [data-radix-accordion-content]');
|
|
1080
|
+
const isInsideTooltip = !!el.closest('[role="tooltip"], .tooltip');
|
|
1081
|
+
|
|
1082
|
+
// Detect button intent for false positive reduction
|
|
1083
|
+
const looksLikeClose = /close|dismiss|cancel|x|×|✕|✖/i.test(text) || /close|dismiss/i.test(classList);
|
|
1084
|
+
const looksLikeToggle = /toggle|expand|collapse|show|hide|menu|hamburger|more/i.test(text) || /toggle|accordion|collaps/i.test(classList);
|
|
1085
|
+
const looksLikeCopy = /copy|clipboard/i.test(text) || /copy/i.test(classList);
|
|
1086
|
+
const looksLikeTheme = /theme|dark|light|mode/i.test(text) || /theme/i.test(classList);
|
|
1087
|
+
const looksLikeTab = el.getAttribute("role") === "tab" || /tab/i.test(classList);
|
|
1088
|
+
const looksLikeSort = /sort|order|filter/i.test(text);
|
|
1089
|
+
|
|
1090
|
+
// Elements that legitimately may not trigger detectable changes
|
|
1091
|
+
const isLikelyFalsePositive = looksLikeClose || looksLikeToggle || looksLikeCopy ||
|
|
1092
|
+
looksLikeTheme || looksLikeTab || looksLikeSort ||
|
|
1093
|
+
isInsideModal || isInsideDropdown || isInsideTooltip;
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
idx,
|
|
1097
|
+
tag: el.tagName.toLowerCase(),
|
|
1098
|
+
role: el.getAttribute("role") || "",
|
|
1099
|
+
href: el.tagName === "A" ? el.getAttribute("href") || "" : "",
|
|
1100
|
+
text: (el.getAttribute("aria-label") || el.innerText || "").trim().slice(0, 80),
|
|
1101
|
+
id: el.id || "",
|
|
1102
|
+
disabled: !!(el.disabled || el.getAttribute("aria-disabled") === "true"),
|
|
1103
|
+
key: `${el.tagName}|${el.id}|${idx}`,
|
|
1104
|
+
// Context for false positive reduction
|
|
1105
|
+
context: {
|
|
1106
|
+
isInsideModal,
|
|
1107
|
+
isInsideDropdown,
|
|
1108
|
+
isInsideAccordion,
|
|
1109
|
+
isInsideTooltip,
|
|
1110
|
+
looksLikeClose,
|
|
1111
|
+
looksLikeToggle,
|
|
1112
|
+
looksLikeCopy,
|
|
1113
|
+
looksLikeTheme,
|
|
1114
|
+
looksLikeTab,
|
|
1115
|
+
isLikelyFalsePositive
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function attemptLogin(page, { auth }) {
|
|
1123
|
+
if (!auth) return { did: false, ok: false };
|
|
1124
|
+
const [email, pass] = String(auth).split(":");
|
|
1125
|
+
if (!email || !pass) return { did: false, ok: false };
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
const emailLoc = page.locator("input[type='email'], input[name*='email' i], input[placeholder*='email' i]").first();
|
|
1129
|
+
const passLoc = page.locator("input[type='password']").first();
|
|
1130
|
+
if ((await emailLoc.count()) === 0 || (await passLoc.count()) === 0) return { did: false, ok: false };
|
|
1131
|
+
|
|
1132
|
+
await emailLoc.fill(email, { timeout: 1200 });
|
|
1133
|
+
await passLoc.fill(pass, { timeout: 1200 });
|
|
1134
|
+
|
|
1135
|
+
const submit = page.locator("button[type='submit'], input[type='submit']").first();
|
|
1136
|
+
const fallback = page.locator("button:has-text('Log in'), button:has-text('Sign in')").first();
|
|
1137
|
+
const before = page.url();
|
|
1138
|
+
|
|
1139
|
+
if ((await submit.count()) > 0) await submit.click({ timeout: 1200 });
|
|
1140
|
+
else if ((await fallback.count()) > 0) await fallback.click({ timeout: 1200 });
|
|
1141
|
+
else return { did: true, ok: false };
|
|
1142
|
+
|
|
1143
|
+
await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
|
|
1144
|
+
const stillLogin = await isLoginPage(page);
|
|
1145
|
+
return { did: true, ok: normalizeUrl(page.url()) !== normalizeUrl(before) || !stillLogin };
|
|
1146
|
+
} catch {
|
|
1147
|
+
return { did: true, ok: false };
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async function runSinglePass({ label, baseUrl, context, shotsDir, danger, maxPages, maxDepth, timeoutMs, root, onProgress, retries = 2, stableWait = 500 }) {
|
|
1152
|
+
const page = await context.newPage();
|
|
1153
|
+
page.setDefaultTimeout(timeoutMs);
|
|
1154
|
+
|
|
1155
|
+
// Helper for flaky-resistant navigation with retries
|
|
1156
|
+
async function safeGoto(targetUrl, opts = {}) {
|
|
1157
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1158
|
+
try {
|
|
1159
|
+
const res = await page.goto(targetUrl, { waitUntil: "domcontentloaded", ...opts });
|
|
1160
|
+
// Wait for stability to reduce flakiness
|
|
1161
|
+
if (stableWait > 0) {
|
|
1162
|
+
await page.waitForTimeout(stableWait);
|
|
1163
|
+
}
|
|
1164
|
+
await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
|
|
1165
|
+
return res;
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
if (attempt === retries) throw err;
|
|
1168
|
+
await page.waitForTimeout(500 * attempt); // Exponential backoff
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Helper for flaky-resistant clicks with retries
|
|
1175
|
+
async function safeClick(locator, opts = {}) {
|
|
1176
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1177
|
+
try {
|
|
1178
|
+
await locator.click({ timeout: timeoutMs / 2, ...opts });
|
|
1179
|
+
if (stableWait > 0) {
|
|
1180
|
+
await page.waitForTimeout(stableWait);
|
|
1181
|
+
}
|
|
1182
|
+
return { success: true };
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
if (attempt === retries) return { success: false, error: err.message };
|
|
1185
|
+
await page.waitForTimeout(300 * attempt);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return { success: false, error: 'Max retries exceeded' };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const reqCounter = { value: 0 };
|
|
1192
|
+
const netErrors = [];
|
|
1193
|
+
const consoleErrors = [];
|
|
1194
|
+
const findings = [];
|
|
1195
|
+
const pagesVisited = [];
|
|
1196
|
+
const fakeDataDetections = []; // Track fake data detections
|
|
1197
|
+
const processedUrls = new Set(); // Avoid duplicate detections
|
|
1198
|
+
|
|
1199
|
+
page.on("requestfinished", () => { reqCounter.value += 1; });
|
|
1200
|
+
page.on("requestfailed", (req) => { netErrors.push({ url: req.url(), failure: req.failure()?.errorText || "unknown" }); });
|
|
1201
|
+
page.on("console", (msg) => { if (msg.type() === "error") consoleErrors.push({ text: msg.text().slice(0, 500) }); });
|
|
1202
|
+
page.on("pageerror", (err) => { consoleErrors.push({ text: String(err?.message || err).slice(0, 500) }); });
|
|
1203
|
+
|
|
1204
|
+
// Intercept responses for fake data detection
|
|
1205
|
+
page.on("response", async (response) => {
|
|
1206
|
+
try {
|
|
1207
|
+
const url = response.url();
|
|
1208
|
+
const status = response.status();
|
|
1209
|
+
|
|
1210
|
+
// Skip already processed URLs and static assets
|
|
1211
|
+
if (processedUrls.has(url)) return;
|
|
1212
|
+
if (/\.(js|css|png|jpg|svg|ico|woff|woff2|ttf|gif|webp)(\?|$)/i.test(url)) return;
|
|
1213
|
+
|
|
1214
|
+
// Only check API-like endpoints
|
|
1215
|
+
if (!url.includes('/api/') && !url.includes('/graphql') && !url.includes('/trpc') &&
|
|
1216
|
+
!response.headers()['content-type']?.includes('application/json')) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
processedUrls.add(url);
|
|
1221
|
+
|
|
1222
|
+
let body = '';
|
|
1223
|
+
try {
|
|
1224
|
+
body = await response.text();
|
|
1225
|
+
} catch {
|
|
1226
|
+
// Some responses can't be read
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const detections = classifyNetworkTraffic(url, body, status);
|
|
1230
|
+
if (detections && detections.length > 0) {
|
|
1231
|
+
fakeDataDetections.push(...detections);
|
|
1232
|
+
}
|
|
1233
|
+
} catch {
|
|
1234
|
+
// Ignore errors in response processing
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
const visited = new Set();
|
|
1239
|
+
const queue = [{ url: baseUrl, depth: 0 }];
|
|
1240
|
+
|
|
1241
|
+
while (queue.length && pagesVisited.length < maxPages) {
|
|
1242
|
+
const item = queue.shift();
|
|
1243
|
+
if (!item) break;
|
|
1244
|
+
|
|
1245
|
+
const targetUrl = normalizeUrl(item.url);
|
|
1246
|
+
if (!sameOrigin(baseUrl, targetUrl) || visited.has(targetUrl)) continue;
|
|
1247
|
+
if (!danger && looksAuthAction(targetUrl)) continue;
|
|
1248
|
+
|
|
1249
|
+
visited.add(targetUrl);
|
|
1250
|
+
|
|
1251
|
+
// Progress callback
|
|
1252
|
+
if (onProgress) {
|
|
1253
|
+
onProgress({ page: pagesVisited.length + 1, maxPages, url: targetUrl });
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const res = await safeGoto(targetUrl).catch(() => null);
|
|
1257
|
+
|
|
1258
|
+
const status = res ? res.status() : null;
|
|
1259
|
+
const loginLike = await isLoginPage(page);
|
|
1260
|
+
pagesVisited.push({ url: page.url(), depth: item.depth, status, loginLike });
|
|
1261
|
+
|
|
1262
|
+
if (status && status >= 400) {
|
|
1263
|
+
const shot = path.join(shotsDir, `${label}_http_${status}_${sha1(targetUrl)}.png`);
|
|
1264
|
+
await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
|
|
1265
|
+
findings.push({
|
|
1266
|
+
id: `R_${label}_HTTP_${status}_${sha1(targetUrl).slice(0, 8)}`,
|
|
1267
|
+
severity: status >= 500 ? "BLOCK" : "WARN",
|
|
1268
|
+
category: "DeadUI",
|
|
1269
|
+
title: `[${label}] HTTP ${status} at ${targetUrl}`,
|
|
1270
|
+
page: targetUrl,
|
|
1271
|
+
reason: "Navigation reached an error status",
|
|
1272
|
+
screenshot: path.relative(root, shot).replace(/\\/g, "/")
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (item.depth < maxDepth) {
|
|
1277
|
+
for (const l of await collectLinks(page, baseUrl)) {
|
|
1278
|
+
const abs = normalizeUrl(l);
|
|
1279
|
+
if (sameOrigin(baseUrl, abs) && !visited.has(abs) && (danger || !looksAuthAction(abs))) {
|
|
1280
|
+
queue.push({ url: abs, depth: item.depth + 1 });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
for (const el of await collectInteractives(page)) {
|
|
1286
|
+
if (el.disabled || (looksRisky(el.text) && !danger)) continue;
|
|
1287
|
+
|
|
1288
|
+
let locator;
|
|
1289
|
+
try {
|
|
1290
|
+
if (el.tag === "a") locator = page.locator("a[href]").nth(el.idx);
|
|
1291
|
+
else if (el.tag === "button") locator = page.locator("button").nth(el.idx);
|
|
1292
|
+
else if (el.role === "button") locator = page.locator("[role='button']").nth(el.idx);
|
|
1293
|
+
else continue;
|
|
1294
|
+
} catch { continue; }
|
|
1295
|
+
|
|
1296
|
+
const out = await clickOutcome(page, locator, { reqCounter });
|
|
1297
|
+
|
|
1298
|
+
if (!out.clickOk) {
|
|
1299
|
+
// Skip click failures for elements that are likely intentionally not clickable
|
|
1300
|
+
// (e.g., visually hidden close buttons, buttons behind overlays)
|
|
1301
|
+
if (el.context?.isLikelyFalsePositive) continue;
|
|
1302
|
+
|
|
1303
|
+
const shot = path.join(shotsDir, `${label}_click_fail_${sha1(el.key)}.png`);
|
|
1304
|
+
await page.screenshot({ path: shot }).catch(() => {});
|
|
1305
|
+
findings.push({
|
|
1306
|
+
id: `R_${label}_CLICK_FAIL_${sha1(el.key).slice(0, 8)}`,
|
|
1307
|
+
severity: "WARN",
|
|
1308
|
+
category: "DeadUI",
|
|
1309
|
+
title: `[${label}] Click failed: ${el.text || el.tag}`,
|
|
1310
|
+
page: page.url(),
|
|
1311
|
+
reason: out.clickError || "click failed",
|
|
1312
|
+
screenshot: path.relative(root, shot).replace(/\\/g, "/")
|
|
1313
|
+
});
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Enhanced Dead UI detection with false positive reduction
|
|
1318
|
+
// An element is considered "dead" if clicking produced NO observable effect:
|
|
1319
|
+
// - No navigation
|
|
1320
|
+
// - No URL change
|
|
1321
|
+
// - No DOM mutations
|
|
1322
|
+
// - No CSS visibility/state changes
|
|
1323
|
+
// - No network requests
|
|
1324
|
+
const noEffect = !out.navHappened && !out.urlChanged && !out.domChanged &&
|
|
1325
|
+
!out.visibilityChanged && out.reqDelta === 0;
|
|
1326
|
+
|
|
1327
|
+
if (noEffect) {
|
|
1328
|
+
// Apply false positive reduction based on element context
|
|
1329
|
+
const ctx = el.context || {};
|
|
1330
|
+
|
|
1331
|
+
// Skip elements that are KNOWN to not produce observable changes
|
|
1332
|
+
// These are legitimate UI patterns that don't need fixes
|
|
1333
|
+
if (ctx.looksLikeClose && ctx.isInsideModal) {
|
|
1334
|
+
// Close button inside a modal - the modal itself may have closed
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
if (ctx.looksLikeCopy) {
|
|
1338
|
+
// Copy buttons work via clipboard API, no DOM change expected
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (ctx.looksLikeTheme) {
|
|
1342
|
+
// Theme toggles may only change CSS custom properties
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Downgrade severity for likely false positives
|
|
1347
|
+
// Instead of BLOCK, use WARN for elements in contexts that commonly have no-op behavior
|
|
1348
|
+
let severity = "BLOCK";
|
|
1349
|
+
let reason = "Click produced no navigation, no network activity, and no DOM change";
|
|
1350
|
+
|
|
1351
|
+
if (ctx.isLikelyFalsePositive) {
|
|
1352
|
+
severity = "WARN";
|
|
1353
|
+
reason = `Click produced no observable change (possible false positive: ${
|
|
1354
|
+
ctx.looksLikeToggle ? 'toggle button' :
|
|
1355
|
+
ctx.looksLikeTab ? 'tab element' :
|
|
1356
|
+
ctx.looksLikeSort ? 'sort control' :
|
|
1357
|
+
ctx.isInsideDropdown ? 'inside dropdown' :
|
|
1358
|
+
ctx.isInsideAccordion ? 'inside accordion' :
|
|
1359
|
+
'contextual element'
|
|
1360
|
+
})`;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Always skip tooltip-related elements as they are purely visual
|
|
1364
|
+
if (ctx.isInsideTooltip) continue;
|
|
1365
|
+
|
|
1366
|
+
const shot = path.join(shotsDir, `${label}_dead_${sha1(el.key)}.png`);
|
|
1367
|
+
await page.screenshot({ path: shot }).catch(() => {});
|
|
1368
|
+
findings.push({
|
|
1369
|
+
id: `R_${label}_DEAD_${sha1(el.key).slice(0, 8)}`,
|
|
1370
|
+
severity,
|
|
1371
|
+
category: "DeadUI",
|
|
1372
|
+
title: `[${label}] Dead UI: ${el.text || el.tag}`,
|
|
1373
|
+
page: page.url(),
|
|
1374
|
+
reason,
|
|
1375
|
+
screenshot: path.relative(root, shot).replace(/\\/g, "/"),
|
|
1376
|
+
confidence: ctx.isLikelyFalsePositive ? 0.5 : 0.9, // Add confidence score
|
|
1377
|
+
context: ctx // Include context for debugging
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
await page.close();
|
|
1384
|
+
|
|
1385
|
+
// Convert fake data detections to findings with confidence-based filtering
|
|
1386
|
+
const seenFakeUrls = new Set();
|
|
1387
|
+
|
|
1388
|
+
// Sort by confidence (highest first) to prioritize most reliable detections
|
|
1389
|
+
const sortedDetections = [...fakeDataDetections].sort((a, b) =>
|
|
1390
|
+
(b.confidence || 0.5) - (a.confidence || 0.5)
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
for (const detection of sortedDetections) {
|
|
1394
|
+
// Dedupe by URL + type + pattern to avoid near-duplicates
|
|
1395
|
+
const key = `${detection.url}:${detection.type}:${detection.pattern || ''}`;
|
|
1396
|
+
if (seenFakeUrls.has(key)) continue;
|
|
1397
|
+
seenFakeUrls.add(key);
|
|
1398
|
+
|
|
1399
|
+
// Skip very low confidence detections (likely false positives)
|
|
1400
|
+
const confidence = detection.confidence || 0.5;
|
|
1401
|
+
if (confidence < 0.50) continue;
|
|
1402
|
+
|
|
1403
|
+
// Downgrade severity for medium confidence detections
|
|
1404
|
+
let severity = detection.severity;
|
|
1405
|
+
if (confidence < 0.70 && severity === 'BLOCK') {
|
|
1406
|
+
severity = 'WARN';
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
findings.push({
|
|
1410
|
+
id: `R_${label}_FAKE_${sha1(key).slice(0, 8)}`,
|
|
1411
|
+
severity,
|
|
1412
|
+
category: detection.type === 'fake-domain' ? 'FakeDomain' :
|
|
1413
|
+
detection.type === 'fake-response' ? 'FakeResponse' : 'MockStatus',
|
|
1414
|
+
title: `[${label}] Fake Data: ${detection.evidence}`,
|
|
1415
|
+
page: detection.url,
|
|
1416
|
+
reason: detection.evidence,
|
|
1417
|
+
confidence, // Include confidence score for transparency
|
|
1418
|
+
pattern: detection.pattern // Include pattern for debugging
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return {
|
|
1423
|
+
label,
|
|
1424
|
+
pagesVisited,
|
|
1425
|
+
findings,
|
|
1426
|
+
consoleErrors: consoleErrors.slice(0, 50),
|
|
1427
|
+
networkErrors: netErrors.slice(0, 50),
|
|
1428
|
+
fakeDataDetections: fakeDataDetections.slice(0, 100)
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function buildAuthCoverageFindings({ baseUrl, matchers, anonPass, authPass }) {
|
|
1433
|
+
const findings = [];
|
|
1434
|
+
const anonByPath = new Map((anonPass.pagesVisited || []).map(p => [pathFromUrl(p.url), p]));
|
|
1435
|
+
const authByPath = new Map((authPass?.pagesVisited || []).map(p => [pathFromUrl(p.url), p]));
|
|
1436
|
+
|
|
1437
|
+
for (const [pathKey, anon] of anonByPath) {
|
|
1438
|
+
const isProtected = matchers.some(m => m.rx.test(pathKey));
|
|
1439
|
+
if (!isProtected) continue;
|
|
1440
|
+
|
|
1441
|
+
const anonLooksBlocked = anon.loginLike || anon.status === 401 || anon.status === 403;
|
|
1442
|
+
const authed = authByPath.get(pathKey);
|
|
1443
|
+
const authedLooksBlocked = authed?.loginLike || authed?.status === 401 || authed?.status === 403;
|
|
1444
|
+
|
|
1445
|
+
if (!anonLooksBlocked) {
|
|
1446
|
+
findings.push({
|
|
1447
|
+
id: `R_AUTH_ANON_ACCESS_${sha1(pathKey).slice(0, 8)}`,
|
|
1448
|
+
severity: "BLOCK",
|
|
1449
|
+
category: "AuthCoverage",
|
|
1450
|
+
title: `Protected route reachable anonymously: ${pathKey}`,
|
|
1451
|
+
page: new URL(pathKey, baseUrl).toString(),
|
|
1452
|
+
reason: "Matcher marks route protected, but anon session did not redirect or deny."
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (authed && authedLooksBlocked) {
|
|
1457
|
+
findings.push({
|
|
1458
|
+
id: `R_AUTH_BLOCKED_${sha1(pathKey).slice(0, 8)}`,
|
|
1459
|
+
severity: "BLOCK",
|
|
1460
|
+
category: "AuthCoverage",
|
|
1461
|
+
title: `Protected route blocked after login: ${pathKey}`,
|
|
1462
|
+
page: new URL(pathKey, baseUrl).toString(),
|
|
1463
|
+
reason: "Authed session still looks like login/401/403 on a protected route."
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return findings;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function coverageFromTruthpack({ truthpack, visitedUrls }) {
|
|
1472
|
+
if (!truthpack) return null;
|
|
1473
|
+
const refs = truthpack?.routes?.clientRefs || [];
|
|
1474
|
+
const uiPaths = new Set(refs.map(r => r?.path || pathFromUrl(r?.url || r)).filter(Boolean));
|
|
1475
|
+
const visitedPaths = new Set(visitedUrls.map(pathFromUrl));
|
|
1476
|
+
const total = uiPaths.size;
|
|
1477
|
+
const hit = Array.from(uiPaths).filter(p => visitedPaths.has(p)).length;
|
|
1478
|
+
return { total, hit, percent: total ? Math.round((hit / total) * 100) : 0, missed: Array.from(uiPaths).filter(p => !visitedPaths.has(p)).slice(0, 50) };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1482
|
+
// REPLAY DATA BUILDERS (for API/dashboard)
|
|
1483
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Build timeline events from pass results for replay
|
|
1487
|
+
*/
|
|
1488
|
+
function buildTimeline(anonPass, authPass) {
|
|
1489
|
+
const timeline = [];
|
|
1490
|
+
const baseTimestamp = Date.now();
|
|
1491
|
+
|
|
1492
|
+
// Add anon pass pages as timeline events
|
|
1493
|
+
if (anonPass?.pagesVisited) {
|
|
1494
|
+
for (let i = 0; i < anonPass.pagesVisited.length; i++) {
|
|
1495
|
+
const page = anonPass.pagesVisited[i];
|
|
1496
|
+
timeline.push({
|
|
1497
|
+
action: 'navigate',
|
|
1498
|
+
url: page.url,
|
|
1499
|
+
selector: null,
|
|
1500
|
+
timestamp: new Date(baseTimestamp + (i * 1000)).toISOString(),
|
|
1501
|
+
screenshot: null,
|
|
1502
|
+
pass: 'anon',
|
|
1503
|
+
status: page.status,
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Add auth pass pages
|
|
1509
|
+
if (authPass?.pagesVisited) {
|
|
1510
|
+
const offset = (anonPass?.pagesVisited?.length || 0) * 1000;
|
|
1511
|
+
for (let i = 0; i < authPass.pagesVisited.length; i++) {
|
|
1512
|
+
const page = authPass.pagesVisited[i];
|
|
1513
|
+
timeline.push({
|
|
1514
|
+
action: 'navigate',
|
|
1515
|
+
url: page.url,
|
|
1516
|
+
selector: null,
|
|
1517
|
+
timestamp: new Date(baseTimestamp + offset + (i * 1000)).toISOString(),
|
|
1518
|
+
screenshot: null,
|
|
1519
|
+
pass: 'auth',
|
|
1520
|
+
status: page.status,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return timeline;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Build network timeline from pass results
|
|
1530
|
+
*/
|
|
1531
|
+
function buildNetworkTimeline(anonPass, authPass) {
|
|
1532
|
+
const network = [];
|
|
1533
|
+
const baseTimestamp = Date.now();
|
|
1534
|
+
let idx = 0;
|
|
1535
|
+
|
|
1536
|
+
// Extract network requests from page visits
|
|
1537
|
+
if (anonPass?.pagesVisited) {
|
|
1538
|
+
for (const page of anonPass.pagesVisited) {
|
|
1539
|
+
network.push({
|
|
1540
|
+
method: 'GET',
|
|
1541
|
+
url: page.url,
|
|
1542
|
+
status: page.status || 200,
|
|
1543
|
+
timestamp: new Date(baseTimestamp + (idx++ * 500)).toISOString(),
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Add network errors as requests
|
|
1549
|
+
if (anonPass?.networkErrors) {
|
|
1550
|
+
for (const err of anonPass.networkErrors) {
|
|
1551
|
+
network.push({
|
|
1552
|
+
method: 'GET',
|
|
1553
|
+
url: err.url,
|
|
1554
|
+
status: 0,
|
|
1555
|
+
timestamp: new Date(baseTimestamp + (idx++ * 500)).toISOString(),
|
|
1556
|
+
error: err.failure,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (authPass?.networkErrors) {
|
|
1562
|
+
for (const err of authPass.networkErrors) {
|
|
1563
|
+
network.push({
|
|
1564
|
+
method: 'GET',
|
|
1565
|
+
url: err.url,
|
|
1566
|
+
status: 0,
|
|
1567
|
+
timestamp: new Date(baseTimestamp + (idx++ * 500)).toISOString(),
|
|
1568
|
+
error: err.failure,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
return network;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Build screenshots list from findings
|
|
1578
|
+
*/
|
|
1579
|
+
function buildScreenshotsList(anonPass, authPass, root) {
|
|
1580
|
+
const screenshots = [];
|
|
1581
|
+
const allFindings = [...(anonPass?.findings || []), ...(authPass?.findings || [])];
|
|
1582
|
+
|
|
1583
|
+
for (const finding of allFindings) {
|
|
1584
|
+
if (finding.screenshot) {
|
|
1585
|
+
screenshots.push({
|
|
1586
|
+
url: finding.page || '',
|
|
1587
|
+
timestamp: new Date().toISOString(),
|
|
1588
|
+
path: finding.screenshot,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return screenshots;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1597
|
+
// FLAKINESS & STABILITY VERIFICATION
|
|
1598
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Aggregate findings from multiple stability runs
|
|
1602
|
+
* Only returns findings that appear in at least `threshold` of runs
|
|
1603
|
+
* @param {Array<Array<Object>>} runFindings - Array of findings arrays from each run
|
|
1604
|
+
* @param {number} threshold - Minimum occurrence rate (0-1) to include a finding
|
|
1605
|
+
* @returns {Array<Object>} Deduplicated findings with flakiness scores
|
|
1606
|
+
*/
|
|
1607
|
+
function aggregateStabilityFindings(runFindings, threshold = 0.66) {
|
|
1608
|
+
const totalRuns = runFindings.length;
|
|
1609
|
+
if (totalRuns === 0) return [];
|
|
1610
|
+
if (totalRuns === 1) return runFindings[0] || [];
|
|
1611
|
+
|
|
1612
|
+
// Group findings by their unique key (category + normalized title/reason)
|
|
1613
|
+
const findingCounts = new Map();
|
|
1614
|
+
|
|
1615
|
+
for (const findings of runFindings) {
|
|
1616
|
+
for (const finding of findings) {
|
|
1617
|
+
// Create a stable key for deduplication
|
|
1618
|
+
const key = `${finding.category}|${finding.title?.replace(/\[ANON\]|\[AUTH\]/g, '').trim()}|${finding.page || ''}`;
|
|
1619
|
+
|
|
1620
|
+
if (!findingCounts.has(key)) {
|
|
1621
|
+
findingCounts.set(key, {
|
|
1622
|
+
finding: { ...finding },
|
|
1623
|
+
count: 0,
|
|
1624
|
+
occurrences: []
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const entry = findingCounts.get(key);
|
|
1629
|
+
entry.count++;
|
|
1630
|
+
entry.occurrences.push(finding);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Filter to findings that meet the threshold and add flakiness score
|
|
1635
|
+
const aggregated = [];
|
|
1636
|
+
|
|
1637
|
+
for (const [key, data] of findingCounts) {
|
|
1638
|
+
const occurrenceRate = data.count / totalRuns;
|
|
1639
|
+
|
|
1640
|
+
if (occurrenceRate >= threshold) {
|
|
1641
|
+
// Calculate flakiness score (1 = always occurs, 0 = never)
|
|
1642
|
+
const flakinessScore = 1 - occurrenceRate;
|
|
1643
|
+
|
|
1644
|
+
// Merge the finding with flakiness metadata
|
|
1645
|
+
const aggregatedFinding = {
|
|
1646
|
+
...data.finding,
|
|
1647
|
+
stability: {
|
|
1648
|
+
occurrenceRate: Math.round(occurrenceRate * 100) / 100,
|
|
1649
|
+
appearedInRuns: data.count,
|
|
1650
|
+
totalRuns,
|
|
1651
|
+
flakinessScore: Math.round(flakinessScore * 100) / 100,
|
|
1652
|
+
isFlaky: flakinessScore > 0.1, // More than 10% variance = flaky
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
// If finding appeared in all runs, it's stable
|
|
1657
|
+
// If it appeared in some runs, mark as potentially flaky
|
|
1658
|
+
if (data.count < totalRuns) {
|
|
1659
|
+
aggregatedFinding.reason = `${aggregatedFinding.reason || ''} (appeared ${data.count}/${totalRuns} runs)`.trim();
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
aggregated.push(aggregatedFinding);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
return aggregated;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Print stability verification results
|
|
1671
|
+
*/
|
|
1672
|
+
function printStabilityResults(totalRuns, stableFindings, filteredCount) {
|
|
1673
|
+
if (totalRuns <= 1) return;
|
|
1674
|
+
|
|
1675
|
+
console.log();
|
|
1676
|
+
console.log(` ${colors.info}${ICONS.target}${c.reset} ${c.bold}Stability Verification${c.reset}`);
|
|
1677
|
+
console.log(` ${c.dim}Total runs:${c.reset} ${totalRuns}`);
|
|
1678
|
+
console.log(` ${c.dim}Stable findings:${c.reset} ${stableFindings} ${c.dim}(appeared in majority of runs)${c.reset}`);
|
|
1679
|
+
|
|
1680
|
+
if (filteredCount > 0) {
|
|
1681
|
+
console.log(` ${c.dim}Filtered (flaky):${c.reset} ${colors.success}${filteredCount}${c.reset} ${c.dim}(inconsistent across runs)${c.reset}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1686
|
+
// MAIN REALITY FUNCTION
|
|
1687
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1688
|
+
|
|
1689
|
+
async function runReality(argsOrOpts = {}) {
|
|
1690
|
+
// Handle array args from CLI
|
|
1691
|
+
let globalOpts = { noBanner: false, json: false, quiet: false, ci: false };
|
|
1692
|
+
if (Array.isArray(argsOrOpts)) {
|
|
1693
|
+
const { flags } = parseGlobalFlags(argsOrOpts);
|
|
1694
|
+
globalOpts = { ...globalOpts, ...flags };
|
|
1695
|
+
if (globalOpts.help) {
|
|
1696
|
+
printHelp(globalOpts);
|
|
1697
|
+
return 0;
|
|
1698
|
+
}
|
|
1699
|
+
// Parse args to options
|
|
1700
|
+
const getArg = (flags) => {
|
|
1701
|
+
for (const f of flags) {
|
|
1702
|
+
const idx = argsOrOpts.indexOf(f);
|
|
1703
|
+
if (idx !== -1 && idx < argsOrOpts.length - 1) return argsOrOpts[idx + 1];
|
|
1704
|
+
}
|
|
1705
|
+
return undefined;
|
|
1706
|
+
};
|
|
1707
|
+
argsOrOpts = {
|
|
1708
|
+
url: getArg(["--url", "-u"]),
|
|
1709
|
+
auth: getArg(["--auth"]),
|
|
1710
|
+
storageState: getArg(["--storage-state"]),
|
|
1711
|
+
saveStorageState: getArg(["--save-storage-state"]),
|
|
1712
|
+
truthpack: getArg(["--truthpack"]),
|
|
1713
|
+
verifyAuth: argsOrOpts.includes("--verify-auth"),
|
|
1714
|
+
headed: argsOrOpts.includes("--headed"),
|
|
1715
|
+
danger: argsOrOpts.includes("--danger"),
|
|
1716
|
+
// Visual artifacts options
|
|
1717
|
+
recordVideo: argsOrOpts.includes("--record-video") || argsOrOpts.includes("--video"),
|
|
1718
|
+
recordTrace: argsOrOpts.includes("--record-trace") || argsOrOpts.includes("--trace"),
|
|
1719
|
+
recordHar: argsOrOpts.includes("--record-har") || argsOrOpts.includes("--har"),
|
|
1720
|
+
// Flakiness reduction options
|
|
1721
|
+
retries: parseInt(getArg(["--retries"]) || "2", 10),
|
|
1722
|
+
stableWait: parseInt(getArg(["--stable-wait"]) || "500", 10),
|
|
1723
|
+
maxPages: parseInt(getArg(["--max-pages"]) || "18", 10),
|
|
1724
|
+
maxDepth: parseInt(getArg(["--max-depth"]) || "2", 10),
|
|
1725
|
+
timeoutMs: parseInt(getArg(["--timeout"]) || "15000", 10),
|
|
1726
|
+
...globalOpts,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
let {
|
|
1731
|
+
repoRoot,
|
|
1732
|
+
url,
|
|
1733
|
+
auth,
|
|
1734
|
+
storageState,
|
|
1735
|
+
saveStorageState,
|
|
1736
|
+
truthpack,
|
|
1737
|
+
verifyAuth = false,
|
|
1738
|
+
headed = false,
|
|
1739
|
+
maxPages = 18,
|
|
1740
|
+
maxDepth = 2,
|
|
1741
|
+
danger = false,
|
|
1742
|
+
timeoutMs = 15000,
|
|
1743
|
+
// Visual artifacts (videos, traces, HAR)
|
|
1744
|
+
recordVideo = false,
|
|
1745
|
+
recordTrace = false,
|
|
1746
|
+
recordHar = false,
|
|
1747
|
+
// Flakiness reduction
|
|
1748
|
+
retries = 2,
|
|
1749
|
+
stableWait = 500,
|
|
1750
|
+
stabilityRuns = 1,
|
|
1751
|
+
flakyThreshold = 0.66
|
|
1752
|
+
} = argsOrOpts;
|
|
1753
|
+
|
|
1754
|
+
if (!url) {
|
|
1755
|
+
printHelp(argsOrOpts);
|
|
1756
|
+
console.log(`\n ${colors.error}${ICONS.cross}${c.reset} ${c.bold}Error:${c.reset} --url is required\n`);
|
|
1757
|
+
return 1;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
const root = repoRoot || process.cwd();
|
|
1761
|
+
const projectName = path.basename(root);
|
|
1762
|
+
const startTime = Date.now();
|
|
1763
|
+
const originalMaxPages = maxPages;
|
|
1764
|
+
const originalVerifyAuth = verifyAuth;
|
|
1765
|
+
|
|
1766
|
+
// TIER ENFORCEMENT
|
|
1767
|
+
let tierInfo = { tier: 'free', limits: {} };
|
|
1768
|
+
try {
|
|
1769
|
+
const access = await entitlements.enforce("reality", {
|
|
1770
|
+
projectPath: root,
|
|
1771
|
+
silent: true,
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
tierInfo = access;
|
|
1775
|
+
const limits = access.limits || entitlements.getLimits(access.tier);
|
|
1776
|
+
|
|
1777
|
+
// Apply tier-based caps
|
|
1778
|
+
if (access.downgrade === "reality.preview" || access.tier === "free") {
|
|
1779
|
+
const previewMax = limits.realityMaxPages || 5;
|
|
1780
|
+
if (maxPages > previewMax) {
|
|
1781
|
+
maxPages = previewMax;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (verifyAuth && !limits.realityAuthBoundary) {
|
|
1785
|
+
verifyAuth = false;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
} catch (e) {
|
|
1789
|
+
// Continue with defaults if entitlements unavailable
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Print banner
|
|
1793
|
+
if (shouldShowBanner(argsOrOpts)) {
|
|
1794
|
+
printBanner();
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
console.log(` ${c.dim}Project:${c.reset} ${c.bold}${projectName}${c.reset}`);
|
|
1798
|
+
console.log(` ${c.dim}URL:${c.reset} ${colors.accent}${url}${c.reset}`);
|
|
1799
|
+
console.log(` ${c.dim}Mode:${c.reset} ${verifyAuth ? `${colors.auth}Two-Pass (Auth)${c.reset}` : `${colors.anon}Single-Pass (Anon)${c.reset}`}`);
|
|
1800
|
+
console.log(` ${c.dim}Budget:${c.reset} ${maxPages} pages, depth ${maxDepth}`);
|
|
1801
|
+
if (stabilityRuns > 1) {
|
|
1802
|
+
console.log(` ${c.dim}Stability:${c.reset} ${colors.info}${stabilityRuns} runs${c.reset}, threshold ${Math.round(flakyThreshold * 100)}%`);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// Tier warning if applicable
|
|
1806
|
+
if (tierInfo.tier === 'free' && (originalMaxPages > maxPages || (originalVerifyAuth && !verifyAuth))) {
|
|
1807
|
+
printTierWarning(tierInfo.tier, tierInfo.limits, originalMaxPages, maxPages, originalVerifyAuth, verifyAuth);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Playwright check
|
|
1811
|
+
if (!chromium) {
|
|
1812
|
+
const hint = playwrightError?.includes("Cannot find module")
|
|
1813
|
+
? "Run: npm i -D playwright && npx playwright install chromium"
|
|
1814
|
+
: `Playwright error: ${playwrightError || "unknown"}`;
|
|
1815
|
+
console.log();
|
|
1816
|
+
console.log(` ${colors.error}${ICONS.cross}${c.reset} ${c.bold}Playwright not available${c.reset}`);
|
|
1817
|
+
console.log(` ${c.dim}${hint}${c.reset}`);
|
|
1818
|
+
console.log();
|
|
1819
|
+
return 1;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const baseUrl = normalizeUrl(url);
|
|
1823
|
+
const outBase = path.join(root, ".vibecheck", "reality", stamp());
|
|
1824
|
+
const shotsDir = path.join(outBase, "screenshots");
|
|
1825
|
+
const videosDir = path.join(outBase, "videos");
|
|
1826
|
+
const tracesDir = path.join(outBase, "traces");
|
|
1827
|
+
const harDir = path.join(outBase, "har");
|
|
1828
|
+
ensureDir(shotsDir);
|
|
1829
|
+
if (recordVideo) ensureDir(videosDir);
|
|
1830
|
+
if (recordTrace) ensureDir(tracesDir);
|
|
1831
|
+
if (recordHar) ensureDir(harDir);
|
|
1832
|
+
|
|
1833
|
+
const tp = loadTruthpack(root, truthpack);
|
|
1834
|
+
const matchers = getProtectedMatchersFromTruthpack(tp);
|
|
1835
|
+
|
|
1836
|
+
if (tp) {
|
|
1837
|
+
console.log(` ${c.dim}Truthpack:${c.reset} ${colors.success}${ICONS.check}${c.reset} loaded (${matchers.length} protected patterns)`);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Launch browser
|
|
1841
|
+
console.log();
|
|
1842
|
+
startSpinner('Launching browser...', colors.accent);
|
|
1843
|
+
const browser = await chromium.launch({ headless: !headed });
|
|
1844
|
+
stopSpinner('Browser launched', true);
|
|
1845
|
+
|
|
1846
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1847
|
+
// STABILITY RUNS (multiple passes for flakiness detection)
|
|
1848
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1849
|
+
|
|
1850
|
+
const allRunFindings = [];
|
|
1851
|
+
let lastAnonPass = null;
|
|
1852
|
+
let lastAuthPass = null;
|
|
1853
|
+
let anonVideoPath = null;
|
|
1854
|
+
let authVideoPath = null;
|
|
1855
|
+
let anonTracePath = null;
|
|
1856
|
+
let authTracePath = null;
|
|
1857
|
+
let savedStatePath = null;
|
|
1858
|
+
|
|
1859
|
+
for (let runNum = 1; runNum <= stabilityRuns; runNum++) {
|
|
1860
|
+
const isFirstRun = runNum === 1;
|
|
1861
|
+
const isLastRun = runNum === stabilityRuns;
|
|
1862
|
+
|
|
1863
|
+
if (stabilityRuns > 1) {
|
|
1864
|
+
console.log();
|
|
1865
|
+
console.log(` ${colors.info}${BOX.hHorizontal.repeat(3)}${c.reset} ${c.bold}Stability Run ${runNum}/${stabilityRuns}${c.reset}`);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1869
|
+
// PASS A: ANONYMOUS
|
|
1870
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1871
|
+
printPassHeader('anon', baseUrl);
|
|
1872
|
+
|
|
1873
|
+
startSpinner('Crawling anonymously...', colors.anon);
|
|
1874
|
+
|
|
1875
|
+
// Build context options for video/HAR recording (only on last run to save resources)
|
|
1876
|
+
const anonContextOpts = {};
|
|
1877
|
+
if (recordVideo && isLastRun) {
|
|
1878
|
+
anonContextOpts.recordVideo = {
|
|
1879
|
+
dir: videosDir,
|
|
1880
|
+
size: { width: 1280, height: 720 }
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
if (recordHar && isLastRun) {
|
|
1884
|
+
anonContextOpts.recordHar = {
|
|
1885
|
+
path: path.join(harDir, 'anon-traffic.har'),
|
|
1886
|
+
mode: 'full'
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
const anonContext = await browser.newContext(anonContextOpts);
|
|
1891
|
+
|
|
1892
|
+
// Start trace recording if enabled (only on last run)
|
|
1893
|
+
if (recordTrace && isLastRun) {
|
|
1894
|
+
await anonContext.tracing.start({
|
|
1895
|
+
screenshots: true,
|
|
1896
|
+
snapshots: true,
|
|
1897
|
+
sources: false
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const anonPass = await runSinglePass({
|
|
1902
|
+
label: "ANON",
|
|
1903
|
+
baseUrl,
|
|
1904
|
+
context: anonContext,
|
|
1905
|
+
shotsDir: isLastRun ? shotsDir : path.join(outBase, `run${runNum}`, 'screenshots'),
|
|
1906
|
+
danger,
|
|
1907
|
+
maxPages,
|
|
1908
|
+
maxDepth,
|
|
1909
|
+
timeoutMs,
|
|
1910
|
+
root,
|
|
1911
|
+
retries,
|
|
1912
|
+
stableWait,
|
|
1913
|
+
onProgress: ({ page, maxPages: mp, url: currentUrl }) => {
|
|
1914
|
+
// Could update spinner here if desired
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
// Ensure shot dir exists for intermediate runs
|
|
1919
|
+
if (!isLastRun) {
|
|
1920
|
+
ensureDir(path.join(outBase, `run${runNum}`, 'screenshots'));
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Save trace if enabled (only last run)
|
|
1924
|
+
if (recordTrace && isLastRun) {
|
|
1925
|
+
anonTracePath = path.join(tracesDir, 'anon-trace.zip');
|
|
1926
|
+
await anonContext.tracing.stop({ path: anonTracePath });
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Get video path before closing context (only last run)
|
|
1930
|
+
if (recordVideo && isLastRun && anonPass.pagesVisited.length > 0) {
|
|
1931
|
+
const pages = anonContext.pages();
|
|
1932
|
+
if (pages.length > 0) {
|
|
1933
|
+
const video = pages[0].video();
|
|
1934
|
+
if (video) {
|
|
1935
|
+
try {
|
|
1936
|
+
anonVideoPath = await video.path();
|
|
1937
|
+
} catch {}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
await anonContext.close();
|
|
1943
|
+
stopSpinner(`Crawled ${anonPass.pagesVisited.length} pages`, true);
|
|
1944
|
+
|
|
1945
|
+
printPassResult('anon', anonPass);
|
|
1946
|
+
lastAnonPass = anonPass;
|
|
1947
|
+
|
|
1948
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1949
|
+
// PASS B: AUTHENTICATED (optional)
|
|
1950
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1951
|
+
let authPass = null;
|
|
1952
|
+
let authFindings = [];
|
|
1953
|
+
|
|
1954
|
+
if (verifyAuth) {
|
|
1955
|
+
printPassHeader('auth', baseUrl);
|
|
1956
|
+
|
|
1957
|
+
startSpinner('Setting up authenticated session...', colors.auth);
|
|
1958
|
+
const ctxOpts = storageState ? { storageState } : {};
|
|
1959
|
+
|
|
1960
|
+
// Add video/HAR recording options (only last run)
|
|
1961
|
+
if (recordVideo && isLastRun) {
|
|
1962
|
+
ctxOpts.recordVideo = {
|
|
1963
|
+
dir: videosDir,
|
|
1964
|
+
size: { width: 1280, height: 720 }
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
if (recordHar && isLastRun) {
|
|
1968
|
+
ctxOpts.recordHar = {
|
|
1969
|
+
path: path.join(harDir, 'auth-traffic.har'),
|
|
1970
|
+
mode: 'full'
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
const authContext = await browser.newContext(ctxOpts);
|
|
1975
|
+
|
|
1976
|
+
// Start trace recording if enabled (only last run)
|
|
1977
|
+
if (recordTrace && isLastRun) {
|
|
1978
|
+
await authContext.tracing.start({
|
|
1979
|
+
screenshots: true,
|
|
1980
|
+
snapshots: true,
|
|
1981
|
+
sources: false
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
const authPage = await authContext.newPage();
|
|
1985
|
+
await authPage.goto(baseUrl, { waitUntil: "domcontentloaded" }).catch(() => {});
|
|
1986
|
+
await authPage.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
|
|
1987
|
+
|
|
1988
|
+
if (!storageState && auth && isFirstRun) {
|
|
1989
|
+
stopSpinner('Attempting login...', true);
|
|
1990
|
+
startSpinner('Logging in...', colors.auth);
|
|
1991
|
+
|
|
1992
|
+
const loginRes = await attemptLogin(authPage, { auth });
|
|
1993
|
+
|
|
1994
|
+
if (loginRes.ok) {
|
|
1995
|
+
stopSpinner('Login successful', true);
|
|
1996
|
+
if (saveStorageState) {
|
|
1997
|
+
const dest = path.isAbsolute(saveStorageState) ? saveStorageState : path.join(root, saveStorageState);
|
|
1998
|
+
ensureDir(path.dirname(dest));
|
|
1999
|
+
await authContext.storageState({ path: dest }).catch(() => {});
|
|
2000
|
+
savedStatePath = dest;
|
|
2001
|
+
console.log(` ${colors.success}${ICONS.check}${c.reset} Session saved: ${c.dim}${path.relative(root, dest)}${c.reset}`);
|
|
2002
|
+
}
|
|
2003
|
+
} else {
|
|
2004
|
+
stopSpinner('Login failed - continuing without auth', false);
|
|
2005
|
+
}
|
|
2006
|
+
} else {
|
|
2007
|
+
stopSpinner('Using existing session', true);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
await authPage.close();
|
|
2011
|
+
|
|
2012
|
+
startSpinner('Crawling with authentication...', colors.auth);
|
|
2013
|
+
authPass = await runSinglePass({
|
|
2014
|
+
label: "AUTH",
|
|
2015
|
+
baseUrl,
|
|
2016
|
+
context: authContext,
|
|
2017
|
+
shotsDir: isLastRun ? shotsDir : path.join(outBase, `run${runNum}`, 'screenshots'),
|
|
2018
|
+
danger,
|
|
2019
|
+
maxPages,
|
|
2020
|
+
maxDepth,
|
|
2021
|
+
timeoutMs,
|
|
2022
|
+
root,
|
|
2023
|
+
retries,
|
|
2024
|
+
stableWait
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
// Save trace if enabled (only last run)
|
|
2028
|
+
if (recordTrace && isLastRun) {
|
|
2029
|
+
authTracePath = path.join(tracesDir, 'auth-trace.zip');
|
|
2030
|
+
await authContext.tracing.stop({ path: authTracePath });
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Get video path before closing context (only last run)
|
|
2034
|
+
if (recordVideo && isLastRun && authPass.pagesVisited.length > 0) {
|
|
2035
|
+
const pages = authContext.pages();
|
|
2036
|
+
if (pages.length > 0) {
|
|
2037
|
+
const video = pages[0].video();
|
|
2038
|
+
if (video) {
|
|
2039
|
+
try {
|
|
2040
|
+
authVideoPath = await video.path();
|
|
2041
|
+
} catch {}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
await authContext.close();
|
|
2047
|
+
stopSpinner(`Crawled ${authPass.pagesVisited.length} pages`, true);
|
|
2048
|
+
|
|
2049
|
+
printPassResult('auth', authPass);
|
|
2050
|
+
lastAuthPass = authPass;
|
|
2051
|
+
|
|
2052
|
+
// Build auth coverage findings
|
|
2053
|
+
if (matchers.length) {
|
|
2054
|
+
startSpinner('Analyzing auth coverage...', colors.authCoverage);
|
|
2055
|
+
authFindings = buildAuthCoverageFindings({ baseUrl, matchers, anonPass, authPass });
|
|
2056
|
+
stopSpinner(`Found ${authFindings.length} auth issues`, authFindings.length === 0);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Collect findings from this run
|
|
2061
|
+
const runFindings = [...anonPass.findings, ...(authPass?.findings || []), ...authFindings];
|
|
2062
|
+
allRunFindings.push(runFindings);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
await browser.close();
|
|
2066
|
+
|
|
2067
|
+
// Use last pass results for page/coverage data
|
|
2068
|
+
const anonPass = lastAnonPass;
|
|
2069
|
+
const authPass = lastAuthPass;
|
|
2070
|
+
|
|
2071
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2072
|
+
// ANALYSIS & RESULTS
|
|
2073
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2074
|
+
|
|
2075
|
+
const allVisited = [...anonPass.pagesVisited.map(p => p.url), ...(authPass?.pagesVisited || []).map(p => p.url)];
|
|
2076
|
+
const coverage = coverageFromTruthpack({ truthpack: tp, visitedUrls: allVisited });
|
|
2077
|
+
|
|
2078
|
+
// Aggregate findings from stability runs (filters out flaky findings)
|
|
2079
|
+
let findings;
|
|
2080
|
+
let filteredFlakyCount = 0;
|
|
2081
|
+
|
|
2082
|
+
if (stabilityRuns > 1) {
|
|
2083
|
+
// Count total unique findings across all runs before filtering
|
|
2084
|
+
const allFindingsFlat = allRunFindings.flat();
|
|
2085
|
+
const uniqueBeforeFilter = new Set(allFindingsFlat.map(f =>
|
|
2086
|
+
`${f.category}|${f.title?.replace(/\[ANON\]|\[AUTH\]/g, '').trim()}|${f.page || ''}`
|
|
2087
|
+
)).size;
|
|
2088
|
+
|
|
2089
|
+
findings = aggregateStabilityFindings(allRunFindings, flakyThreshold);
|
|
2090
|
+
filteredFlakyCount = uniqueBeforeFilter - findings.length;
|
|
2091
|
+
|
|
2092
|
+
printStabilityResults(stabilityRuns, findings.length, filteredFlakyCount);
|
|
2093
|
+
} else {
|
|
2094
|
+
// Single run - use findings directly
|
|
2095
|
+
findings = allRunFindings[0] || [];
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const blocks = findings.filter(f => f.severity === "BLOCK").length;
|
|
2099
|
+
const warns = findings.filter(f => f.severity === "WARN").length;
|
|
2100
|
+
|
|
2101
|
+
// Build artifact manifest
|
|
2102
|
+
const artifacts = {
|
|
2103
|
+
screenshots: shotsDir ? path.relative(root, shotsDir).replace(/\\/g, "/") : null,
|
|
2104
|
+
videos: recordVideo ? {
|
|
2105
|
+
directory: path.relative(root, videosDir).replace(/\\/g, "/"),
|
|
2106
|
+
anon: anonVideoPath ? path.relative(root, anonVideoPath).replace(/\\/g, "/") : null,
|
|
2107
|
+
auth: authVideoPath ? path.relative(root, authVideoPath).replace(/\\/g, "/") : null
|
|
2108
|
+
} : null,
|
|
2109
|
+
traces: recordTrace ? {
|
|
2110
|
+
directory: path.relative(root, tracesDir).replace(/\\/g, "/"),
|
|
2111
|
+
anon: anonTracePath ? path.relative(root, anonTracePath).replace(/\\/g, "/") : null,
|
|
2112
|
+
auth: authTracePath ? path.relative(root, authTracePath).replace(/\\/g, "/") : null
|
|
2113
|
+
} : null,
|
|
2114
|
+
har: recordHar ? {
|
|
2115
|
+
directory: path.relative(root, harDir).replace(/\\/g, "/"),
|
|
2116
|
+
anon: path.join(harDir, 'anon-traffic.har'),
|
|
2117
|
+
auth: path.join(harDir, 'auth-traffic.har')
|
|
2118
|
+
} : null
|
|
2119
|
+
};
|
|
2120
|
+
|
|
2121
|
+
// Build report
|
|
2122
|
+
const report = {
|
|
2123
|
+
meta: {
|
|
2124
|
+
startedAt: new Date(startTime).toISOString(),
|
|
2125
|
+
finishedAt: new Date().toISOString(),
|
|
2126
|
+
durationMs: Date.now() - startTime,
|
|
2127
|
+
baseUrl,
|
|
2128
|
+
verifyAuth,
|
|
2129
|
+
maxPages,
|
|
2130
|
+
maxDepth,
|
|
2131
|
+
truthpackLoaded: !!tp,
|
|
2132
|
+
protectedMatcherCount: matchers.length,
|
|
2133
|
+
savedStorageState: savedStatePath ? path.relative(root, savedStatePath).replace(/\\/g, "/") : null,
|
|
2134
|
+
recordVideo,
|
|
2135
|
+
recordTrace,
|
|
2136
|
+
recordHar,
|
|
2137
|
+
// Flakiness/stability metadata
|
|
2138
|
+
stabilityRuns,
|
|
2139
|
+
flakyThreshold,
|
|
2140
|
+
filteredFlakyCount: stabilityRuns > 1 ? filteredFlakyCount : 0
|
|
2141
|
+
},
|
|
2142
|
+
artifacts,
|
|
2143
|
+
coverage,
|
|
2144
|
+
passes: { anon: anonPass, auth: authPass },
|
|
2145
|
+
findings,
|
|
2146
|
+
consoleErrors: [...anonPass.consoleErrors, ...(authPass?.consoleErrors || [])].slice(0, 50),
|
|
2147
|
+
networkErrors: [...anonPass.networkErrors, ...(authPass?.networkErrors || [])].slice(0, 50)
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
// Write reports
|
|
2151
|
+
fs.writeFileSync(path.join(outBase, "reality_report.json"), JSON.stringify(report, null, 2), "utf8");
|
|
2152
|
+
|
|
2153
|
+
const latestDir = path.join(root, ".vibecheck", "reality");
|
|
2154
|
+
ensureDir(latestDir);
|
|
2155
|
+
fs.writeFileSync(path.join(latestDir, "latest.json"), JSON.stringify({ latest: path.relative(root, outBase).replace(/\\/g, "/") }, null, 2));
|
|
2156
|
+
fs.writeFileSync(path.join(latestDir, "last_reality.json"), JSON.stringify(report, null, 2), "utf8");
|
|
2157
|
+
|
|
2158
|
+
const duration = Date.now() - startTime;
|
|
2159
|
+
|
|
2160
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2161
|
+
// SEND TO API (for dashboard replay)
|
|
2162
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2163
|
+
|
|
2164
|
+
// Build reality result for API (timeline, network, screenshots, deadUI, fakeSuccess)
|
|
2165
|
+
const realityResult = {
|
|
2166
|
+
timeline: buildTimeline(anonPass, authPass),
|
|
2167
|
+
network: buildNetworkTimeline(anonPass, authPass),
|
|
2168
|
+
screenshots: buildScreenshotsList(anonPass, authPass, root),
|
|
2169
|
+
deadUI: findings.filter(f => f.category === 'DeadUI').map((f, idx) => ({
|
|
2170
|
+
stepIndex: idx,
|
|
2171
|
+
selector: f.title || '',
|
|
2172
|
+
reason: f.reason || '',
|
|
2173
|
+
})),
|
|
2174
|
+
fakeSuccess: findings.filter(f => f.category === 'FakeResponse' || f.category === 'FakeDomain').map((f, idx) => ({
|
|
2175
|
+
stepIndex: idx,
|
|
2176
|
+
message: f.title || f.reason || '',
|
|
2177
|
+
actualStatus: 0, // Could be extracted from evidence
|
|
2178
|
+
})),
|
|
2179
|
+
};
|
|
2180
|
+
|
|
2181
|
+
// Try to send to API (non-blocking, doesn't fail the run)
|
|
2182
|
+
try {
|
|
2183
|
+
const { sendRunToApi, extractFindings, calculateScore } = require('../../packages/cli/dist/runtime/api-run-sender.js');
|
|
2184
|
+
|
|
2185
|
+
const runId = crypto.randomUUID ? crypto.randomUUID() : `reality-${Date.now()}`;
|
|
2186
|
+
const apiFindings = extractFindings ? extractFindings({ findings }) : findings.map(f => ({
|
|
2187
|
+
severity: f.severity?.toLowerCase() || 'medium',
|
|
2188
|
+
category: f.category || 'general',
|
|
2189
|
+
message: f.title || f.reason || 'Unknown issue',
|
|
2190
|
+
file: f.page,
|
|
2191
|
+
}));
|
|
2192
|
+
|
|
2193
|
+
const score = calculateScore ? calculateScore(verdict, apiFindings) : (verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 50 : 0);
|
|
2194
|
+
|
|
2195
|
+
sendRunToApi({
|
|
2196
|
+
runId,
|
|
2197
|
+
command: 'reality',
|
|
2198
|
+
status: 'completed',
|
|
2199
|
+
verdict,
|
|
2200
|
+
score,
|
|
2201
|
+
exitCode: blocks ? 2 : warns ? 1 : 0,
|
|
2202
|
+
startTime: new Date(startTime).toISOString(),
|
|
2203
|
+
endTime: new Date().toISOString(),
|
|
2204
|
+
duration,
|
|
2205
|
+
projectPath: root,
|
|
2206
|
+
findings: apiFindings,
|
|
2207
|
+
metadata: {
|
|
2208
|
+
baseUrl,
|
|
2209
|
+
pagesVisited: anonPass.pagesVisited.length + (authPass?.pagesVisited?.length || 0),
|
|
2210
|
+
coverage: coverage?.percent,
|
|
2211
|
+
},
|
|
2212
|
+
realityResult,
|
|
2213
|
+
videoUrl: anonVideoPath ? path.relative(root, anonVideoPath).replace(/\\/g, "/") : undefined,
|
|
2214
|
+
traceUrl: anonTracePath ? path.relative(root, anonTracePath).replace(/\\/g, "/") : undefined,
|
|
2215
|
+
}).catch(() => {
|
|
2216
|
+
// Silently ignore API errors - CLI works offline
|
|
2217
|
+
});
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
// Module not available or other error - continue without API sync
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2223
|
+
// OUTPUT
|
|
2224
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2225
|
+
|
|
2226
|
+
// Determine verdict
|
|
2227
|
+
const verdict = blocks > 0 ? 'BLOCK' : warns > 0 ? 'WARN' : 'SHIP';
|
|
2228
|
+
|
|
2229
|
+
// Use Mission Control format
|
|
2230
|
+
if (!argsOrOpts.json && !argsOrOpts.quiet) {
|
|
2231
|
+
const { formatRealityOutput } = require('./lib/reality-output');
|
|
2232
|
+
|
|
2233
|
+
// Build test results from findings
|
|
2234
|
+
const tests = findings.map(f => ({
|
|
2235
|
+
name: f.title || f.category || 'Test',
|
|
2236
|
+
route: f.page || f.url || '',
|
|
2237
|
+
passed: f.severity !== 'BLOCK',
|
|
2238
|
+
}));
|
|
2239
|
+
|
|
2240
|
+
console.log(formatRealityOutput({
|
|
2241
|
+
verdict,
|
|
2242
|
+
tests,
|
|
2243
|
+
passed: tests.filter(t => t.passed).length,
|
|
2244
|
+
failed: tests.filter(t => !t.passed).length,
|
|
2245
|
+
warnings: warns,
|
|
2246
|
+
coverage: coverage.percent || 0,
|
|
2247
|
+
duration,
|
|
2248
|
+
url: baseUrl,
|
|
2249
|
+
success: verdict === 'SHIP',
|
|
2250
|
+
}, { projectPath: root, version: 'v3.5.5' }));
|
|
2251
|
+
|
|
2252
|
+
// Show report path
|
|
2253
|
+
console.log(`\n ${colors.accent}Report:${c.reset} ${path.relative(root, outBase)}/reality_report.json`);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
process.exitCode = blocks ? 2 : warns ? 1 : 0;
|
|
2257
|
+
return process.exitCode;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
module.exports = { runReality };
|