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.

308 lines â€ĸ 16.2 kB
"use strict"; 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