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
JavaScript
;
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