@neurolint/cli
Version:
NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support
304 lines (245 loc) • 10.1 kB
JavaScript
/**
* Copyright (c) 2025 NeuroLint
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Layer 8: Security Forensics - CLI Reporter
*
* Terminal output for security scan results following NeuroLint layer styling.
* Uses [INFO], [SUCCESS], [WARNING], [ERROR] prefixes consistent with other layers.
*/
;
const path = require('path');
const { SEVERITY_LEVELS, SEVERITY_WEIGHTS, IOC_CATEGORIES } = require('../constants');
const SeverityCalculator = require('../utils/severity-calculator');
const { COLORS, SYMBOLS, isColorSupported } = require('../../../shared-core/cli-output');
const HINTS = {
critical: 'Isolate affected systems immediately and review access logs.',
high: 'Prioritize remediation and audit recent code changes.',
medium: 'Schedule review in your next security sprint.',
low: 'Consider addressing during regular maintenance.',
malicious_code: 'Check git history for unauthorized commits.',
data_exfiltration: 'Review network logs and API access patterns.',
backdoor: 'Rotate all credentials and API keys immediately.',
suspicious_dependency: 'Audit package.json and lock files for tampering.'
};
class CLIReporter {
constructor(options = {}) {
this.verbose = options.verbose || false;
this.useColors = options.colors !== false && isColorSupported();
this.showContext = options.showContext !== false;
this.maxContextLines = options.maxContextLines || 3;
this.groupByFile = options.groupByFile !== false;
this.showHints = options.showHints !== false;
}
c(color, text) {
if (!this.useColors) return text;
const colorCode = COLORS[color];
if (!colorCode) return text;
return `${colorCode}${text}${COLORS.reset}`;
}
generateReport(scanResult, options = {}) {
const lines = [];
lines.push(this.generateHeader());
lines.push('');
lines.push(this.generateSummary(scanResult));
lines.push('');
if (scanResult.findings && scanResult.findings.length > 0) {
if (this.groupByFile) {
lines.push(this.generateFindingsByFile(scanResult.findings));
} else {
lines.push(this.generateFindingsBySeverity(scanResult.findings));
}
} else {
lines.push(this.generateCleanReport());
}
lines.push('');
lines.push(this.generateFooter(scanResult));
return lines.join('\n');
}
generateHeader() {
return `${SYMBOLS.info} NeuroLint Security Forensics - Layer 8
${SYMBOLS.info} Post-Exploitation Detection & Incident Response`;
}
generateSummary(scanResult) {
const severity = SeverityCalculator.calculateOverallSeverity(scanResult.findings || []);
const stats = scanResult.stats || {};
const statusText = this.getStatusText(severity.level);
const executionTime = stats.totalExecutionTime
? `${(stats.totalExecutionTime / 1000).toFixed(2)}s`
: 'N/A';
const lines = [
`${SYMBOLS.info} SCAN SUMMARY`,
`${SYMBOLS.info} Status: ${statusText}`,
`${SYMBOLS.info} Files: ${stats.filesScanned || 0} scanned, ${stats.filesSkipped || 0} skipped`,
`${SYMBOLS.info} Duration: ${executionTime}`,
`${SYMBOLS.info} Findings: ${scanResult.findings?.length || 0} total`,
`${SYMBOLS.info} Breakdown: Critical=${severity.breakdown.critical}, High=${severity.breakdown.high}, Medium=${severity.breakdown.medium}, Low=${severity.breakdown.low}`
];
return lines.join('\n');
}
getStatusText(level) {
const statusMap = {
critical: this.c('red', 'CRITICAL'),
high: this.c('yellow', 'COMPROMISED'),
medium: this.c('blue', 'WARNINGS'),
low: this.c('cyan', 'MINOR ISSUES'),
info: this.c('dim', 'INFO'),
clean: this.c('green', 'CLEAN')
};
return statusMap[level] || statusMap.clean;
}
generateFindingsByFile(findings) {
const fileGroups = {};
for (const finding of findings) {
const file = finding.file || 'Unknown';
if (!fileGroups[file]) {
fileGroups[file] = [];
}
fileGroups[file].push(finding);
}
const lines = [];
lines.push(`${SYMBOLS.info} FINDINGS BY FILE`);
lines.push('');
for (const [file, fileFindings] of Object.entries(fileGroups)) {
const sortedFindings = fileFindings.sort((a, b) => {
return (SEVERITY_WEIGHTS[b.severity] || 0) - (SEVERITY_WEIGHTS[a.severity] || 0);
});
const relativePath = file.length > 60 ? '...' + file.slice(-57) : file;
lines.push(`${SYMBOLS.info} File: ${relativePath}`);
for (const finding of sortedFindings) {
lines.push(this.formatFinding(finding));
}
lines.push('');
}
return lines.join('\n');
}
generateFindingsBySeverity(findings) {
const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
const severityGroups = {};
for (const severity of severityOrder) {
severityGroups[severity] = findings.filter(f => f.severity === severity);
}
const lines = [];
lines.push(`${SYMBOLS.info} FINDINGS BY SEVERITY`);
lines.push('');
for (const severity of severityOrder) {
const group = severityGroups[severity];
if (group.length === 0) continue;
const severityLabel = this.getSeverityLabel(severity);
lines.push(`${severityLabel} (${group.length})`);
for (const finding of group) {
lines.push(this.formatFinding(finding));
}
lines.push('');
}
return lines.join('\n');
}
getSeverityLabel(severity) {
const labels = {
critical: `${SYMBOLS.error} CRITICAL`,
high: `${SYMBOLS.warning} HIGH`,
medium: `${SYMBOLS.info} MEDIUM`,
low: `${SYMBOLS.info} LOW`,
info: `${SYMBOLS.info} INFO`
};
return labels[severity] || labels.info;
}
formatFinding(finding) {
const lines = [];
const indent = ' ';
const severityPrefix = {
critical: SYMBOLS.error,
high: SYMBOLS.warning,
medium: SYMBOLS.info,
low: SYMBOLS.info,
info: SYMBOLS.info
};
const prefix = severityPrefix[finding.severity] || SYMBOLS.info;
lines.push(`${indent}${prefix} ${finding.signatureName || finding.signatureId}`);
lines.push(`${indent} ID: ${finding.signatureId}`);
lines.push(`${indent} Location: Line ${finding.line}, Col ${finding.column}`);
if (finding.matchedText && finding.matchedText !== '[Behavioral Pattern]') {
const truncated = finding.matchedText.length > 80
? finding.matchedText.substring(0, 77) + '...'
: finding.matchedText;
lines.push(`${indent} Match: ${truncated}`);
}
if (finding.description) {
lines.push(`${indent} Desc: ${finding.description}`);
}
if (finding.remediation) {
lines.push(`${indent} Fix: ${finding.remediation}`);
}
if (finding.confidence) {
const confidence = Math.round(finding.confidence * 100);
lines.push(`${indent} Confidence: ${confidence}%`);
}
if (this.showHints && finding.severity) {
const hint = this.getHintForFinding(finding);
if (hint) {
lines.push(`${indent} Hint: ${hint}`);
}
}
if (this.showContext && finding.context && finding.context.length > 0) {
lines.push(`${indent} Context:`);
for (const ctx of finding.context.slice(0, this.maxContextLines)) {
const lineNum = ctx.lineNumber.toString().padStart(4);
const marker = ctx.isMatch ? '>' : ' ';
const content = ctx.content.substring(0, 70);
lines.push(`${indent} ${marker} ${lineNum} | ${content}`);
}
}
return lines.join('\n');
}
getHintForFinding(finding) {
if (finding.category && HINTS[finding.category]) {
return HINTS[finding.category];
}
return HINTS[finding.severity] || null;
}
generateCleanReport() {
return `${SYMBOLS.success} No indicators of compromise detected!
${SYMBOLS.info} Your codebase appears clean based on the current scan.
${SYMBOLS.info} For deeper analysis, try: neurolint security:scan-compromise . --mode deep`;
}
generateFooter(scanResult) {
const timestamp = new Date().toISOString();
const findings = scanResult.findings || [];
let actionMessage = '';
if (findings.some(f => f.severity === 'critical')) {
actionMessage = `${SYMBOLS.error} IMMEDIATE ACTION REQUIRED: Critical security issues detected!`;
} else if (findings.some(f => f.severity === 'high')) {
actionMessage = `${SYMBOLS.warning} Review high-severity findings and remediate promptly.`;
} else if (findings.length > 0) {
actionMessage = `${SYMBOLS.info} Review findings and address as appropriate.`;
} else {
actionMessage = `${SYMBOLS.success} Scan completed successfully.`;
}
return `${actionMessage}
${SYMBOLS.info} Scan completed at: ${timestamp}
${SYMBOLS.info} For JSON output: neurolint security:scan-compromise . --json
${SYMBOLS.info} For full report: neurolint security:incident-response . --output ./report`;
}
printReport(scanResult, options = {}) {
const report = this.generateReport(scanResult, options);
const findings = scanResult.findings || [];
if (findings.some(f => f.severity === 'critical' || f.severity === 'high')) {
process.stderr.write(report + '\n');
} else {
process.stdout.write(report + '\n');
}
return report;
}
}
module.exports = CLIReporter;