@vibecheckai/cli 3.5.0 → 3.5.2
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/registry.js +214 -237
- package/bin/runners/cli-utils.js +33 -2
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/cursor.js +2 -49
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/analysis-core.js +25 -5
- package/bin/runners/lib/analyzers.js +431 -481
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- 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 +18 -218
- package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
- package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
- package/bin/runners/lib/engines/mock-data-engine.js +10 -53
- package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
- package/bin/runners/lib/engines/type-aware-engine.js +39 -263
- package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
- 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 +73 -373
- 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/entitlements-v2.js +73 -97
- package/bin/runners/lib/error-handler.js +44 -3
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/html-proof-report.js +700 -350
- package/bin/runners/lib/missions/plan.js +6 -46
- package/bin/runners/lib/missions/templates.js +0 -232
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/scan-output.js +91 -76
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -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/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +23 -23
- package/bin/runners/lib/ship-output.js +75 -31
- package/bin/runners/lib/terminal-ui.js +6 -113
- package/bin/runners/lib/truth.js +351 -10
- package/bin/runners/lib/unified-cli-output.js +430 -603
- package/bin/runners/lib/unified-output.js +13 -9
- package/bin/runners/runAIAgent.js +10 -5
- package/bin/runners/runAgent.js +0 -3
- package/bin/runners/runAllowlist.js +389 -0
- package/bin/runners/runApprove.js +0 -33
- package/bin/runners/runAuth.js +73 -45
- package/bin/runners/runCheckpoint.js +51 -11
- package/bin/runners/runClassify.js +85 -21
- package/bin/runners/runContext.js +0 -3
- package/bin/runners/runDoctor.js +41 -28
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.js +0 -3
- package/bin/runners/runFirewallHook.js +0 -3
- package/bin/runners/runFix.js +66 -76
- package/bin/runners/runGuard.js +18 -411
- package/bin/runners/runInit.js +113 -30
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runMcp.js +19 -25
- package/bin/runners/runPolish.js +64 -240
- package/bin/runners/runPromptFirewall.js +12 -5
- package/bin/runners/runProve.js +57 -22
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +59 -68
- package/bin/runners/runReport.js +38 -33
- package/bin/runners/runRuntime.js +8 -5
- package/bin/runners/runScan.js +1413 -190
- package/bin/runners/runShip.js +113 -719
- package/bin/runners/runTruth.js +0 -3
- package/bin/runners/runValidate.js +13 -9
- package/bin/runners/runWatch.js +23 -14
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +204 -185
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +554 -0
- package/mcp-server/index-v1.js +698 -0
- package/mcp-server/index.js +210 -238
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +499 -0
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +395 -0
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/package.json +12 -3
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +605 -0
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +378 -45
- package/mcp-server/tools-v3.js +353 -442
- package/mcp-server/tsconfig.json +37 -0
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/package.json +1 -1
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
- package/bin/runners/lib/audit-logger.js +0 -532
- package/bin/runners/lib/authority/authorities/architecture.js +0 -364
- package/bin/runners/lib/authority/authorities/compliance.js +0 -341
- package/bin/runners/lib/authority/authorities/human.js +0 -343
- package/bin/runners/lib/authority/authorities/quality.js +0 -420
- package/bin/runners/lib/authority/authorities/security.js +0 -228
- package/bin/runners/lib/authority/index.js +0 -293
- package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
- package/bin/runners/lib/cli-charts.js +0 -368
- package/bin/runners/lib/cli-config-display.js +0 -405
- package/bin/runners/lib/cli-demo.js +0 -275
- package/bin/runners/lib/cli-errors.js +0 -438
- package/bin/runners/lib/cli-help-formatter.js +0 -439
- package/bin/runners/lib/cli-interactive-menu.js +0 -509
- package/bin/runners/lib/cli-prompts.js +0 -441
- package/bin/runners/lib/cli-scan-cards.js +0 -362
- package/bin/runners/lib/compliance-reporter.js +0 -710
- package/bin/runners/lib/conductor/index.js +0 -671
- package/bin/runners/lib/easy/README.md +0 -123
- package/bin/runners/lib/easy/index.js +0 -140
- package/bin/runners/lib/easy/interactive-wizard.js +0 -788
- package/bin/runners/lib/easy/one-click-firewall.js +0 -564
- package/bin/runners/lib/easy/zero-config-reality.js +0 -714
- package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
- package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
- package/bin/runners/lib/engines/confidence-scoring.js +0 -276
- package/bin/runners/lib/engines/context-detection.js +0 -264
- package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
- package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
- package/bin/runners/lib/engines/env-variables-engine.js +0 -458
- package/bin/runners/lib/engines/error-handling-engine.js +0 -437
- package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
- package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
- package/bin/runners/lib/engines/framework-detection.js +0 -508
- package/bin/runners/lib/engines/import-order-engine.js +0 -429
- package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
- package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
- package/bin/runners/lib/engines/orchestrator.js +0 -334
- package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
- package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
- package/bin/runners/lib/enhanced-features/index.js +0 -305
- package/bin/runners/lib/enhanced-output.js +0 -631
- package/bin/runners/lib/enterprise.js +0 -300
- package/bin/runners/lib/firewall/command-validator.js +0 -351
- package/bin/runners/lib/firewall/config.js +0 -341
- package/bin/runners/lib/firewall/content-validator.js +0 -519
- package/bin/runners/lib/firewall/index.js +0 -101
- package/bin/runners/lib/firewall/path-validator.js +0 -256
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
- package/bin/runners/lib/mcp-utils.js +0 -425
- package/bin/runners/lib/output/index.js +0 -1022
- package/bin/runners/lib/policy-engine.js +0 -652
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
- package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
- package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
- package/bin/runners/lib/polish/autofix/index.js +0 -200
- package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
- package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
- package/bin/runners/lib/polish/backend-checks.js +0 -148
- package/bin/runners/lib/polish/documentation-checks.js +0 -111
- package/bin/runners/lib/polish/frontend-checks.js +0 -168
- package/bin/runners/lib/polish/index.js +0 -71
- package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
- package/bin/runners/lib/polish/library-detection.js +0 -175
- package/bin/runners/lib/polish/performance-checks.js +0 -100
- package/bin/runners/lib/polish/security-checks.js +0 -148
- package/bin/runners/lib/polish/utils.js +0 -203
- package/bin/runners/lib/prompt-builder.js +0 -540
- package/bin/runners/lib/proof-certificate.js +0 -634
- package/bin/runners/lib/reality/accessibility-audit.js +0 -946
- package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
- package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
- package/bin/runners/lib/reality/performance-tracker.js +0 -1077
- package/bin/runners/lib/reality/scenario-generator.js +0 -1404
- package/bin/runners/lib/reality/visual-regression.js +0 -852
- package/bin/runners/lib/reality-profiler.js +0 -717
- package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
- package/bin/runners/lib/review/ai-code-review.js +0 -832
- package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
- package/bin/runners/lib/sbom-generator.js +0 -641
- package/bin/runners/lib/scan-output-enhanced.js +0 -512
- package/bin/runners/lib/security/owasp-scanner.js +0 -939
- package/bin/runners/lib/validators/contract-validator.js +0 -283
- package/bin/runners/lib/validators/dead-export-detector.js +0 -279
- package/bin/runners/lib/validators/dep-audit.js +0 -245
- package/bin/runners/lib/validators/env-validator.js +0 -319
- package/bin/runners/lib/validators/index.js +0 -120
- package/bin/runners/lib/validators/license-checker.js +0 -252
- package/bin/runners/lib/validators/route-validator.js +0 -290
- package/bin/runners/runAuthority.js +0 -528
- package/bin/runners/runConductor.js +0 -772
- package/bin/runners/runContainer.js +0 -366
- package/bin/runners/runEasy.js +0 -410
- package/bin/runners/runIaC.js +0 -372
- package/bin/runners/runVibe.js +0 -791
- package/mcp-server/tools.js +0 -495
package/bin/runners/runScan.js
CHANGED
|
@@ -1,46 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vibecheck Scan -
|
|
2
|
+
* vibecheck Scan - Route Integrity & Code Analysis
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* - type-aware-engine (type safety)
|
|
9
|
-
* - error-handling-engine (reliability)
|
|
4
|
+
* The ultimate scanner combining:
|
|
5
|
+
* - Route integrity (dead links, orphan routes, coverage)
|
|
6
|
+
* - Security analysis (secrets, auth, vulnerabilities)
|
|
7
|
+
* - Code quality (mocks, placeholders, hygiene)
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Modes:
|
|
10
|
+
* - vibecheck scan: Layer 1 (AST) - Fast static analysis
|
|
11
|
+
* - vibecheck scan --truth: Layer 1+2 (+ build manifests) - CI/ship
|
|
12
|
+
* - vibecheck scan --reality --url <url>: Layer 1+2+3 (+ Playwright) - Full proof
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
const path = require("path");
|
|
17
16
|
const fs = require("fs");
|
|
18
17
|
const { withErrorHandling, createUserError } = require("./lib/error-handler");
|
|
19
|
-
const { enforceLimit, trackUsage
|
|
18
|
+
const { enforceLimit, trackUsage } = require("./lib/entitlements");
|
|
20
19
|
const { emitScanStart, emitScanComplete } = require("./lib/audit-bridge");
|
|
21
20
|
const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
|
|
21
|
+
const {
|
|
22
|
+
createScan,
|
|
23
|
+
updateScanProgress,
|
|
24
|
+
submitScanResults,
|
|
25
|
+
reportScanError,
|
|
26
|
+
isApiAvailable
|
|
27
|
+
} = require("./lib/api-client");
|
|
22
28
|
const { EXIT, verdictToExitCode } = require("./lib/exit-codes");
|
|
23
29
|
|
|
24
|
-
// Import orchestrator for engine management
|
|
25
|
-
const { runScanEngines, SCAN_ENGINES } = require("./lib/engines/orchestrator");
|
|
26
|
-
|
|
27
30
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
28
|
-
// TERMINAL UI
|
|
31
|
+
// ENHANCED TERMINAL UI & OUTPUT MODULES
|
|
29
32
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
33
|
|
|
31
34
|
const {
|
|
32
35
|
ansi,
|
|
33
36
|
colors,
|
|
34
37
|
Spinner,
|
|
38
|
+
PhaseProgress,
|
|
39
|
+
renderBanner,
|
|
40
|
+
renderSection,
|
|
35
41
|
formatDuration,
|
|
36
42
|
} = require("./lib/terminal-ui");
|
|
37
43
|
|
|
38
44
|
const {
|
|
45
|
+
formatScanOutput,
|
|
39
46
|
formatSARIF,
|
|
40
47
|
getExitCode,
|
|
48
|
+
printError,
|
|
49
|
+
EXIT_CODES,
|
|
41
50
|
calculateScore,
|
|
42
51
|
} = require("./lib/scan-output");
|
|
43
52
|
|
|
53
|
+
const {
|
|
54
|
+
enrichFindings,
|
|
55
|
+
saveBaseline,
|
|
56
|
+
} = require("./lib/fingerprint");
|
|
57
|
+
|
|
44
58
|
const BANNER = `
|
|
45
59
|
${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
|
|
46
60
|
${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
|
|
@@ -50,7 +64,7 @@ ${ansi.rgb(120, 120, 255)} ╚████╔╝ ██║██████
|
|
|
50
64
|
${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
|
|
51
65
|
|
|
52
66
|
${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
|
|
53
|
-
${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}
|
|
67
|
+
${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Route Integrity${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}Security${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Quality${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(100, 100, 100)}Ship with Confidence${ansi.reset} ${ansi.dim}│${ansi.reset}
|
|
54
68
|
${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
|
|
55
69
|
`;
|
|
56
70
|
|
|
@@ -58,32 +72,94 @@ function printBanner() {
|
|
|
58
72
|
console.log(BANNER);
|
|
59
73
|
}
|
|
60
74
|
|
|
75
|
+
// Legacy compatibility functions - now use enhanced modules
|
|
76
|
+
function printCoverageMap(coverageMap) {
|
|
77
|
+
const { renderCoverageMap } = require("./lib/scan-output");
|
|
78
|
+
console.log(renderCoverageMap(coverageMap));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function printBreakdown(breakdown) {
|
|
82
|
+
const { renderBreakdown } = require("./lib/scan-output");
|
|
83
|
+
console.log(renderBreakdown(breakdown));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function printBlockers(blockers) {
|
|
87
|
+
const { renderBlockers } = require("./lib/scan-output");
|
|
88
|
+
console.log(renderBlockers(blockers));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printLayers(layers) {
|
|
92
|
+
const { renderLayers } = require("./lib/scan-output");
|
|
93
|
+
console.log(renderLayers(layers));
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
62
97
|
// ARGS PARSER
|
|
63
98
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
64
99
|
|
|
65
100
|
function parseArgs(args) {
|
|
101
|
+
// Parse global flags first
|
|
66
102
|
const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
|
|
67
103
|
|
|
68
104
|
const opts = {
|
|
69
105
|
path: globalFlags.path || process.cwd(),
|
|
106
|
+
truth: false,
|
|
107
|
+
reality: false,
|
|
108
|
+
realitySniff: false,
|
|
109
|
+
baseUrl: null,
|
|
70
110
|
json: globalFlags.json || false,
|
|
71
111
|
sarif: false,
|
|
72
112
|
verbose: globalFlags.verbose || false,
|
|
73
113
|
help: globalFlags.help || false,
|
|
74
|
-
|
|
114
|
+
autofix: false,
|
|
115
|
+
save: true, // Always save results by default
|
|
75
116
|
noBanner: globalFlags.noBanner || false,
|
|
76
117
|
ci: globalFlags.ci || false,
|
|
77
118
|
quiet: globalFlags.quiet || false,
|
|
119
|
+
// Baseline tracking (fingerprints)
|
|
120
|
+
baseline: true, // Compare against baseline by default
|
|
121
|
+
updateBaseline: false, // --update-baseline to save current findings as baseline
|
|
122
|
+
// Allowlist subcommand
|
|
123
|
+
allowlist: null, // null = not using allowlist, or 'list' | 'add' | 'remove' | 'check'
|
|
124
|
+
allowlistId: null,
|
|
125
|
+
allowlistPattern: null,
|
|
126
|
+
allowlistReason: null,
|
|
127
|
+
allowlistScope: 'global',
|
|
128
|
+
allowlistFile: null,
|
|
129
|
+
allowlistExpires: null,
|
|
78
130
|
};
|
|
79
131
|
|
|
132
|
+
// Parse command-specific args from cleanArgs
|
|
80
133
|
for (let i = 0; i < cleanArgs.length; i++) {
|
|
81
134
|
const arg = cleanArgs[i];
|
|
82
135
|
|
|
83
|
-
if (arg === '--
|
|
136
|
+
if (arg === '--truth' || arg === '-t') opts.truth = true;
|
|
137
|
+
else if (arg === '--reality' || arg === '-r') { opts.reality = true; opts.truth = true; }
|
|
138
|
+
else if (arg === '--reality-sniff' || arg === '--sniff') opts.realitySniff = true;
|
|
139
|
+
else if (arg === '--url' || arg === '-u') { opts.baseUrl = cleanArgs[++i]; opts.reality = true; opts.truth = true; }
|
|
140
|
+
else if (arg === '--sarif') opts.sarif = true;
|
|
141
|
+
else if (arg === '--autofix' || arg === '--fix' || arg === '-f') opts.autofix = true;
|
|
84
142
|
else if (arg === '--no-save') opts.save = false;
|
|
143
|
+
else if (arg === '--no-baseline') opts.baseline = false;
|
|
144
|
+
else if (arg === '--update-baseline' || arg === '--set-baseline') opts.updateBaseline = true;
|
|
85
145
|
else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
|
|
86
146
|
else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
|
|
147
|
+
// Allowlist subcommand support
|
|
148
|
+
else if (arg === '--allowlist') {
|
|
149
|
+
const nextArg = cleanArgs[i + 1];
|
|
150
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
151
|
+
opts.allowlist = nextArg;
|
|
152
|
+
i++;
|
|
153
|
+
} else {
|
|
154
|
+
opts.allowlist = 'list'; // Default to list
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (arg === '--id' && opts.allowlist) opts.allowlistId = cleanArgs[++i];
|
|
158
|
+
else if (arg === '--pattern' && opts.allowlist) opts.allowlistPattern = cleanArgs[++i];
|
|
159
|
+
else if (arg === '--reason' && opts.allowlist) opts.allowlistReason = cleanArgs[++i];
|
|
160
|
+
else if (arg === '--scope' && opts.allowlist) opts.allowlistScope = cleanArgs[++i];
|
|
161
|
+
else if (arg === '--file' && opts.allowlist) opts.allowlistFile = cleanArgs[++i];
|
|
162
|
+
else if (arg === '--expires' && opts.allowlist) opts.allowlistExpires = cleanArgs[++i];
|
|
87
163
|
else if (!arg.startsWith('-')) opts.path = path.resolve(arg);
|
|
88
164
|
}
|
|
89
165
|
|
|
@@ -97,41 +173,70 @@ function printHelp(showBanner = true) {
|
|
|
97
173
|
console.log(`
|
|
98
174
|
${ansi.bold}USAGE${ansi.reset}
|
|
99
175
|
${colors.accent}vibecheck scan${ansi.reset} [path] [options]
|
|
176
|
+
|
|
177
|
+
${ansi.dim}Aliases: s, check${ansi.reset}
|
|
178
|
+
|
|
179
|
+
The core analysis engine. Scans your codebase for route integrity issues,
|
|
180
|
+
security vulnerabilities, code quality problems, and more.
|
|
181
|
+
|
|
182
|
+
${ansi.bold}SCAN LAYERS${ansi.reset}
|
|
183
|
+
${colors.accent}(default)${ansi.reset} Layer 1: AST static analysis ${ansi.dim}(fast, ~2s)${ansi.reset}
|
|
184
|
+
${colors.accent}--truth, -t${ansi.reset} Layer 1+2: + build manifest verification ${ansi.dim}(CI/ship)${ansi.reset}
|
|
185
|
+
${colors.accent}--reality, -r${ansi.reset} Layer 1+2+3: + Playwright runtime proof ${ansi.dim}[PRO]${ansi.reset}
|
|
186
|
+
${colors.accent}--reality-sniff${ansi.reset} Include Reality Sniff AI artifact detection
|
|
187
|
+
|
|
188
|
+
${ansi.bold}FIX MODE${ansi.reset} ${ansi.cyan}[STARTER]${ansi.reset}
|
|
189
|
+
${colors.accent}--autofix, -f${ansi.reset} Generate AI fix missions for detected issues
|
|
190
|
+
|
|
191
|
+
${ansi.bold}BASELINE TRACKING${ansi.reset}
|
|
192
|
+
${colors.accent}--baseline${ansi.reset} Compare against previous scan ${ansi.dim}(default: on)${ansi.reset}
|
|
193
|
+
${colors.accent}--no-baseline${ansi.reset} Skip baseline comparison
|
|
194
|
+
${colors.accent}--update-baseline${ansi.reset} Save current findings as new baseline
|
|
100
195
|
|
|
101
|
-
${ansi.bold}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
• error-handling-engine ${ansi.dim}(reliability)${ansi.reset}
|
|
196
|
+
${ansi.bold}ALLOWLIST (suppress false positives)${ansi.reset}
|
|
197
|
+
${colors.accent}--allowlist list${ansi.reset} Show all suppressed findings
|
|
198
|
+
${colors.accent}--allowlist add --id <id> --reason "..."${ansi.reset}
|
|
199
|
+
${colors.accent}--allowlist remove --id <id>${ansi.reset} Remove suppression
|
|
200
|
+
${colors.accent}--allowlist check --id <id>${ansi.reset} Check if suppressed
|
|
107
201
|
|
|
108
202
|
${ansi.bold}OUTPUT OPTIONS${ansi.reset}
|
|
109
|
-
${colors.accent}--json${ansi.reset} Output as JSON
|
|
110
|
-
${colors.accent}--sarif${ansi.reset} SARIF format (GitHub code scanning)
|
|
203
|
+
${colors.accent}--json${ansi.reset} Output as JSON ${ansi.dim}(machine-readable)${ansi.reset}
|
|
204
|
+
${colors.accent}--sarif${ansi.reset} SARIF format ${ansi.dim}(GitHub code scanning)${ansi.reset}
|
|
111
205
|
${colors.accent}--no-save${ansi.reset} Don't save results to .vibecheck/results/
|
|
112
206
|
${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
|
|
113
207
|
|
|
114
208
|
${ansi.bold}GLOBAL OPTIONS${ansi.reset}
|
|
115
209
|
${colors.accent}--path, -p <dir>${ansi.reset} Run in specified directory
|
|
116
210
|
${colors.accent}--quiet, -q${ansi.reset} Suppress non-essential output
|
|
117
|
-
${colors.accent}--ci${ansi.reset} CI mode (quiet + no-banner)
|
|
211
|
+
${colors.accent}--ci${ansi.reset} CI mode ${ansi.dim}(quiet + no-banner)${ansi.reset}
|
|
118
212
|
${colors.accent}--help, -h${ansi.reset} Show this help
|
|
119
213
|
|
|
120
214
|
${ansi.bold}💡 EXAMPLES${ansi.reset}
|
|
121
215
|
|
|
122
|
-
${ansi.dim}# Quick scan (
|
|
216
|
+
${ansi.dim}# Quick scan (most common)${ansi.reset}
|
|
123
217
|
vibecheck scan
|
|
124
218
|
|
|
125
|
-
${ansi.dim}#
|
|
126
|
-
vibecheck scan --
|
|
219
|
+
${ansi.dim}# Scan with AI fix missions${ansi.reset}
|
|
220
|
+
vibecheck scan --autofix
|
|
127
221
|
|
|
128
|
-
${ansi.dim}#
|
|
129
|
-
vibecheck scan
|
|
222
|
+
${ansi.dim}# Suppress a false positive${ansi.reset}
|
|
223
|
+
vibecheck scan --allowlist add --id R_DEAD_abc123 --reason "Feature toggle"
|
|
130
224
|
|
|
131
|
-
|
|
225
|
+
${ansi.dim}# CI pipeline (JSON output, strict)${ansi.reset}
|
|
226
|
+
vibecheck scan --ci --json > results.json
|
|
132
227
|
|
|
133
|
-
|
|
134
|
-
|
|
228
|
+
${ansi.dim}# Full reality proof (requires running app)${ansi.reset}
|
|
229
|
+
vibecheck scan --reality --url http://localhost:3000
|
|
230
|
+
|
|
231
|
+
${ansi.bold}📄 OUTPUT${ansi.reset}
|
|
232
|
+
Results: .vibecheck/results/latest.json
|
|
233
|
+
Missions: .vibecheck/missions/ ${ansi.dim}(with --autofix)${ansi.reset}
|
|
234
|
+
History: .vibecheck/results/history/
|
|
235
|
+
|
|
236
|
+
${ansi.bold}🔗 RELATED COMMANDS${ansi.reset}
|
|
237
|
+
${colors.accent}vibecheck ship${ansi.reset} Get final SHIP/WARN/BLOCK verdict
|
|
238
|
+
${colors.accent}vibecheck fix${ansi.reset} Apply AI-generated fixes ${ansi.cyan}[STARTER]${ansi.reset}
|
|
239
|
+
${colors.accent}vibecheck prove${ansi.reset} Full proof pipeline with evidence ${ansi.magenta}[PRO]${ansi.reset}
|
|
135
240
|
|
|
136
241
|
${ansi.dim}─────────────────────────────────────────────────────────────${ansi.reset}
|
|
137
242
|
${ansi.dim}Documentation: https://docs.vibecheckai.dev/cli/scan${ansi.reset}
|
|
@@ -139,7 +244,444 @@ function printHelp(showBanner = true) {
|
|
|
139
244
|
}
|
|
140
245
|
|
|
141
246
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
142
|
-
//
|
|
247
|
+
// ALLOWLIST MANAGEMENT (integrated from runAllowlist)
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
249
|
+
|
|
250
|
+
async function handleAllowlistCommand(opts) {
|
|
251
|
+
const root = opts.path || process.cwd();
|
|
252
|
+
|
|
253
|
+
// Load evidence-pack module for allowlist functions
|
|
254
|
+
let evidencePack;
|
|
255
|
+
try {
|
|
256
|
+
evidencePack = require("./lib/evidence-pack");
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.error(`${colors.error}✗${ansi.reset} Failed to load allowlist module: ${e.message}`);
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const action = opts.allowlist;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
switch (action) {
|
|
266
|
+
case "list": {
|
|
267
|
+
const allowlist = evidencePack.loadAllowlist(root);
|
|
268
|
+
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
console.log(JSON.stringify(allowlist, null, 2));
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!opts.quiet) {
|
|
275
|
+
console.log(`\n 📋 ${ansi.bold}Allowlist Entries${ansi.reset}\n`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!allowlist.entries || allowlist.entries.length === 0) {
|
|
279
|
+
console.log(` ${ansi.dim}No entries in allowlist.${ansi.reset}\n`);
|
|
280
|
+
console.log(` ${ansi.dim}Add entries with: vibecheck scan --allowlist add --id <id> --reason "..."${ansi.reset}\n`);
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const entry of allowlist.entries) {
|
|
285
|
+
const expireStr = entry.expiresAt
|
|
286
|
+
? `${ansi.dim}expires ${new Date(entry.expiresAt).toLocaleDateString()}${ansi.reset}`
|
|
287
|
+
: '';
|
|
288
|
+
const scopeStr = entry.scope !== 'global'
|
|
289
|
+
? `${colors.accent}[${entry.scope}]${ansi.reset} `
|
|
290
|
+
: '';
|
|
291
|
+
|
|
292
|
+
console.log(` ${colors.success}✓${ansi.reset} ${ansi.bold}${entry.id}${ansi.reset} ${scopeStr}${expireStr}`);
|
|
293
|
+
|
|
294
|
+
if (entry.findingId) {
|
|
295
|
+
console.log(` ${ansi.dim}Finding:${ansi.reset} ${entry.findingId}`);
|
|
296
|
+
}
|
|
297
|
+
if (entry.pattern) {
|
|
298
|
+
console.log(` ${ansi.dim}Pattern:${ansi.reset} ${entry.pattern}`);
|
|
299
|
+
}
|
|
300
|
+
console.log(` ${ansi.dim}Reason:${ansi.reset} ${entry.reason || 'No reason provided'}`);
|
|
301
|
+
console.log(` ${ansi.dim}Added:${ansi.reset} ${entry.addedAt} by ${entry.addedBy || 'user'}`);
|
|
302
|
+
console.log();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(` ${ansi.dim}Total: ${allowlist.entries.length} entries${ansi.reset}\n`);
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case "add": {
|
|
310
|
+
if (!opts.allowlistId && !opts.allowlistPattern) {
|
|
311
|
+
console.error(`\n ${colors.error}✗${ansi.reset} Either --id or --pattern is required\n`);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!opts.allowlistReason) {
|
|
316
|
+
console.error(`\n ${colors.error}✗${ansi.reset} --reason is required\n`);
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const entry = {
|
|
321
|
+
findingId: opts.allowlistId,
|
|
322
|
+
pattern: opts.allowlistPattern,
|
|
323
|
+
reason: opts.allowlistReason,
|
|
324
|
+
scope: opts.allowlistScope,
|
|
325
|
+
addedBy: 'cli'
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (opts.allowlistFile) entry.file = opts.allowlistFile;
|
|
329
|
+
if (opts.allowlistExpires) {
|
|
330
|
+
const days = parseInt(opts.allowlistExpires, 10);
|
|
331
|
+
entry.expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const added = evidencePack.addToAllowlist(root, entry);
|
|
335
|
+
|
|
336
|
+
if (opts.json) {
|
|
337
|
+
console.log(JSON.stringify({ added }, null, 2));
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(`\n ${colors.success}+${ansi.reset} Added allowlist entry: ${ansi.bold}${added.id}${ansi.reset}\n`);
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
case "remove": {
|
|
346
|
+
if (!opts.allowlistId) {
|
|
347
|
+
console.error(`\n ${colors.error}✗${ansi.reset} --id is required for remove\n`);
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const allowlist = evidencePack.loadAllowlist(root);
|
|
352
|
+
const before = allowlist.entries.length;
|
|
353
|
+
allowlist.entries = allowlist.entries.filter(e => e.id !== opts.allowlistId && e.findingId !== opts.allowlistId);
|
|
354
|
+
const removed = before - allowlist.entries.length;
|
|
355
|
+
|
|
356
|
+
if (removed > 0) {
|
|
357
|
+
evidencePack.saveAllowlist(root, allowlist);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (opts.json) {
|
|
361
|
+
console.log(JSON.stringify({ removed, remaining: allowlist.entries.length }, null, 2));
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (removed > 0) {
|
|
366
|
+
console.log(`\n ${colors.success}-${ansi.reset} Removed ${removed} entry from allowlist\n`);
|
|
367
|
+
} else {
|
|
368
|
+
console.log(`\n ${colors.warning}⚠${ansi.reset} Entry not found: ${opts.allowlistId}\n`);
|
|
369
|
+
}
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case "check": {
|
|
374
|
+
if (!opts.allowlistId) {
|
|
375
|
+
console.error(`\n ${colors.error}✗${ansi.reset} --id is required for check\n`);
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const allowlist = evidencePack.loadAllowlist(root);
|
|
380
|
+
const result = evidencePack.isAllowlisted({ id: opts.allowlistId }, allowlist);
|
|
381
|
+
|
|
382
|
+
if (opts.json) {
|
|
383
|
+
console.log(JSON.stringify(result, null, 2));
|
|
384
|
+
return result.allowed ? 0 : 1;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (result.allowed) {
|
|
388
|
+
console.log(`\n ${colors.success}✓${ansi.reset} ${ansi.bold}Allowlisted${ansi.reset}`);
|
|
389
|
+
console.log(` ${ansi.dim}Reason:${ansi.reset} ${result.reason}`);
|
|
390
|
+
console.log(` ${ansi.dim}Entry:${ansi.reset} ${result.entry?.id}\n`);
|
|
391
|
+
} else {
|
|
392
|
+
console.log(`\n ${colors.warning}⚠${ansi.reset} ${ansi.bold}Not allowlisted${ansi.reset}\n`);
|
|
393
|
+
}
|
|
394
|
+
return result.allowed ? 0 : 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
default:
|
|
398
|
+
console.error(`\n ${colors.error}✗${ansi.reset} Unknown allowlist action: ${action}\n`);
|
|
399
|
+
console.log(` ${ansi.dim}Available: list, add, remove, check${ansi.reset}\n`);
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (opts.json) {
|
|
404
|
+
console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
405
|
+
} else {
|
|
406
|
+
console.error(`\n ${colors.error}✗${ansi.reset} ${error.message}\n`);
|
|
407
|
+
}
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
413
|
+
// AUTOFIX & MISSION GENERATION
|
|
414
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Normalize severity values to standard format
|
|
418
|
+
*/
|
|
419
|
+
function normalizeSeverity(severity) {
|
|
420
|
+
if (!severity) return 'medium';
|
|
421
|
+
const sev = String(severity).toLowerCase();
|
|
422
|
+
if (sev === 'block' || sev === 'critical') return 'critical';
|
|
423
|
+
if (sev === 'high') return 'high';
|
|
424
|
+
if (sev === 'warn' || sev === 'warning' || sev === 'medium') return 'medium';
|
|
425
|
+
if (sev === 'info' || sev === 'low') return 'low';
|
|
426
|
+
return 'medium'; // Default fallback
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function generateMissions(findings, projectPath, opts) {
|
|
430
|
+
const missionsDir = path.join(projectPath, '.vibecheck', 'missions');
|
|
431
|
+
|
|
432
|
+
// Ensure missions directory exists
|
|
433
|
+
if (!fs.existsSync(missionsDir)) {
|
|
434
|
+
fs.mkdirSync(missionsDir, { recursive: true });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const missions = [];
|
|
438
|
+
const missionIndex = [];
|
|
439
|
+
|
|
440
|
+
for (let i = 0; i < findings.length; i++) {
|
|
441
|
+
const finding = findings[i];
|
|
442
|
+
const missionId = `mission-${String(i + 1).padStart(3, '0')}`;
|
|
443
|
+
const normalizedSeverity = normalizeSeverity(finding.severity);
|
|
444
|
+
|
|
445
|
+
// Generate AI-ready mission prompt
|
|
446
|
+
const mission = {
|
|
447
|
+
id: missionId,
|
|
448
|
+
createdAt: new Date().toISOString(),
|
|
449
|
+
finding: {
|
|
450
|
+
id: finding.id || `finding-${i + 1}`,
|
|
451
|
+
severity: normalizedSeverity,
|
|
452
|
+
originalSeverity: finding.severity, // Keep original for reference
|
|
453
|
+
category: finding.category,
|
|
454
|
+
title: finding.title || finding.message,
|
|
455
|
+
file: finding.file,
|
|
456
|
+
line: finding.line,
|
|
457
|
+
},
|
|
458
|
+
prompt: generateMissionPrompt(finding),
|
|
459
|
+
constraints: generateConstraints(finding),
|
|
460
|
+
verification: generateVerificationSteps(finding),
|
|
461
|
+
status: 'pending',
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
missions.push(mission);
|
|
465
|
+
missionIndex.push({
|
|
466
|
+
id: missionId,
|
|
467
|
+
severity: normalizedSeverity,
|
|
468
|
+
title: finding.title || finding.message,
|
|
469
|
+
file: finding.file,
|
|
470
|
+
status: 'pending',
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Save individual mission JSON
|
|
474
|
+
const missionPath = path.join(missionsDir, `${missionId}.json`);
|
|
475
|
+
fs.writeFileSync(missionPath, JSON.stringify(mission, null, 2));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Generate MISSIONS.md index
|
|
479
|
+
const missionsMarkdown = generateMissionsMarkdown(missionIndex, projectPath);
|
|
480
|
+
fs.writeFileSync(path.join(missionsDir, 'MISSIONS.md'), missionsMarkdown);
|
|
481
|
+
|
|
482
|
+
// Save missions summary
|
|
483
|
+
const summary = {
|
|
484
|
+
generatedAt: new Date().toISOString(),
|
|
485
|
+
totalMissions: missions.length,
|
|
486
|
+
bySeverity: {
|
|
487
|
+
critical: missionIndex.filter(m => m.severity === 'critical').length,
|
|
488
|
+
high: missionIndex.filter(m => m.severity === 'high').length,
|
|
489
|
+
medium: missionIndex.filter(m => m.severity === 'medium').length,
|
|
490
|
+
low: missionIndex.filter(m => m.severity === 'low').length,
|
|
491
|
+
},
|
|
492
|
+
missions: missionIndex,
|
|
493
|
+
};
|
|
494
|
+
fs.writeFileSync(path.join(missionsDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
|
495
|
+
|
|
496
|
+
return { missions, summary };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function generateMissionPrompt(finding) {
|
|
500
|
+
const file = finding.file || 'unknown file';
|
|
501
|
+
const line = finding.line ? ` at line ${finding.line}` : '';
|
|
502
|
+
const title = finding.title || finding.message || 'Unknown issue';
|
|
503
|
+
const category = finding.category || 'general';
|
|
504
|
+
|
|
505
|
+
let prompt = `## Mission: Fix ${title}
|
|
506
|
+
|
|
507
|
+
**File:** \`${file}\`${line}
|
|
508
|
+
**Category:** ${category}
|
|
509
|
+
**Severity:** ${finding.severity || 'medium'}
|
|
510
|
+
|
|
511
|
+
### Problem
|
|
512
|
+
${finding.message || title}
|
|
513
|
+
|
|
514
|
+
### Context
|
|
515
|
+
`;
|
|
516
|
+
|
|
517
|
+
// Add category-specific context
|
|
518
|
+
if (category === 'ROUTE' || category === 'routes') {
|
|
519
|
+
prompt += `This is a route integrity issue. The route may be:
|
|
520
|
+
- Referenced but not defined
|
|
521
|
+
- Defined but not reachable
|
|
522
|
+
- Missing proper error handling
|
|
523
|
+
`;
|
|
524
|
+
} else if (category === 'ENV' || category === 'env') {
|
|
525
|
+
prompt += `This is an environment variable issue. The variable may be:
|
|
526
|
+
- Used but not defined in .env
|
|
527
|
+
- Missing from .env.example
|
|
528
|
+
- Not documented
|
|
529
|
+
`;
|
|
530
|
+
} else if (category === 'AUTH' || category === 'auth' || category === 'security') {
|
|
531
|
+
prompt += `This is a security/authentication issue. Check for:
|
|
532
|
+
- Proper authentication middleware
|
|
533
|
+
- Authorization checks
|
|
534
|
+
- Secure defaults
|
|
535
|
+
`;
|
|
536
|
+
} else if (category === 'MOCK' || category === 'mock') {
|
|
537
|
+
prompt += `This is a mock/placeholder issue. The code may contain:
|
|
538
|
+
- TODO comments that need implementation
|
|
539
|
+
- Placeholder data that should be real
|
|
540
|
+
- Fake success responses
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
prompt += `
|
|
545
|
+
### Requirements
|
|
546
|
+
1. Fix the issue while maintaining existing functionality
|
|
547
|
+
2. Follow the project's coding style and patterns
|
|
548
|
+
3. Add appropriate error handling
|
|
549
|
+
4. Update tests if they exist
|
|
550
|
+
|
|
551
|
+
### Verification
|
|
552
|
+
After making changes:
|
|
553
|
+
1. Run \`vibecheck scan\` to verify the issue is resolved
|
|
554
|
+
2. Run any existing tests for the affected file
|
|
555
|
+
3. Manually verify the functionality if applicable
|
|
556
|
+
`;
|
|
557
|
+
|
|
558
|
+
if (finding.fix || finding.fixSuggestion) {
|
|
559
|
+
prompt += `
|
|
560
|
+
### Suggested Fix
|
|
561
|
+
${finding.fix || finding.fixSuggestion}
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return prompt;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function generateConstraints(finding) {
|
|
569
|
+
const constraints = [
|
|
570
|
+
'Do not break existing functionality',
|
|
571
|
+
'Follow existing code patterns in the project',
|
|
572
|
+
'Add error handling where appropriate',
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
if (finding.category === 'security' || finding.category === 'AUTH') {
|
|
576
|
+
constraints.push('Ensure no security regressions');
|
|
577
|
+
constraints.push('Follow OWASP security guidelines');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (finding.category === 'ROUTE' || finding.category === 'routes') {
|
|
581
|
+
constraints.push('Maintain API backwards compatibility');
|
|
582
|
+
constraints.push('Update route documentation if changed');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return constraints;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function generateVerificationSteps(finding) {
|
|
589
|
+
const steps = [
|
|
590
|
+
'Run `vibecheck scan` and confirm this issue no longer appears',
|
|
591
|
+
'Run `vibecheck checkpoint` to ensure no regressions',
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
if (finding.file) {
|
|
595
|
+
steps.push(`Review changes in \`${finding.file}\``);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (finding.category === 'ROUTE' || finding.category === 'routes') {
|
|
599
|
+
steps.push('Test the affected route(s) manually or with automated tests');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (finding.category === 'ENV' || finding.category === 'env') {
|
|
603
|
+
steps.push('Verify environment variable is properly documented');
|
|
604
|
+
steps.push('Check .env.example is updated');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return steps;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function generateMissionsMarkdown(missionIndex, projectPath) {
|
|
611
|
+
const projectName = path.basename(projectPath);
|
|
612
|
+
const now = new Date().toISOString();
|
|
613
|
+
|
|
614
|
+
let md = `# Vibecheck Missions
|
|
615
|
+
|
|
616
|
+
> Generated: ${now}
|
|
617
|
+
> Project: ${projectName}
|
|
618
|
+
|
|
619
|
+
## Summary
|
|
620
|
+
|
|
621
|
+
| Severity | Count |
|
|
622
|
+
|----------|-------|
|
|
623
|
+
| Critical | ${missionIndex.filter(m => m.severity === 'critical').length} |
|
|
624
|
+
| High | ${missionIndex.filter(m => m.severity === 'high').length} |
|
|
625
|
+
| Medium | ${missionIndex.filter(m => m.severity === 'medium').length} |
|
|
626
|
+
| Low | ${missionIndex.filter(m => m.severity === 'low').length} |
|
|
627
|
+
| **Total** | **${missionIndex.length}** |
|
|
628
|
+
|
|
629
|
+
## Missions
|
|
630
|
+
|
|
631
|
+
`;
|
|
632
|
+
|
|
633
|
+
// Group by severity
|
|
634
|
+
const bySeverity = {
|
|
635
|
+
critical: missionIndex.filter(m => m.severity === 'critical'),
|
|
636
|
+
high: missionIndex.filter(m => m.severity === 'high'),
|
|
637
|
+
medium: missionIndex.filter(m => m.severity === 'medium'),
|
|
638
|
+
low: missionIndex.filter(m => m.severity === 'low'),
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
for (const [severity, missions] of Object.entries(bySeverity)) {
|
|
642
|
+
if (missions.length === 0) continue;
|
|
643
|
+
|
|
644
|
+
const emoji = severity === 'critical' ? '🔴' : severity === 'high' ? '🟠' : severity === 'medium' ? '🟡' : '🔵';
|
|
645
|
+
md += `### ${emoji} ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${missions.length})\n\n`;
|
|
646
|
+
|
|
647
|
+
for (const mission of missions) {
|
|
648
|
+
const checkbox = mission.status === 'completed' ? '[x]' : '[ ]';
|
|
649
|
+
md += `- ${checkbox} **${mission.id}**: ${mission.title}\n`;
|
|
650
|
+
if (mission.file) {
|
|
651
|
+
md += ` - File: \`${mission.file}\`\n`;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
md += '\n';
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
md += `---
|
|
658
|
+
|
|
659
|
+
## How to Use
|
|
660
|
+
|
|
661
|
+
1. **Review missions**: Read each mission file in \`.vibecheck/missions/\`
|
|
662
|
+
2. **Copy prompts**: Use the prompt in each mission file with your AI assistant
|
|
663
|
+
3. **Verify fixes**: Run \`vibecheck scan\` after each fix
|
|
664
|
+
4. **Track progress**: Update mission status in this file
|
|
665
|
+
|
|
666
|
+
## Commands
|
|
667
|
+
|
|
668
|
+
\`\`\`bash
|
|
669
|
+
# Re-scan after fixes
|
|
670
|
+
vibecheck scan
|
|
671
|
+
|
|
672
|
+
# Check progress
|
|
673
|
+
vibecheck checkpoint
|
|
674
|
+
|
|
675
|
+
# Final verification
|
|
676
|
+
vibecheck ship
|
|
677
|
+
\`\`\`
|
|
678
|
+
`;
|
|
679
|
+
|
|
680
|
+
return md;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
684
|
+
// MAIN SCAN FUNCTION - ROUTE INTEGRITY SYSTEM
|
|
143
685
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
144
686
|
|
|
145
687
|
async function runScan(args) {
|
|
@@ -151,248 +693,929 @@ async function runScan(args) {
|
|
|
151
693
|
return 0;
|
|
152
694
|
}
|
|
153
695
|
|
|
154
|
-
//
|
|
696
|
+
// CRITICAL: Check for JSON/SARIF output FIRST - skip all banners/output if these flags are set
|
|
697
|
+
if (opts.json || opts.sarif) {
|
|
698
|
+
process.env.NO_COLOR = '1';
|
|
699
|
+
// Skip all banner/output until we output JSON/SARIF
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Handle --allowlist subcommand
|
|
703
|
+
if (opts.allowlist) {
|
|
704
|
+
return await handleAllowlistCommand(opts);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Entitlement check (graceful offline handling)
|
|
155
708
|
try {
|
|
156
709
|
await enforceLimit('scans');
|
|
157
710
|
await trackUsage('scans');
|
|
158
711
|
} catch (err) {
|
|
159
712
|
if (err.code === 'LIMIT_EXCEEDED') {
|
|
160
|
-
|
|
713
|
+
if (!opts.json && !opts.sarif) {
|
|
714
|
+
console.error(err.upgradePrompt || err.message);
|
|
715
|
+
}
|
|
161
716
|
return 1;
|
|
162
717
|
}
|
|
163
|
-
// Network
|
|
164
|
-
if (
|
|
165
|
-
|
|
718
|
+
// Network error - fall back to free tier only (SECURITY: never grant paid features offline)
|
|
719
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND' || err.name === 'NetworkError') {
|
|
720
|
+
if (!opts.json && !opts.sarif) {
|
|
721
|
+
console.warn(` ${colors.warning}⚠${ansi.reset} API unavailable, running in ${colors.success}FREE${ansi.reset} tier mode`);
|
|
722
|
+
console.warn(` ${ansi.dim}Paid features require API connection. Continuing with free features only.${ansi.reset}\n`);
|
|
723
|
+
}
|
|
724
|
+
// Continue with free tier features only - scan command is free tier
|
|
725
|
+
} else {
|
|
726
|
+
throw err; // Re-throw unexpected errors
|
|
166
727
|
}
|
|
167
728
|
}
|
|
168
729
|
|
|
169
|
-
// Print banner
|
|
170
|
-
if (shouldShowBanner(opts)) {
|
|
730
|
+
// Print banner (respects --no-banner, VIBECHECK_NO_BANNER, --ci, --quiet, --json, --sarif)
|
|
731
|
+
if (shouldShowBanner(opts) && !opts.json && !opts.sarif) {
|
|
171
732
|
printBanner();
|
|
172
733
|
}
|
|
173
734
|
|
|
174
735
|
const projectPath = path.resolve(opts.path);
|
|
175
736
|
const startTime = Date.now();
|
|
176
|
-
const projectName = path.basename(projectPath);
|
|
177
737
|
|
|
178
|
-
// Emit audit event
|
|
738
|
+
// Emit audit event for scan start
|
|
179
739
|
emitScanStart(projectPath, args);
|
|
740
|
+
const projectName = path.basename(projectPath);
|
|
741
|
+
|
|
742
|
+
// Initialize API integration
|
|
743
|
+
let apiScan = null;
|
|
744
|
+
let apiConnected = false;
|
|
745
|
+
|
|
746
|
+
// Try to connect to API for dashboard integration
|
|
747
|
+
try {
|
|
748
|
+
apiConnected = await isApiAvailable();
|
|
749
|
+
if (apiConnected) {
|
|
750
|
+
// Create scan record in dashboard
|
|
751
|
+
apiScan = await createScan({
|
|
752
|
+
localPath: projectPath,
|
|
753
|
+
branch: opts.branch || 'main',
|
|
754
|
+
enableLLM: opts.llm || false,
|
|
755
|
+
});
|
|
756
|
+
if (!opts.json && !opts.sarif) {
|
|
757
|
+
console.log(`${colors.info}📡${ansi.reset} Connected to dashboard (Scan ID: ${apiScan.scanId})`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
// API connection is optional, continue without it
|
|
762
|
+
if (!opts.json && !opts.sarif) {
|
|
763
|
+
console.log(`${colors.dim}ℹ${ansi.reset} Dashboard integration unavailable`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
180
766
|
|
|
181
767
|
// Validate project path
|
|
182
768
|
if (!fs.existsSync(projectPath)) {
|
|
183
769
|
throw createUserError(`Project path does not exist: ${projectPath}`, "ValidationError");
|
|
184
770
|
}
|
|
185
771
|
|
|
186
|
-
//
|
|
187
|
-
|
|
772
|
+
// Determine layers
|
|
773
|
+
const layers = {
|
|
774
|
+
ast: true,
|
|
775
|
+
truth: opts.truth,
|
|
776
|
+
reality: opts.reality,
|
|
777
|
+
realitySniff: opts.realitySniff,
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Print scan info (skip if JSON/SARIF mode)
|
|
781
|
+
if (!opts.json && !opts.sarif) {
|
|
782
|
+
const layerNames = [];
|
|
783
|
+
if (layers.ast) layerNames.push('AST');
|
|
784
|
+
if (layers.truth) layerNames.push('Truth');
|
|
785
|
+
if (layers.reality) layerNames.push('Reality');
|
|
786
|
+
if (layers.realitySniff) layerNames.push('Reality Sniff');
|
|
787
|
+
|
|
188
788
|
console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${projectName}${ansi.reset}`);
|
|
189
789
|
console.log(` ${ansi.dim}Path:${ansi.reset} ${projectPath}`);
|
|
190
|
-
console.log(` ${ansi.dim}
|
|
790
|
+
console.log(` ${ansi.dim}Layers:${ansi.reset} ${colors.accent}${layerNames.join(' → ')}${ansi.reset}`);
|
|
191
791
|
console.log();
|
|
192
792
|
}
|
|
193
793
|
|
|
794
|
+
// Reality layer requires URL
|
|
795
|
+
if (opts.reality && !opts.baseUrl) {
|
|
796
|
+
if (!opts.json && !opts.sarif) {
|
|
797
|
+
console.log(` ${colors.warning}⚠${ansi.reset} ${ansi.bold}Reality layer requires --url${ansi.reset}`);
|
|
798
|
+
console.log(` ${ansi.dim}Example: vibecheck scan --reality --url http://localhost:3000${ansi.reset}`);
|
|
799
|
+
console.log();
|
|
800
|
+
}
|
|
801
|
+
return EXIT.USER_ERROR;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Initialize spinner outside try block for error handling
|
|
194
805
|
let spinner = null;
|
|
195
806
|
|
|
196
807
|
try {
|
|
197
|
-
//
|
|
198
|
-
|
|
808
|
+
// Import systems - try TypeScript compiled first, fallback to JS runtime
|
|
809
|
+
let scanRouteIntegrity;
|
|
810
|
+
let useFallbackScanner = false;
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
scanRouteIntegrity = require('../../dist/lib/route-integrity').scanRouteIntegrity;
|
|
814
|
+
} catch (e) {
|
|
815
|
+
// Fallback to JS-based scanner using truth.js and analyzers.js
|
|
816
|
+
useFallbackScanner = true;
|
|
817
|
+
const { buildTruthpackSmart } = require('./lib/truth');
|
|
818
|
+
const {
|
|
819
|
+
findMissingRoutes,
|
|
820
|
+
findEnvGaps,
|
|
821
|
+
findFakeSuccess,
|
|
822
|
+
findGhostAuth,
|
|
823
|
+
findMockData,
|
|
824
|
+
findTodoFixme,
|
|
825
|
+
findConsoleLogs,
|
|
826
|
+
findHardcodedSecrets,
|
|
827
|
+
findDeadCode,
|
|
828
|
+
findDeprecatedApis,
|
|
829
|
+
findEmptyCatch,
|
|
830
|
+
findUnsafeRegex,
|
|
831
|
+
findSecurityVulnerabilities,
|
|
832
|
+
findPerformanceIssues,
|
|
833
|
+
findCodeQualityIssues,
|
|
834
|
+
findCrossFileIssues,
|
|
835
|
+
findTypeSafetyIssues,
|
|
836
|
+
findAccessibilityIssues,
|
|
837
|
+
findAPIConsistencyIssues,
|
|
838
|
+
// NEW: AI Hallucination Detectors
|
|
839
|
+
findOptimisticNoRollback,
|
|
840
|
+
findSilentCatch,
|
|
841
|
+
findMethodMismatch,
|
|
842
|
+
findDeadUI,
|
|
843
|
+
clearFileCache, // V3: Memory management
|
|
844
|
+
} = require('./lib/analyzers');
|
|
845
|
+
|
|
846
|
+
scanRouteIntegrity = async function({ projectPath, layers, baseUrl, verbose }) {
|
|
847
|
+
// Build truthpack for route analysis
|
|
848
|
+
const truthpack = await buildTruthpackSmart({ repoRoot: projectPath });
|
|
849
|
+
|
|
850
|
+
// Run ALL analyzers for comprehensive scanning
|
|
851
|
+
const findings = [];
|
|
852
|
+
|
|
853
|
+
// Core analyzers (route integrity, env, auth)
|
|
854
|
+
findings.push(...findMissingRoutes(truthpack));
|
|
855
|
+
findings.push(...findMethodMismatch(truthpack));
|
|
856
|
+
findings.push(...findEnvGaps(truthpack));
|
|
857
|
+
findings.push(...findFakeSuccess(projectPath));
|
|
858
|
+
findings.push(...findOptimisticNoRollback(projectPath));
|
|
859
|
+
findings.push(...findSilentCatch(projectPath));
|
|
860
|
+
findings.push(...findDeadUI(projectPath));
|
|
861
|
+
findings.push(...findGhostAuth(truthpack, projectPath));
|
|
862
|
+
|
|
863
|
+
// Code quality analyzers (MOCK, TODO, console.log, etc.)
|
|
864
|
+
findings.push(...findMockData(projectPath));
|
|
865
|
+
findings.push(...findTodoFixme(projectPath));
|
|
866
|
+
findings.push(...findConsoleLogs(projectPath));
|
|
867
|
+
findings.push(...findHardcodedSecrets(projectPath));
|
|
868
|
+
findings.push(...findDeadCode(projectPath));
|
|
869
|
+
findings.push(...findDeprecatedApis(projectPath));
|
|
870
|
+
findings.push(...findEmptyCatch(projectPath));
|
|
871
|
+
findings.push(...findUnsafeRegex(projectPath));
|
|
872
|
+
|
|
873
|
+
// Enhanced analyzers (Security, Performance, Code Quality)
|
|
874
|
+
findings.push(...findSecurityVulnerabilities(projectPath));
|
|
875
|
+
findings.push(...findPerformanceIssues(projectPath));
|
|
876
|
+
findings.push(...findCodeQualityIssues(projectPath));
|
|
877
|
+
|
|
878
|
+
// V3: Clear file cache and AST cache to prevent memory leaks in large monorepos
|
|
879
|
+
clearFileCache();
|
|
880
|
+
const engines = require("./lib/engines/vibecheck-engines");
|
|
881
|
+
if (engines.clearASTCache) {
|
|
882
|
+
engines.clearASTCache();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Convert to scan format matching TypeScript scanner output
|
|
886
|
+
const shipBlockers = findings.map((f, i) => ({
|
|
887
|
+
id: f.id || `finding-${i}`,
|
|
888
|
+
ruleId: f.category,
|
|
889
|
+
category: f.category,
|
|
890
|
+
severity: f.severity === 'BLOCK' ? 'critical' : f.severity === 'WARN' ? 'warning' : 'info',
|
|
891
|
+
title: f.title,
|
|
892
|
+
message: f.title,
|
|
893
|
+
description: f.why,
|
|
894
|
+
file: f.evidence?.[0]?.file || '',
|
|
895
|
+
line: parseInt(f.evidence?.[0]?.lines?.split('-')[0]) || 1,
|
|
896
|
+
evidence: f.evidence || [],
|
|
897
|
+
fixHints: f.fixHints || [],
|
|
898
|
+
autofixAvailable: false,
|
|
899
|
+
verdict: f.severity === 'BLOCK' ? 'FAIL' : 'WARN',
|
|
900
|
+
}));
|
|
901
|
+
|
|
902
|
+
// Return structure matching TypeScript scanner
|
|
903
|
+
return {
|
|
904
|
+
report: {
|
|
905
|
+
shipBlockers,
|
|
906
|
+
realitySniffFindings: [],
|
|
907
|
+
routeMap: truthpack.routes,
|
|
908
|
+
summary: {
|
|
909
|
+
total: shipBlockers.length,
|
|
910
|
+
critical: shipBlockers.filter(f => f.severity === 'critical').length,
|
|
911
|
+
warning: shipBlockers.filter(f => f.severity === 'warning').length,
|
|
912
|
+
},
|
|
913
|
+
verdict: shipBlockers.some(f => f.severity === 'critical') ? 'BLOCK' :
|
|
914
|
+
shipBlockers.some(f => f.severity === 'warning') ? 'WARN' : 'SHIP'
|
|
915
|
+
},
|
|
916
|
+
outputPaths: {},
|
|
917
|
+
truthpack
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Try to import new unified output system (may not be compiled yet)
|
|
923
|
+
let buildVerdictOutput, normalizeFinding, formatStandardOutput, formatScanOutputFromUnified, getExitCodeFromUnified, CacheManager;
|
|
924
|
+
let useUnifiedOutput = false;
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const outputContract = require('../../dist/lib/cli/output-contract');
|
|
928
|
+
buildVerdictOutput = outputContract.buildVerdictOutput;
|
|
929
|
+
normalizeFinding = outputContract.normalizeFinding;
|
|
930
|
+
formatStandardOutput = outputContract.formatStandardOutput;
|
|
931
|
+
|
|
932
|
+
const unifiedOutput = require('./lib/unified-output');
|
|
933
|
+
formatScanOutputFromUnified = unifiedOutput.formatScanOutput;
|
|
934
|
+
getExitCodeFromUnified = unifiedOutput.getExitCode;
|
|
935
|
+
|
|
936
|
+
const cacheModule = require('../../dist/lib/cli/cache-manager');
|
|
937
|
+
CacheManager = cacheModule.CacheManager;
|
|
938
|
+
useUnifiedOutput = true;
|
|
939
|
+
} catch (error) {
|
|
940
|
+
// Fallback to old system if new one not available
|
|
941
|
+
if (opts.verbose) {
|
|
942
|
+
console.warn('Unified output system not available, using enhanced format');
|
|
943
|
+
}
|
|
944
|
+
useUnifiedOutput = false;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Initialize cache if available
|
|
948
|
+
let cache = null;
|
|
949
|
+
let cached = false;
|
|
950
|
+
let cachedResult = null;
|
|
951
|
+
|
|
952
|
+
if (CacheManager) {
|
|
953
|
+
cache = new CacheManager(projectPath);
|
|
954
|
+
const cacheKey = 'scan';
|
|
955
|
+
|
|
956
|
+
// Compute project hash for caching
|
|
957
|
+
const sourceFiles = await findSourceFiles(projectPath);
|
|
958
|
+
const projectHash = await cache.computeProjectHash(sourceFiles, { layers, baseUrl: opts.baseUrl });
|
|
959
|
+
|
|
960
|
+
// Check cache
|
|
961
|
+
if (!opts.verbose) {
|
|
962
|
+
cachedResult = await cache.get(cacheKey, projectHash);
|
|
963
|
+
if (cachedResult && buildVerdictOutput) {
|
|
964
|
+
cached = true;
|
|
965
|
+
// Use cached result
|
|
966
|
+
const verdict = buildVerdictOutput(cachedResult.findings, cachedResult.timings, true);
|
|
967
|
+
const output = formatStandardOutput(verdict, cachedResult.findings, cachedResult.scanId, projectPath, {
|
|
968
|
+
version: require('../../package.json').version || '1.0.0',
|
|
969
|
+
nodeVersion: process.version,
|
|
970
|
+
platform: process.platform,
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
if (opts.json) {
|
|
974
|
+
console.log(JSON.stringify(output, null, 2));
|
|
975
|
+
return getExitCode(verdict);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
console.log(formatScanOutput({ verdict, findings: cachedResult.findings, cached }, { verbose: opts.verbose }));
|
|
979
|
+
return getExitCode(verdict);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Start scanning with enhanced spinner
|
|
985
|
+
const timings = { discovery: 0, analysis: 0, verification: 0, detection: 0, total: 0 };
|
|
986
|
+
timings.discovery = Date.now();
|
|
987
|
+
|
|
988
|
+
// Only show spinner if not in JSON/SARIF mode
|
|
989
|
+
if (!opts.json && !opts.sarif) {
|
|
199
990
|
spinner = new Spinner({ color: colors.primary });
|
|
200
|
-
spinner.start(
|
|
991
|
+
spinner.start('Analyzing codebase...');
|
|
201
992
|
}
|
|
202
993
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
994
|
+
const result = await scanRouteIntegrity({
|
|
995
|
+
projectPath,
|
|
996
|
+
layers,
|
|
997
|
+
baseUrl: opts.baseUrl,
|
|
998
|
+
verbose: opts.verbose,
|
|
999
|
+
onProgress: opts.verbose ? (phase, progress) => {
|
|
1000
|
+
spinner.succeed(`${phase}: ${Math.round(progress)}%`);
|
|
1001
|
+
if (progress < 100) spinner.start(`Running ${phase}...`);
|
|
208
1002
|
} : undefined,
|
|
209
1003
|
});
|
|
1004
|
+
|
|
1005
|
+
timings.analysis = Date.now() - timings.discovery;
|
|
1006
|
+
|
|
1007
|
+
// Run new detection engines (Dead UI, Billing Bypass, Fake Success)
|
|
1008
|
+
let detectionFindings = [];
|
|
1009
|
+
try {
|
|
1010
|
+
if (spinner) spinner.start('Running detection engines...');
|
|
1011
|
+
const detectionStart = Date.now();
|
|
1012
|
+
|
|
1013
|
+
// Dynamic import for TypeScript detection engines
|
|
1014
|
+
const { DeadUIDetector } = require('../../dist/engines/detection/dead-ui-detector');
|
|
1015
|
+
const { BillingBypassDetector } = require('../../dist/engines/detection/billing-bypass-detector');
|
|
1016
|
+
const { FakeSuccessDetector } = require('../../dist/engines/detection/fake-success-detector');
|
|
1017
|
+
|
|
1018
|
+
// Load ignore patterns from .vibecheckignore + defaults
|
|
1019
|
+
const { loadIgnorePatterns } = require('./lib/agent-firewall/utils/ignore-checker');
|
|
1020
|
+
const ignorePatterns = loadIgnorePatterns(projectPath);
|
|
1021
|
+
const exclude = [
|
|
1022
|
+
// Default excludes
|
|
1023
|
+
'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '_archive',
|
|
1024
|
+
// From .vibecheckignore
|
|
1025
|
+
'mcp-server', 'bin', 'packages/cli', 'tests', '__tests__', 'examples',
|
|
1026
|
+
'templates', 'docs', '.guardrail', '.cursor', '.vibecheck',
|
|
1027
|
+
// Test files
|
|
1028
|
+
'*.test.ts', '*.test.tsx', '*.spec.ts', '*.spec.tsx', '*.test.js', '*.spec.js',
|
|
1029
|
+
// Type definitions
|
|
1030
|
+
'*.d.ts', '*.d.ts.map',
|
|
1031
|
+
];
|
|
1032
|
+
|
|
1033
|
+
// Run detectors in parallel
|
|
1034
|
+
const [deadUIResult, billingResult, fakeSuccessResult] = await Promise.all([
|
|
1035
|
+
new DeadUIDetector(projectPath).scan({ exclude }),
|
|
1036
|
+
new BillingBypassDetector(projectPath).scan({ exclude }),
|
|
1037
|
+
new FakeSuccessDetector(projectPath).scan({ exclude }),
|
|
1038
|
+
]);
|
|
1039
|
+
|
|
1040
|
+
// Convert to normalized findings format
|
|
1041
|
+
for (const finding of deadUIResult.findings) {
|
|
1042
|
+
detectionFindings.push({
|
|
1043
|
+
id: finding.id,
|
|
1044
|
+
ruleId: finding.type,
|
|
1045
|
+
category: 'DEAD_UI',
|
|
1046
|
+
severity: finding.severity,
|
|
1047
|
+
title: finding.message,
|
|
1048
|
+
description: finding.suggestion,
|
|
1049
|
+
file: finding.file,
|
|
1050
|
+
line: finding.line,
|
|
1051
|
+
evidence: finding.evidence,
|
|
1052
|
+
autofixAvailable: false,
|
|
1053
|
+
verdict: 'FAIL',
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
for (const finding of billingResult.findings) {
|
|
1058
|
+
detectionFindings.push({
|
|
1059
|
+
id: finding.id,
|
|
1060
|
+
ruleId: finding.type,
|
|
1061
|
+
category: 'BILLING',
|
|
1062
|
+
severity: finding.severity,
|
|
1063
|
+
title: finding.message,
|
|
1064
|
+
description: finding.suggestion,
|
|
1065
|
+
file: finding.file,
|
|
1066
|
+
line: finding.line,
|
|
1067
|
+
evidence: finding.evidence,
|
|
1068
|
+
autofixAvailable: false,
|
|
1069
|
+
verdict: 'FAIL',
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
for (const finding of fakeSuccessResult.findings) {
|
|
1074
|
+
detectionFindings.push({
|
|
1075
|
+
id: finding.id,
|
|
1076
|
+
ruleId: finding.type,
|
|
1077
|
+
category: 'FAKE_SUCCESS',
|
|
1078
|
+
severity: finding.severity,
|
|
1079
|
+
title: finding.message,
|
|
1080
|
+
description: finding.suggestion,
|
|
1081
|
+
file: finding.file,
|
|
1082
|
+
line: finding.line,
|
|
1083
|
+
evidence: finding.evidence,
|
|
1084
|
+
autofixAvailable: false,
|
|
1085
|
+
verdict: 'FAIL',
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
timings.detection = Date.now() - detectionStart;
|
|
1090
|
+
if (spinner && !opts.json && !opts.sarif) {
|
|
1091
|
+
spinner.succeed(`Detection complete (${detectionFindings.length} findings)`);
|
|
1092
|
+
}
|
|
1093
|
+
} catch (detectionError) {
|
|
1094
|
+
// Detection engines not compiled yet - continue without them
|
|
1095
|
+
if (opts.verbose && !opts.json && !opts.sarif) {
|
|
1096
|
+
console.log(` ${ansi.dim}Detection engines not available: ${detectionError.message}${ansi.reset}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (spinner && !opts.json && !opts.sarif) {
|
|
1099
|
+
spinner.warn('Detection skipped (not compiled)');
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
210
1102
|
|
|
211
|
-
|
|
1103
|
+
timings.verification = Date.now() - timings.analysis - timings.discovery;
|
|
1104
|
+
timings.total = Date.now() - startTime;
|
|
212
1105
|
|
|
213
|
-
if (spinner) {
|
|
214
|
-
spinner.succeed(
|
|
1106
|
+
if (spinner && !opts.json && !opts.sarif) {
|
|
1107
|
+
spinner.succeed('Analysis complete');
|
|
215
1108
|
}
|
|
216
1109
|
|
|
217
|
-
|
|
218
|
-
const findings = scanResult.findings.map((f, i) => ({
|
|
219
|
-
id: f.id || `finding-${i}`,
|
|
220
|
-
ruleId: f.type || f.category,
|
|
221
|
-
category: f.category || 'CodeQuality',
|
|
222
|
-
severity: normalizeSeverity(f.severity),
|
|
223
|
-
title: f.title || f.message,
|
|
224
|
-
message: f.message || f.title,
|
|
225
|
-
file: f.file || '',
|
|
226
|
-
line: f.line || 1,
|
|
227
|
-
confidence: f.confidence || 'medium',
|
|
228
|
-
engine: f.engine,
|
|
229
|
-
fixHint: f.fixHint,
|
|
230
|
-
}));
|
|
231
|
-
|
|
232
|
-
// Calculate verdict
|
|
233
|
-
const blockers = findings.filter(f => f.severity === 'critical');
|
|
234
|
-
const warnings = findings.filter(f => f.severity === 'warning');
|
|
235
|
-
const verdict = blockers.length > 0 ? 'BLOCK' : warnings.length > 0 ? 'WARN' : 'PASS';
|
|
236
|
-
const score = calculateScore({
|
|
237
|
-
critical: blockers.length,
|
|
238
|
-
high: 0,
|
|
239
|
-
medium: warnings.length,
|
|
240
|
-
low: findings.length - blockers.length - warnings.length
|
|
241
|
-
});
|
|
1110
|
+
const { report, outputPaths } = result;
|
|
242
1111
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
1112
|
+
// Use new unified output if available, otherwise fallback to enhanced format
|
|
1113
|
+
if (useUnifiedOutput && buildVerdictOutput && normalizeFinding && formatScanOutputFromUnified) {
|
|
1114
|
+
// Normalize findings with stable IDs
|
|
1115
|
+
const existingIDs = new Set();
|
|
1116
|
+
const normalizedFindings = [];
|
|
1117
|
+
|
|
1118
|
+
// Normalize route integrity findings
|
|
1119
|
+
if (report.shipBlockers) {
|
|
1120
|
+
for (let i = 0; i < report.shipBlockers.length; i++) {
|
|
1121
|
+
const blocker = report.shipBlockers[i];
|
|
1122
|
+
const category = blocker.category || 'ROUTE';
|
|
1123
|
+
const normalized = normalizeFinding(blocker, category, i, existingIDs);
|
|
1124
|
+
normalizedFindings.push(normalized);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Normalize Reality Sniff findings if present
|
|
1129
|
+
if (report.realitySniffFindings) {
|
|
1130
|
+
for (let i = 0; i < report.realitySniffFindings.length; i++) {
|
|
1131
|
+
const finding = report.realitySniffFindings[i];
|
|
1132
|
+
const category = finding.ruleId?.startsWith('auth') ? 'AUTH' : 'REALITY';
|
|
1133
|
+
const normalized = normalizeFinding(finding, category, normalizedFindings.length, existingIDs);
|
|
1134
|
+
normalizedFindings.push(normalized);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Add detection engine findings (Dead UI, Billing, Fake Success)
|
|
1139
|
+
for (const finding of detectionFindings) {
|
|
1140
|
+
normalizedFindings.push(finding);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Build verdict
|
|
1144
|
+
const verdict = buildVerdictOutput(normalizedFindings, timings, false);
|
|
1145
|
+
const scanId = `scan_${Date.now()}`;
|
|
1146
|
+
|
|
1147
|
+
// Cache result
|
|
1148
|
+
if (cache) {
|
|
1149
|
+
const sourceFiles = await findSourceFiles(projectPath);
|
|
1150
|
+
const projectHash = await cache.computeProjectHash(sourceFiles, { layers, baseUrl: opts.baseUrl });
|
|
1151
|
+
await cache.set('scan', projectHash, {
|
|
1152
|
+
findings: normalizedFindings,
|
|
1153
|
+
timings,
|
|
1154
|
+
scanId,
|
|
1155
|
+
}, {
|
|
1156
|
+
filesScanned: sourceFiles.length,
|
|
1157
|
+
findings: normalizedFindings.length,
|
|
1158
|
+
duration: timings.total,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Build standard output
|
|
1163
|
+
const standardOutput = formatStandardOutput(verdict, normalizedFindings, scanId, projectPath, {
|
|
1164
|
+
version: require('../../package.json').version || '1.0.0',
|
|
1165
|
+
nodeVersion: process.version,
|
|
1166
|
+
platform: process.platform,
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// JSON output mode - CRITICAL: Output ONLY JSON, no banners/colors
|
|
248
1170
|
if (opts.json) {
|
|
1171
|
+
// Ensure pure JSON output with no ANSI codes or banners
|
|
1172
|
+
process.env.NO_COLOR = '1';
|
|
1173
|
+
const calculatedExitCode = getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
|
|
249
1174
|
const jsonOutput = {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
verdict,
|
|
255
|
-
score,
|
|
1175
|
+
success: calculatedExitCode === 0 || calculatedExitCode === 1, // 0 or 1 are success (SHIP/WARN)
|
|
1176
|
+
verdict: verdict?.verdict || 'UNKNOWN',
|
|
1177
|
+
score: report.score?.overall || (verdict?.verdict === 'PASS' ? 100 : 50),
|
|
1178
|
+
findings: normalizedFindings || [],
|
|
256
1179
|
summary: {
|
|
257
|
-
total:
|
|
258
|
-
blockers: blockers
|
|
259
|
-
warnings: warnings
|
|
260
|
-
|
|
261
|
-
duration,
|
|
1180
|
+
total: normalizedFindings?.length || 0,
|
|
1181
|
+
blockers: verdict?.summary?.blockers || 0,
|
|
1182
|
+
warnings: verdict?.summary?.warnings || 0,
|
|
1183
|
+
info: verdict?.summary?.info || 0,
|
|
262
1184
|
},
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
file: f.file,
|
|
270
|
-
line: f.line,
|
|
271
|
-
engine: f.engine,
|
|
272
|
-
})),
|
|
273
|
-
timestamp: new Date().toISOString(),
|
|
1185
|
+
stats: {
|
|
1186
|
+
filesScanned: report.stats?.filesScanned || 0,
|
|
1187
|
+
linesScanned: report.stats?.linesScanned || 0,
|
|
1188
|
+
durationMs: timings.total || 0,
|
|
1189
|
+
},
|
|
1190
|
+
exitCode: calculatedExitCode
|
|
274
1191
|
};
|
|
275
1192
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
276
|
-
|
|
1193
|
+
// CRITICAL: Exit immediately, don't output anything else
|
|
1194
|
+
process.exit(calculatedExitCode);
|
|
1195
|
+
return calculatedExitCode;
|
|
277
1196
|
}
|
|
278
|
-
|
|
279
|
-
// SARIF output
|
|
1197
|
+
|
|
1198
|
+
// SARIF output mode - CRITICAL: Output ONLY valid SARIF, no banners/colors
|
|
280
1199
|
if (opts.sarif) {
|
|
281
|
-
|
|
1200
|
+
process.env.NO_COLOR = '1';
|
|
1201
|
+
const sarif = formatSARIF(normalizedFindings, {
|
|
282
1202
|
projectPath,
|
|
283
1203
|
version: require('../../package.json').version || '1.0.0'
|
|
284
1204
|
});
|
|
285
|
-
|
|
286
|
-
|
|
1205
|
+
// formatSARIF returns a JSON string, so output it directly
|
|
1206
|
+
console.log(sarif);
|
|
1207
|
+
const exitCode = getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
|
|
1208
|
+
// CRITICAL: Exit immediately, don't output anything else
|
|
1209
|
+
process.exit(exitCode);
|
|
1210
|
+
return exitCode;
|
|
287
1211
|
}
|
|
1212
|
+
|
|
1213
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1214
|
+
// FINGERPRINTING & BASELINE COMPARISON
|
|
1215
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
288
1216
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
1217
|
+
let diff = null;
|
|
1218
|
+
if (opts.baseline) {
|
|
1219
|
+
try {
|
|
1220
|
+
const enrichResult = enrichFindings(normalizedFindings, projectPath, true);
|
|
1221
|
+
diff = enrichResult.diff;
|
|
1222
|
+
|
|
1223
|
+
// Update findings with fingerprints and status
|
|
1224
|
+
for (let i = 0; i < normalizedFindings.length; i++) {
|
|
1225
|
+
if (enrichResult.findings[i]) {
|
|
1226
|
+
normalizedFindings[i].fingerprint = enrichResult.findings[i].fingerprint;
|
|
1227
|
+
normalizedFindings[i].status = enrichResult.findings[i].status;
|
|
1228
|
+
normalizedFindings[i].firstSeen = enrichResult.findings[i].firstSeen;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
} catch (fpError) {
|
|
1232
|
+
if (opts.verbose) {
|
|
1233
|
+
console.warn(` ${ansi.dim}Fingerprinting skipped: ${fpError.message}${ansi.reset}`);
|
|
1234
|
+
}
|
|
307
1235
|
}
|
|
308
|
-
if (blockers.length > 5) {
|
|
309
|
-
console.log(` ${ansi.dim}... and ${blockers.length - 5} more${ansi.reset}`);
|
|
310
|
-
}
|
|
311
|
-
console.log();
|
|
312
1236
|
}
|
|
313
1237
|
|
|
314
|
-
//
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1238
|
+
// Update baseline if requested
|
|
1239
|
+
if (opts.updateBaseline) {
|
|
1240
|
+
try {
|
|
1241
|
+
saveBaseline(projectPath, normalizedFindings, {
|
|
1242
|
+
verdict: verdict?.verdict,
|
|
1243
|
+
scanTime: new Date().toISOString(),
|
|
1244
|
+
});
|
|
1245
|
+
if (!opts.json && !opts.quiet) {
|
|
1246
|
+
console.log(` ${colors.success}✓${ansi.reset} Baseline updated with ${normalizedFindings.length} findings`);
|
|
1247
|
+
}
|
|
1248
|
+
} catch (blError) {
|
|
1249
|
+
if (opts.verbose) {
|
|
1250
|
+
console.warn(` ${ansi.dim}Baseline save failed: ${blError.message}${ansi.reset}`);
|
|
1251
|
+
}
|
|
323
1252
|
}
|
|
324
|
-
console.log();
|
|
325
1253
|
}
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
1254
|
+
|
|
1255
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1256
|
+
// ENHANCED OUTPUT
|
|
1257
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1258
|
+
|
|
1259
|
+
// Use enhanced output formatter (from scan-output.js) - ONLY if not JSON/SARIF mode
|
|
1260
|
+
if (!opts.json && !opts.sarif) {
|
|
1261
|
+
const resultForOutput = {
|
|
1262
|
+
verdict,
|
|
1263
|
+
findings: normalizedFindings,
|
|
1264
|
+
layers: report.layers || [],
|
|
1265
|
+
coverage: report.coverageMap,
|
|
1266
|
+
breakdown: report.score?.breakdown,
|
|
1267
|
+
timings,
|
|
1268
|
+
cached,
|
|
1269
|
+
diff, // Include diff for display
|
|
1270
|
+
};
|
|
1271
|
+
console.log(formatScanOutput(resultForOutput, { verbose: opts.verbose }));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Additional details if verbose
|
|
1275
|
+
if (opts.verbose) {
|
|
1276
|
+
printBreakdown(report.score.breakdown);
|
|
1277
|
+
printCoverageMap(report.coverageMap);
|
|
1278
|
+
printLayers(report.layers);
|
|
1279
|
+
|
|
1280
|
+
printSection('REPORTS', '📄');
|
|
333
1281
|
console.log();
|
|
1282
|
+
console.log(` ${c.cyan}${outputPaths.md}${c.reset}`);
|
|
1283
|
+
console.log(` ${c.dim}${outputPaths.json}${c.reset}`);
|
|
1284
|
+
if (outputPaths.sarif) {
|
|
1285
|
+
console.log(` ${c.dim}${outputPaths.sarif}${c.reset}`);
|
|
1286
|
+
}
|
|
334
1287
|
}
|
|
335
|
-
|
|
336
|
-
// Emit completion
|
|
337
|
-
emitScanComplete(projectPath, verdict === 'PASS' ? 'success' : 'failure', {
|
|
338
|
-
score,
|
|
339
|
-
issueCount: findings.length,
|
|
340
|
-
durationMs: duration,
|
|
341
|
-
});
|
|
342
1288
|
|
|
343
|
-
//
|
|
1289
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1290
|
+
// SAVE RESULTS
|
|
1291
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1292
|
+
|
|
344
1293
|
if (opts.save) {
|
|
345
1294
|
const resultsDir = path.join(projectPath, '.vibecheck', 'results');
|
|
346
1295
|
if (!fs.existsSync(resultsDir)) {
|
|
347
1296
|
fs.mkdirSync(resultsDir, { recursive: true });
|
|
348
1297
|
}
|
|
349
1298
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1299
|
+
// Save latest.json
|
|
1300
|
+
const latestPath = path.join(resultsDir, 'latest.json');
|
|
1301
|
+
fs.writeFileSync(latestPath, JSON.stringify(standardOutput, null, 2));
|
|
1302
|
+
|
|
1303
|
+
// Save timestamped copy
|
|
1304
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1305
|
+
const historyDir = path.join(resultsDir, 'history');
|
|
1306
|
+
if (!fs.existsSync(historyDir)) {
|
|
1307
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
1308
|
+
}
|
|
1309
|
+
fs.writeFileSync(path.join(historyDir, `scan-${timestamp}.json`), JSON.stringify(standardOutput, null, 2));
|
|
1310
|
+
|
|
1311
|
+
if (!opts.json) {
|
|
1312
|
+
console.log(`\n ${ansi.dim}Results saved to: ${latestPath}${ansi.reset}`);
|
|
1313
|
+
}
|
|
362
1314
|
}
|
|
363
1315
|
|
|
364
|
-
|
|
1316
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1317
|
+
// AUTOFIX MODE - Generate missions
|
|
1318
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1319
|
+
|
|
1320
|
+
if (opts.autofix && normalizedFindings.length > 0) {
|
|
1321
|
+
// Check entitlement
|
|
1322
|
+
const entitlementsV2 = require("./lib/entitlements-v2");
|
|
1323
|
+
const access = await entitlementsV2.enforce("scan.autofix", {
|
|
1324
|
+
projectPath,
|
|
1325
|
+
silent: false,
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
if (!access.allowed) {
|
|
1329
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.bold}--autofix requires STARTER plan${ansi.reset}`);
|
|
1330
|
+
console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}`);
|
|
1331
|
+
console.log(` ${ansi.dim}Scan results saved. Run 'vibecheck fix' for manual mission generation.${ansi.reset}\n`);
|
|
1332
|
+
} else {
|
|
1333
|
+
console.log(`\n ${colors.accent}${icons.lightning}${ansi.reset} ${ansi.bold}Generating AI missions...${ansi.reset}\n`);
|
|
1334
|
+
|
|
1335
|
+
const { missions, summary } = await generateMissions(normalizedFindings, projectPath, opts);
|
|
1336
|
+
|
|
1337
|
+
console.log(` ${colors.success}✓${ansi.reset} Generated ${missions.length} missions`);
|
|
1338
|
+
console.log(` ${ansi.dim}Critical: ${summary.bySeverity.critical}${ansi.reset}`);
|
|
1339
|
+
console.log(` ${ansi.dim}High: ${summary.bySeverity.high}${ansi.reset}`);
|
|
1340
|
+
console.log(` ${ansi.dim}Medium: ${summary.bySeverity.medium}${ansi.reset}`);
|
|
1341
|
+
console.log(` ${ansi.dim}Low: ${summary.bySeverity.low}${ansi.reset}`);
|
|
1342
|
+
console.log();
|
|
1343
|
+
console.log(` ${ansi.dim}Missions saved to: .vibecheck/missions/${ansi.reset}`);
|
|
1344
|
+
console.log(` ${ansi.dim}Open MISSIONS.md for prompts to use with your AI assistant.${ansi.reset}`);
|
|
1345
|
+
console.log();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Emit audit event for scan complete
|
|
1350
|
+
emitScanComplete(projectPath, verdict.verdict === 'PASS' ? 'success' : 'failure', {
|
|
1351
|
+
score: report.score?.overall || (verdict.verdict === 'PASS' ? 100 : 50),
|
|
1352
|
+
grade: report.score?.grade || (verdict.verdict === 'PASS' ? 'A' : 'F'),
|
|
1353
|
+
issueCount: verdict.summary.blockers,
|
|
1354
|
+
durationMs: timings.total,
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// Submit results to dashboard if connected
|
|
1358
|
+
if (apiConnected && apiScan) {
|
|
1359
|
+
try {
|
|
1360
|
+
await submitScanResults(apiScan.scanId, {
|
|
1361
|
+
verdict: verdict.verdict,
|
|
1362
|
+
score: report.score?.overall || 0,
|
|
1363
|
+
findings: report.findings || [],
|
|
1364
|
+
filesScanned: report.stats?.filesScanned || 0,
|
|
1365
|
+
linesScanned: report.stats?.linesScanned || 0,
|
|
1366
|
+
durationMs: timings.total,
|
|
1367
|
+
metadata: {
|
|
1368
|
+
layers,
|
|
1369
|
+
profile: opts.profile,
|
|
1370
|
+
version: require('../../package.json').version,
|
|
1371
|
+
},
|
|
1372
|
+
});
|
|
1373
|
+
console.log(`${colors.success}✓${ansi.reset} Results sent to dashboard`);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
console.log(`${colors.warning}⚠${ansi.reset} Failed to send results to dashboard: ${err.message}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
|
|
1380
|
+
} else {
|
|
1381
|
+
// Legacy fallback output when unified output system isn't available
|
|
1382
|
+
const findings = [...(report.shipBlockers || []), ...detectionFindings];
|
|
1383
|
+
const criticalCount = findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length;
|
|
1384
|
+
const warningCount = findings.filter(f => f.severity === 'warning' || f.severity === 'WARN').length;
|
|
1385
|
+
|
|
1386
|
+
const verdict = criticalCount > 0 ? 'BLOCK' : warningCount > 0 ? 'WARN' : 'SHIP';
|
|
1387
|
+
|
|
1388
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1389
|
+
// FINGERPRINTING & BASELINE COMPARISON (Legacy path)
|
|
1390
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1391
|
+
|
|
1392
|
+
const normalizedLegacyFindings = findings.map(f => ({
|
|
1393
|
+
severity: f.severity === 'critical' || f.severity === 'BLOCK' ? 'critical' :
|
|
1394
|
+
f.severity === 'warning' || f.severity === 'WARN' ? 'medium' : 'low',
|
|
1395
|
+
category: f.category || 'ROUTE',
|
|
1396
|
+
title: f.title || f.message,
|
|
1397
|
+
message: f.message || f.title,
|
|
1398
|
+
file: f.file || f.evidence?.[0]?.file,
|
|
1399
|
+
line: f.line || parseInt(f.evidence?.[0]?.lines?.split('-')[0]) || 1,
|
|
1400
|
+
evidence: f.evidence,
|
|
1401
|
+
fix: f.fixSuggestion,
|
|
1402
|
+
}));
|
|
1403
|
+
|
|
1404
|
+
let diff = null;
|
|
1405
|
+
if (opts.baseline) {
|
|
1406
|
+
try {
|
|
1407
|
+
const enrichResult = enrichFindings(normalizedLegacyFindings, projectPath, true);
|
|
1408
|
+
diff = enrichResult.diff;
|
|
1409
|
+
|
|
1410
|
+
// Update findings with fingerprints and status
|
|
1411
|
+
for (let i = 0; i < normalizedLegacyFindings.length; i++) {
|
|
1412
|
+
if (enrichResult.findings[i]) {
|
|
1413
|
+
normalizedLegacyFindings[i].fingerprint = enrichResult.findings[i].fingerprint;
|
|
1414
|
+
normalizedLegacyFindings[i].status = enrichResult.findings[i].status;
|
|
1415
|
+
normalizedLegacyFindings[i].firstSeen = enrichResult.findings[i].firstSeen;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
} catch (fpError) {
|
|
1419
|
+
if (opts.verbose) {
|
|
1420
|
+
console.warn(` ${ansi.dim}Fingerprinting skipped: ${fpError.message}${ansi.reset}`);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Update baseline if requested
|
|
1426
|
+
if (opts.updateBaseline) {
|
|
1427
|
+
try {
|
|
1428
|
+
saveBaseline(projectPath, normalizedLegacyFindings, {
|
|
1429
|
+
verdict,
|
|
1430
|
+
scanTime: new Date().toISOString(),
|
|
1431
|
+
});
|
|
1432
|
+
if (!opts.json && !opts.quiet) {
|
|
1433
|
+
console.log(` ${colors.success}✓${ansi.reset} Baseline updated with ${normalizedLegacyFindings.length} findings`);
|
|
1434
|
+
}
|
|
1435
|
+
} catch (blError) {
|
|
1436
|
+
if (opts.verbose) {
|
|
1437
|
+
console.warn(` ${ansi.dim}Baseline save failed: ${blError.message}${ansi.reset}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// JSON output mode - CRITICAL: Output ONLY JSON, no banners/colors
|
|
1443
|
+
if (opts.json) {
|
|
1444
|
+
process.env.NO_COLOR = '1';
|
|
1445
|
+
const jsonOutput = {
|
|
1446
|
+
success: verdict === 'SHIP' || verdict === 'WARN',
|
|
1447
|
+
verdict: verdict,
|
|
1448
|
+
score: verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 70 : 40,
|
|
1449
|
+
findings: normalizedLegacyFindings || [],
|
|
1450
|
+
summary: {
|
|
1451
|
+
total: normalizedLegacyFindings?.length || 0,
|
|
1452
|
+
blockers: criticalCount,
|
|
1453
|
+
warnings: warningCount,
|
|
1454
|
+
info: findings.length - criticalCount - warningCount,
|
|
1455
|
+
},
|
|
1456
|
+
stats: {
|
|
1457
|
+
filesScanned: 0,
|
|
1458
|
+
linesScanned: 0,
|
|
1459
|
+
durationMs: timings.total || 0,
|
|
1460
|
+
},
|
|
1461
|
+
exitCode: verdictToExitCode(verdict)
|
|
1462
|
+
};
|
|
1463
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1464
|
+
process.exit(jsonOutput.exitCode);
|
|
1465
|
+
return jsonOutput.exitCode;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// SARIF output mode - CRITICAL: Output ONLY valid SARIF, no banners/colors
|
|
1469
|
+
if (opts.sarif) {
|
|
1470
|
+
process.env.NO_COLOR = '1';
|
|
1471
|
+
const sarif = formatSARIF(normalizedLegacyFindings, {
|
|
1472
|
+
projectPath,
|
|
1473
|
+
version: require('../../package.json').version || '1.0.0'
|
|
1474
|
+
});
|
|
1475
|
+
console.log(sarif);
|
|
1476
|
+
const exitCode = verdictToExitCode(verdict);
|
|
1477
|
+
process.exit(exitCode);
|
|
1478
|
+
return exitCode;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Use enhanced output formatter for legacy fallback
|
|
1482
|
+
const severityCounts = {
|
|
1483
|
+
critical: criticalCount,
|
|
1484
|
+
high: 0,
|
|
1485
|
+
medium: warningCount,
|
|
1486
|
+
low: findings.length - criticalCount - warningCount,
|
|
1487
|
+
};
|
|
1488
|
+
const score = calculateScore(severityCounts);
|
|
1489
|
+
|
|
1490
|
+
const result = {
|
|
1491
|
+
verdict: { verdict, score },
|
|
1492
|
+
findings: normalizedLegacyFindings,
|
|
1493
|
+
layers: [],
|
|
1494
|
+
timings,
|
|
1495
|
+
diff,
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
console.log(formatScanOutput(result, { verbose: opts.verbose }));
|
|
1499
|
+
|
|
1500
|
+
// Emit audit event
|
|
1501
|
+
emitScanComplete(projectPath, verdict === 'SHIP' ? 'success' : 'failure', {
|
|
1502
|
+
score: verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 70 : 40,
|
|
1503
|
+
issueCount: criticalCount + warningCount,
|
|
1504
|
+
durationMs: timings.total,
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
// Submit results to dashboard if connected
|
|
1508
|
+
if (apiConnected && apiScan) {
|
|
1509
|
+
try {
|
|
1510
|
+
await submitScanResults(apiScan.scanId, {
|
|
1511
|
+
verdict,
|
|
1512
|
+
score: verdict === 'SHIP' ? 100 : verdict === 'WARN' ? 70 : 40,
|
|
1513
|
+
findings: normalizedLegacyFindings,
|
|
1514
|
+
filesScanned: result.stats?.filesScanned || 0,
|
|
1515
|
+
linesScanned: result.stats?.linesScanned || 0,
|
|
1516
|
+
durationMs: timings.total,
|
|
1517
|
+
metadata: {
|
|
1518
|
+
layers,
|
|
1519
|
+
profile: opts.profile,
|
|
1520
|
+
version: require('../../package.json').version,
|
|
1521
|
+
},
|
|
1522
|
+
});
|
|
1523
|
+
console.log(`${colors.success}✓${ansi.reset} Results sent to dashboard`);
|
|
1524
|
+
} catch (err) {
|
|
1525
|
+
console.log(`${colors.warning}⚠${ansi.reset} Failed to send results to dashboard: ${err.message}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return verdictToExitCode(verdict);
|
|
1530
|
+
}
|
|
365
1531
|
|
|
366
1532
|
} catch (error) {
|
|
367
|
-
if (spinner) {
|
|
1533
|
+
if (spinner && !opts.json && !opts.sarif) {
|
|
368
1534
|
spinner.fail(`Scan failed: ${error.message}`);
|
|
369
1535
|
}
|
|
370
1536
|
|
|
371
|
-
|
|
372
|
-
if (opts.
|
|
373
|
-
|
|
1537
|
+
// JSON/SARIF error output
|
|
1538
|
+
if (opts.json || opts.sarif) {
|
|
1539
|
+
process.env.NO_COLOR = '1';
|
|
1540
|
+
const errorOutput = {
|
|
1541
|
+
success: false,
|
|
1542
|
+
error: {
|
|
1543
|
+
code: error.code || 'SCAN_ERROR',
|
|
1544
|
+
message: error.message,
|
|
1545
|
+
},
|
|
1546
|
+
exitCode: error.code === 'VALIDATION_ERROR' ? EXIT.USER_ERROR : EXIT.INTERNAL_ERROR
|
|
1547
|
+
};
|
|
1548
|
+
if (opts.json) {
|
|
1549
|
+
console.log(JSON.stringify(errorOutput, null, 2));
|
|
1550
|
+
} else {
|
|
1551
|
+
// SARIF error - return empty SARIF with error in message
|
|
1552
|
+
const sarif = formatSARIF([], { projectPath, version: require('../../package.json').version || '1.0.0' });
|
|
1553
|
+
console.log(sarif);
|
|
1554
|
+
}
|
|
1555
|
+
process.exit(errorOutput.exitCode);
|
|
1556
|
+
return errorOutput.exitCode;
|
|
374
1557
|
}
|
|
375
1558
|
|
|
1559
|
+
// Use enhanced error handling (only for non-JSON/SARIF modes)
|
|
1560
|
+
const exitCode = printError(error, 'Scan');
|
|
1561
|
+
|
|
1562
|
+
// Emit audit event for scan error
|
|
376
1563
|
emitScanComplete(projectPath, 'error', {
|
|
377
1564
|
errorCode: error.code || 'SCAN_ERROR',
|
|
378
1565
|
errorMessage: error.message,
|
|
379
1566
|
durationMs: Date.now() - startTime,
|
|
380
1567
|
});
|
|
1568
|
+
|
|
1569
|
+
// Report error to dashboard if connected
|
|
1570
|
+
if (apiConnected && apiScan) {
|
|
1571
|
+
try {
|
|
1572
|
+
await reportScanError(apiScan.scanId, error);
|
|
1573
|
+
if (!opts.json && !opts.sarif) {
|
|
1574
|
+
console.log(`${colors.info}📡${ansi.reset} Error reported to dashboard`);
|
|
1575
|
+
}
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
if (!opts.json && !opts.sarif) {
|
|
1578
|
+
console.log(`${colors.warning}⚠${ansi.reset} Failed to report error to dashboard: ${err.message}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
381
1582
|
|
|
382
|
-
return
|
|
1583
|
+
return exitCode;
|
|
383
1584
|
}
|
|
384
1585
|
}
|
|
385
1586
|
|
|
386
|
-
// Helper to
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1587
|
+
// Helper function to find source files for cache hash
|
|
1588
|
+
async function findSourceFiles(projectPath) {
|
|
1589
|
+
const files = [];
|
|
1590
|
+
const fs = require('fs');
|
|
1591
|
+
const path = require('path');
|
|
1592
|
+
|
|
1593
|
+
async function walk(dir) {
|
|
1594
|
+
try {
|
|
1595
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1596
|
+
for (const entry of entries) {
|
|
1597
|
+
const fullPath = path.join(dir, entry.name);
|
|
1598
|
+
if (entry.isDirectory()) {
|
|
1599
|
+
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
1600
|
+
await walk(fullPath);
|
|
1601
|
+
}
|
|
1602
|
+
} else if (entry.isFile()) {
|
|
1603
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1604
|
+
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
|
|
1605
|
+
files.push(fullPath);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
} catch {
|
|
1610
|
+
// Skip inaccessible directories
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
await walk(projectPath);
|
|
1615
|
+
return files;
|
|
393
1616
|
}
|
|
394
1617
|
|
|
395
|
-
// Export
|
|
1618
|
+
// Export with error handling wrapper
|
|
396
1619
|
module.exports = {
|
|
397
1620
|
runScan: withErrorHandling(runScan, "Scan failed"),
|
|
398
1621
|
};
|