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.
308 lines âĸ 16.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HumanReporter = void 0;
exports.getCodeContext = getCodeContext;
const fs_1 = require("fs");
const colors_1 = __importDefault(require("../utils/colors"));
const rules_1 = require("../rules");
const suppressions_1 = require("../utils/suppressions");
const Posture_1 = require("../core/Posture");
/**
* Render scan results as colorized terminal output. Pure presentation: no
* scanning, no filtering of suppressions/baselines (the orchestrator does
* that). Kept in its own module so the orchestrator can stay focused on
* running scanners and so a future TerminalReporter / CompactReporter can
* live next to this one.
*/
class HumanReporter {
logger;
useColor;
constructor(logger, colorMode = 'auto') {
this.logger = logger;
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;
}
getSeverityBand(severity, count) {
if (count === 0)
return '';
const bands = {
high: colors_1.default.hex('#ff6b7d').bgHex('#ff6b7d').white,
medium: colors_1.default.hex('#ffa502').bgHex('#ffa502').black,
low: colors_1.default.hex('#7bed9f').bgHex('#7bed9f').black,
};
const colorFn = this.useColor ? bands[severity] : ((t) => t);
return colorFn(` ${count} ${severity.toUpperCase()} `);
}
print(results, options) {
const suppressedCount = results.filter((r) => r.suppressed).length;
const activeResults = (0, suppressions_1.filterSuppressedResults)(results, {
showSuppressed: options?.showSuppressed,
ignoreSuppressed: options?.ignoreSuppressed,
});
const quiet = !!options?.quiet;
if (activeResults.length === 0 && suppressedCount === 0) {
if (!quiet)
this.logger.success('đǎ 0 critical findings. No active issues found.');
return;
}
if (activeResults.length === 0 && suppressedCount > 0) {
if (!quiet)
this.logger.success(`đǎ 0 critical findings. ${suppressedCount} issues suppressed.`);
return;
}
const filteredResults = this.applyResultFilters(activeResults, options);
const totalActive = activeResults.length;
if (!quiet && !options?.maxIssues && totalActive > 50) {
this.logger.info(this.colorize(colors_1.default.gray, `Found ${totalActive} issues. Tip: use --max-issues 10 to focus on critical items first.`));
}
const highCount = filteredResults.filter((r) => r.severity === 'high').length;
const mediumCount = filteredResults.filter((r) => r.severity === 'medium').length;
const lowCount = filteredResults.filter((r) => r.severity === 'low').length;
const bands = [
this.getSeverityBand('high', highCount),
this.getSeverityBand('medium', mediumCount),
this.getSeverityBand('low', lowCount),
].filter(Boolean).join(' ');
const suppressedText = suppressedCount > 0
? this.colorize(colors_1.default.gray, ` ${suppressedCount} suppressed`)
: '';
console.log(`\n${this.brand('đǎ')} ${this.colorize(colors_1.default.bold, 'Triage')}: ${bands}${suppressedText}`);
if (filteredResults.length !== activeResults.length) {
console.log(`${this.colorize(colors_1.default.gray, ` (showing ${filteredResults.length} of ${activeResults.length} active issues)`)}`);
}
this.logger.separator();
this.logger.title(`Found ${filteredResults.length} issues:`);
const grouped = this.groupResults(filteredResults, options?.groupBy || 'severity');
Object.entries(grouped).forEach(([groupKey, groupResults]) => {
const icon = this.getGroupIcon(groupKey, options?.groupBy || 'category');
const lotus = this.brand(icon);
const count = this.colorize(colors_1.default.gray, `(${groupResults.length})`);
console.log(`\n${lotus} ${this.colorize(colors_1.default.bold, groupKey.toUpperCase())} ${count}:`);
if (options?.format === 'table') {
this.renderTableGroup(groupResults);
return;
}
this.renderListGroup(groupResults, options);
});
this.logger.separator();
this.printSummary(results);
this.printContextualGuidance(results, options);
}
renderTableGroup(rows) {
const header = `${this.colorize(colors_1.default.gray, 'SEV'.padEnd(6))} ${this.colorize(colors_1.default.gray, 'RULE'.padEnd(8))} ${this.colorize(colors_1.default.gray, 'FILE:LINE'.padEnd(32))} ${this.colorize(colors_1.default.gray, 'CONF'.padEnd(6))} ${this.colorize(colors_1.default.gray, 'MESSAGE')}`;
console.log(` ${header}`);
rows.forEach((result) => {
const sev = (result.severity || '').toUpperCase().padEnd(6);
const rule = (result.ruleId || '').padEnd(8);
const loc = result.file ? `${result.file}${result.line ? `:${result.line}` : ''}` : '';
const locCol = (loc.length > 32 ? loc.slice(0, 29) + 'âĻ' : loc).padEnd(32);
const conf = (result.confidence ?? 0).toFixed(2).padEnd(6);
console.log(` ${sev} ${rule} ${locCol} ${conf} ${result.message}`);
});
}
renderListGroup(rows, options) {
rows.forEach((result) => {
const isError = result.type === 'error';
const icon = isError ? this.colorize(colors_1.default.red, 'â') : this.colorize(colors_1.default.yellow, 'â');
const location = result.file ? this.colorize(colors_1.default.gray, ` (${result.file}:${result.line})`) : '';
const sev = (result.severity || '').toLowerCase();
const badge = sev === 'high'
? this.colorize(colors_1.default.bgRed.white, ' HIGH ')
: sev === 'medium'
? this.colorize(colors_1.default.bgYellow.black, ' MED ')
: sev === 'low'
? this.colorize(colors_1.default.bgBlue.white, ' LOW ')
: '';
const rule = result.ruleId ? this.colorize(colors_1.default.gray, ` {${result.ruleId}}`) : '';
const msgColor = isError ? colors_1.default.red : colors_1.default.yellow;
const suppressedIndicator = result.suppressed ? this.colorize(colors_1.default.gray, ' [SUPPRESSED]') : '';
const confText = (options?.showConfidence || options?.verbose)
? this.colorize(colors_1.default.gray, ` (confidence: ${(result.confidence ?? 0).toFixed(2)})`)
: '';
console.log(` ${icon} ${badge} ${this.colorize(msgColor, result.message)}${location}${rule}${confText}${suppressedIndicator}`);
if (result.fix) {
console.log(` ${this.brand('đǎ')} ${this.colorize(colors_1.default.green, result.fix)}`);
}
if (result.suppressed && result.suppressionReason) {
console.log(` ${this.colorize(colors_1.default.gray, 'đ')} ${this.colorize(colors_1.default.italic, 'Suppressed: ' + result.suppressionReason)}`);
}
if (options?.explain && result.ruleId) {
const ruleMeta = rules_1.RULES[result.ruleId];
if (ruleMeta?.impact) {
console.log(` ${this.colorize(colors_1.default.blue, 'đĄ')} ${this.colorize(colors_1.default.italic, ruleMeta.impact)}`);
}
}
if (options?.showContext && result.file && result.line) {
const context = getCodeContext(result.file, result.line);
if (context) {
console.log(` ${this.colorize(colors_1.default.gray, 'ââ Code context:')}`);
context.forEach((line, idx) => {
const lineNum = (result.line - 2 + idx).toString().padStart(3);
const isTarget = idx === 2;
const marker = isTarget ? this.colorize(colors_1.default.red, 'âē') : this.colorize(colors_1.default.gray, ' ');
const lineColor = isTarget ? colors_1.default.yellow : colors_1.default.gray;
console.log(` ${this.colorize(colors_1.default.gray, 'â')} ${marker} ${this.colorize(lineColor, lineNum)} ${this.colorize(lineColor, line)}`);
});
console.log(` ${this.colorize(colors_1.default.gray, 'ââ')}`);
}
}
});
}
applyResultFilters(results, options) {
let filtered = [...results];
if (options?.minSeverity) {
const order = { low: 1, medium: 2, high: 3 };
const min = order[options.minSeverity];
filtered = filtered.filter((r) => order[r.severity] >= min);
}
filtered.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];
});
if (options?.maxIssues && options.maxIssues > 0) {
filtered = filtered.slice(0, options.maxIssues);
}
return filtered;
}
groupResults(results, groupBy) {
return results.reduce((acc, result) => {
let key;
switch (groupBy) {
case 'file':
key = result.file || 'unknown';
break;
case 'rule':
key = result.ruleId || 'unknown';
break;
case 'severity':
key = result.severity || 'unknown';
break;
case 'category':
default:
key = result.category;
break;
}
(acc[key] = acc[key] || []).push(result);
return acc;
}, {});
}
getGroupIcon(groupKey, groupBy) {
if (groupBy === 'category') {
const icons = {
security: 'đ', links: 'đ', accessibility: 'âŋ', performance: 'âĄ', seo: 'đ', development: 'đ ī¸', config: 'âī¸',
};
return icons[groupKey] || 'đ';
}
if (groupBy === 'file')
return 'đ';
if (groupBy === 'rule')
return 'âī¸';
if (groupBy === 'severity') {
const sev = { high: 'đ¨', medium: 'â ī¸', low: 'âšī¸' };
return sev[groupKey] || 'đ';
}
return 'đ';
}
printSummary(results) {
const errors = results.filter((r) => r.type === 'error').length;
const warnings = results.filter((r) => r.type === 'warning').length;
console.log(`\n${this.brand('đǎ')} ${this.colorize(colors_1.default.bold, 'Summary')}: ${this.colorize(colors_1.default.red, errors + ' errors')}, ${this.colorize(colors_1.default.yellow, warnings + ' warnings')}`);
const posture = (0, Posture_1.calculateSecurityPosture)(results);
const postureColor = posture.score >= 80 ? colors_1.default.green : posture.score >= 60 ? colors_1.default.yellow : colors_1.default.red;
const postureBar = this.renderPostureBar(posture.score);
console.log(`${this.brand('đǎ')} ${this.colorize(colors_1.default.bold, 'Security Posture')}: ${this.colorize(postureColor, posture.score + '/100')} ${postureBar}`);
console.log(` ${this.colorize(colors_1.default.gray, posture.summary)}`);
if (errors > 0)
this.logger.error('Critical issues found that should be fixed immediately');
else
this.logger.success('0 critical findings');
}
renderPostureBar(score) {
const width = 20;
const filled = Math.round((score / 100) * width);
const empty = width - filled;
const filledChar = this.useColor ? colors_1.default.green('â') : 'â';
const emptyChar = this.useColor ? colors_1.default.gray('â') : 'â';
return `[${filledChar.repeat(filled)}${emptyChar.repeat(empty)}]`;
}
printContextualGuidance(results, options) {
if (options?.json || options?.interactive || options?.quiet)
return;
const active = results.filter((r) => !r.suppressed);
const total = active.length;
const high = active.filter((r) => r.severity === 'high').length;
const criticalErrors = active.filter((r) => r.type === 'error' && r.severity === 'high').length;
const lowConfidence = active.filter((r) => (r.confidence ?? 1) < 0.8).length;
const suppressed = results.filter((r) => r.suppressed).length;
const suggestions = [];
if (total === 0) {
if (suppressed > 0) {
suggestions.push(`${this.brand('đǎ')} All issues suppressed. Use ${this.colorize(colors_1.default.cyan, '--show-suppressed')} to review.`);
}
else {
suggestions.push(`${this.brand('đǎ')} No issues found! For complete analysis: ${this.colorize(colors_1.default.cyan, 'ubon scan')}`);
}
}
else {
if (criticalErrors > 0) {
suggestions.push(`${this.colorize(colors_1.default.red, 'đ¨')} Critical issues need immediate attention! Try: ${this.colorize(colors_1.default.cyan, 'ubon scan --interactive')}`);
}
else if (high > 0) {
suggestions.push(`${this.colorize(colors_1.default.yellow, 'â ī¸')} High severity issues found. Focus first: ${this.colorize(colors_1.default.cyan, 'ubon check --focus-critical')}`);
}
if (total > 20) {
suggestions.push(`${this.colorize(colors_1.default.blue, 'đĄ')} Found ${total} issues. Focus on most critical: ${this.colorize(colors_1.default.cyan, 'ubon check --max-issues 5 --group-by severity')}`);
}
if (lowConfidence > total * 0.5) {
suggestions.push(`${this.colorize(colors_1.default.blue, 'đĄ')} Many low-confidence findings. Try: ${this.colorize(colors_1.default.cyan, 'ubon check --min-confidence 0.9')}`);
}
if (total > 0 && total <= 15) {
suggestions.push(`${this.colorize(colors_1.default.green, 'đ¤')} Share with AI: Copy output and ask "Help me fix these ${total} issues, starting with high severity"`);
}
const fixable = active.filter((r) => r.fixEdits && r.fixEdits.length > 0).length;
if (fixable > 0) {
suggestions.push(`${this.colorize(colors_1.default.green, 'đ§')} ${fixable} issues can be auto-fixed: ${this.colorize(colors_1.default.cyan, 'ubon check --apply-fixes')}`);
}
}
if (suggestions.length > 0) {
console.log('');
suggestions.forEach((s) => console.log(s));
}
}
}
exports.HumanReporter = HumanReporter;
function getCodeContext(filePath, lineNumber) {
try {
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
const lines = content.split('\n');
const startLine = Math.max(0, lineNumber - 3);
const endLine = Math.min(lines.length - 1, lineNumber + 1);
const out = [];
for (let i = startLine; i <= endLine; i++)
out.push(lines[i] || '');
return out.length > 0 ? out : null;
}
catch {
return null;
}
}
//# sourceMappingURL=HumanReporter.js.map