UNPKG

ubon

Version:

Security scanner for AI-generated apps (Cursor, Lovable, Windsurf, v0). Catches hardcoded secrets, prompt injection, hallucinated imports, Server Actions / Edge runtime mistakes, and the vibe-coded vulnerabilities traditional linters miss.

178 lines 8.55 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InteractiveReporter = void 0; const colors_1 = __importDefault(require("../utils/colors")); const rules_1 = require("../rules"); const HumanReporter_1 = require("./HumanReporter"); /** * Walks the user through issues one at a time. Optimized for triage * sessions where the user wants to *understand* a finding before deciding * to fix, suppress, or ignore it. Output uses a fixed 65-column box so it * renders consistently across terminal widths. */ class InteractiveReporter { useColor; constructor(colorMode = 'auto') { this.useColor = this.shouldUseColor(colorMode); } shouldUseColor(mode) { if (mode === 'always') return true; if (mode === 'never') return false; return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR; } colorize(fn, text) { return this.useColor ? fn(text) : text; } brand(text) { return this.useColor ? colors_1.default.hex('#c99cb3')(text) : text; } async run(results, options) { console.log(`\n${this.brand('🪷')} Found ${results.length} issues. Let's walk through them together...\n`); if (results.length === 0) { console.log(`${this.brand('🪷')} Perfect! No issues found. Your app is ready to bloom! ✨\n`); return; } const sorted = [...results].sort((a, b) => { const sev = { high: 3, medium: 2, low: 1 }; const type = { error: 3, warning: 2, info: 1 }; const sevDiff = sev[b.severity] - sev[a.severity]; if (sevDiff !== 0) return sevDiff; return type[b.type] - type[a.type]; }); for (let i = 0; i < sorted.length; i++) { const result = sorted[i]; const choice = await this.presentIssue(result, i + 1, sorted.length, options); if (choice === 'quit') { console.log(`\n${this.brand('🪷')} Interactive session ended. Remaining issues can be viewed with normal scan.\n`); break; } } console.log(`\n${this.brand('🪷')} Interactive walkthrough complete! ✨\n`); } async presentIssue(result, current, total, _options) { const severityColor = this.getSeverityColor(result.severity); const typeIcon = result.type === 'error' ? '❌' : '⚠️'; console.log(`┌${'─'.repeat(65)}┐`); console.log(`│ Issue ${current} of ${total} ${' '.repeat(Math.max(0, 65 - (`Issue ${current} of ${total} `).length))}│`); console.log(`│ ${typeIcon} ${severityColor} - ${result.ruleId} ${' '.repeat(Math.max(0, 65 - (`${typeIcon} ${result.severity.toUpperCase()} - ${result.ruleId} `).length))}│`); console.log(`│ ${result.message} ${' '.repeat(Math.max(0, 65 - (result.message.length + 1)))}│`); if (result.file) { const location = `${result.file}${result.line ? `:${result.line}` : ''}`; console.log(`│ ${this.colorize(colors_1.default.gray, location)} ${' '.repeat(Math.max(0, 65 - (location.length + 1)))}│`); } console.log(`├${'─'.repeat(65)}┤`); const meta = rules_1.RULES[result.ruleId]; if (meta?.impact) { console.log(`│ ${this.colorize(colors_1.default.blue, '💡 Why this matters:')} ${' '.repeat(65 - '💡 Why this matters: '.length)}│`); this.wrapText(meta.impact, 63).forEach((line) => { console.log(`│ ${line} ${' '.repeat(Math.max(0, 65 - (line.length + 1)))}│`); }); console.log(`├${'─'.repeat(65)}┤`); } if (result.fix) { console.log(`│ ${this.colorize(colors_1.default.green, '🔧 Suggested fix:')} ${' '.repeat(65 - '🔧 Suggested fix: '.length)}│`); this.wrapText(result.fix, 63).forEach((line) => { console.log(`│ ${line} ${' '.repeat(Math.max(0, 65 - (line.length + 1)))}│`); }); console.log(`├${'─'.repeat(65)}┤`); } if (result.file && result.line) { const context = (0, HumanReporter_1.getCodeContext)(result.file, result.line); if (context) { console.log(`│ ${this.colorize(colors_1.default.gray, '📋 Code context:')} ${' '.repeat(65 - '📋 Code context: '.length)}│`); context.slice(0, 3).forEach((line, idx) => { const lineNum = (result.line - 2 + idx).toString().padStart(3); const isTarget = idx === 1; const marker = isTarget ? this.colorize(colors_1.default.red, '►') : ' '; const display = `${marker} ${lineNum} ${line}`.slice(0, 63); console.log(`│ ${display} ${' '.repeat(Math.max(0, 65 - (display.length + 1)))}│`); }); console.log(`├${'─'.repeat(65)}┤`); } } console.log(`│ [${this.colorize(colors_1.default.green, 'f')}]ix automatically [${this.colorize(colors_1.default.yellow, 's')}]kip [${this.colorize(colors_1.default.blue, 'b')}]aseline [${this.colorize(colors_1.default.cyan, 'n')}]ext ${' '.repeat(24)}│`); console.log(`│ [${this.colorize(colors_1.default.red, 'q')}]uit [${this.colorize(colors_1.default.gray, '?')}]help ${' '.repeat(50)}│`); console.log(`└${'─'.repeat(65)}┘`); return await this.promptUserChoice(); } getSeverityColor(severity) { const colors = { high: this.colorize(colors_1.default.red, 'HIGH'), medium: this.colorize(colors_1.default.yellow, 'MEDIUM'), low: this.colorize(colors_1.default.green, 'LOW'), }; return colors[severity] || severity.toUpperCase(); } wrapText(text, maxWidth) { const words = text.split(' '); const lines = []; let cur = ''; for (const w of words) { if ((cur + w).length <= maxWidth) cur += (cur ? ' ' : '') + w; else { if (cur) lines.push(cur); cur = w; } } if (cur) lines.push(cur); return lines; } async promptUserChoice() { return new Promise((resolve) => { const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Choose an action: ', (answer) => { rl.close(); const choice = answer.toLowerCase().trim(); switch (choice) { case 'f': case 'fix': resolve('fix'); break; case 's': case 'skip': resolve('skip'); break; case 'b': case 'baseline': resolve('baseline'); break; case 'n': case 'next': resolve('next'); break; case 'q': case 'quit': resolve('quit'); break; case '?': case 'help': console.log('\nAvailable actions:'); console.log(' f, fix - Apply automatic fix if available'); console.log(' s, skip - Skip this issue'); console.log(' b, baseline - Add to baseline (suppress)'); console.log(' n, next - Continue to next issue'); console.log(' q, quit - Exit interactive mode'); console.log(' ?, help - Show this help\n'); resolve('help'); break; default: console.log('Invalid choice. Press ? for help.'); resolve('help'); break; } }); }); } } exports.InteractiveReporter = InteractiveReporter; //# sourceMappingURL=InteractiveReporter.js.map