@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
843 lines (730 loc) • 29.9 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
*
* Post-exploitation detection, compromise scanning, and incident response.
*
* CORE PRINCIPLES (following NeuroLint architecture):
*
* 1. READ-ONLY BY DEFAULT: Layer 8 is primarily a detection layer.
* It analyzes code but does NOT modify it unless explicitly in quarantine mode.
*
* 2. NEVER BREAK CODE: Even in quarantine mode, we follow the AST → Regex → Revert
* pattern. If validation fails, we revert to the original state.
*
* 3. LAYER CONTRACT: Implements the standard NeuroLint layer interface:
* - analyze(code, options) - Detect issues (primary function)
* - transform(code, options) - Quarantine/neutralize (optional, requires explicit flag)
*
* 4. INTEGRATION WITH LAYER 7: Emits securityFindings[] that Layer 7 (Adaptive)
* can consume for pattern learning. Layer 8 owns detection, Layer 7 observes.
*/
;
const fs = require('fs').promises;
const path = require('path');
const DetectorOrchestrator = require('./detectors');
const { CLIReporter, JSONReporter } = require('./reporters');
const SeverityCalculator = require('./utils/severity-calculator');
const HashUtils = require('./utils/hash-utils');
const ErrorAggregator = require('./utils/error-aggregator');
const {
LAYER_8_VERSION,
DETECTION_MODES,
MODE_CONFIGURATIONS,
EXCLUDED_PATHS_DEFAULT,
SEVERITY_LEVELS
} = require('./constants');
const LAYER_INFO = {
id: 8,
name: 'security-forensics',
version: LAYER_8_VERSION,
description: 'Post-exploitation detection and incident response',
supportsAST: true,
isReadOnly: true
};
class Layer8SecurityForensics {
constructor(options = {}) {
this.options = {
mode: options.mode || DETECTION_MODES.STANDARD,
verbose: options.verbose || false,
dryRun: options.dryRun !== false,
quarantine: options.quarantine || false,
failOn: options.failOn || 'critical',
exclude: options.exclude || EXCLUDED_PATHS_DEFAULT,
include: options.include || ['**/*.{js,jsx,ts,tsx,json,mjs,cjs}'],
...options
};
this.errorAggregator = new ErrorAggregator({ verbose: this.options.verbose });
this.detector = new DetectorOrchestrator({
mode: this.options.mode,
verbose: this.options.verbose,
customSignatures: this.options.customSignatures
});
this.cliReporter = new CLIReporter({
verbose: this.options.verbose,
colors: this.options.colors,
showContext: this.options.showContext
});
this.jsonReporter = new JSONReporter({
includeContext: this.options.includeContext,
prettyPrint: this.options.prettyPrint
});
}
async analyze(code, options = {}) {
const filePath = options.filename || options.filePath || 'unknown';
const startTime = Date.now();
try {
const result = await this.detector.scanFile(code, filePath, options);
const severity = SeverityCalculator.calculateOverallSeverity(result.findings);
return {
layer: LAYER_INFO.id,
layerName: LAYER_INFO.name,
success: true,
issues: result.findings.map(f => ({
type: f.signatureId,
message: f.description,
severity: f.severity,
location: { line: f.line, column: f.column },
fix: f.remediation
})),
securityFindings: result.findings,
summary: {
overallSeverity: severity.level,
findingsCount: result.findings.length,
breakdown: severity.breakdown,
shouldFail: SeverityCalculator.shouldFailBuild(severity, this.options.failOn)
},
executionTime: Date.now() - startTime
};
} catch (error) {
return {
layer: LAYER_INFO.id,
layerName: LAYER_INFO.name,
success: false,
issues: [],
securityFindings: [],
error: error.message,
executionTime: Date.now() - startTime
};
}
}
async transform(code, options = {}) {
if (!this.options.quarantine) {
// Even in read-only mode, perform analysis to emit securityFindings for Layer 7
const analysis = await this.analyze(code, options);
return {
success: true,
code: code,
originalCode: code,
changeCount: 0,
securityFindings: analysis.securityFindings || [],
message: 'Layer 8 is read-only. Enable quarantine mode for code modification.',
executionTime: 0
};
}
const originalCode = code;
const startTime = Date.now();
try {
const analysis = await this.analyze(code, options);
if (analysis.securityFindings.length === 0) {
return {
success: true,
code: originalCode,
originalCode,
changeCount: 0,
securityFindings: [],
message: 'No security issues to quarantine',
executionTime: Date.now() - startTime
};
}
let modifiedCode = code;
let changeCount = 0;
for (const finding of analysis.securityFindings) {
if (finding.severity === SEVERITY_LEVELS.CRITICAL) {
const result = this.neutralizeThreat(modifiedCode, finding);
if (result.changed) {
modifiedCode = result.code;
changeCount++;
}
}
}
const validation = this.validateTransformation(originalCode, modifiedCode);
if (validation.shouldRevert) {
return {
success: false,
code: originalCode,
originalCode,
changeCount: 0,
securityFindings: analysis.securityFindings || [],
revertReason: validation.reason,
message: 'Transformation reverted to preserve code integrity',
executionTime: Date.now() - startTime
};
}
return {
success: true,
code: modifiedCode,
originalCode,
changeCount,
securityFindings: analysis.securityFindings || [],
message: `Neutralized ${changeCount} critical threats`,
executionTime: Date.now() - startTime
};
} catch (error) {
return {
success: false,
code: originalCode,
originalCode,
changeCount: 0,
securityFindings: [],
error: error.message,
message: 'Transformation failed, original code preserved',
executionTime: Date.now() - startTime
};
}
}
neutralizeThreat(code, finding) {
const lines = code.split('\n');
const lineIndex = finding.line - 1;
if (lineIndex < 0 || lineIndex >= lines.length) {
return { changed: false, code };
}
const originalLine = lines[lineIndex];
const commentPrefix = '/* NEUROLINT-QUARANTINE: ';
const commentSuffix = ` [${finding.signatureId}] */`;
if (originalLine.includes('NEUROLINT-QUARANTINE')) {
return { changed: false, code };
}
lines[lineIndex] = commentPrefix + originalLine + commentSuffix;
return {
changed: true,
code: lines.join('\n')
};
}
validateTransformation(before, after) {
if (before === after) {
return { shouldRevert: false, reason: 'No changes made' };
}
try {
const parser = require('@babel/parser');
parser.parse(after, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
allowImportExportEverywhere: true,
strictMode: false
});
return { shouldRevert: false };
} catch (error) {
return {
shouldRevert: true,
reason: `Syntax error after transformation: ${error.message}`
};
}
}
async scanCompromise(targetPath, options = {}) {
const startTime = Date.now();
const files = await this.getFiles(targetPath, options);
if (options.verbose) {
console.log(`Scanning ${files.length} files for indicators of compromise...`);
}
const fileContents = await Promise.all(
files.map(async (filePath) => {
try {
const code = await fs.readFile(filePath, 'utf8');
return { code, filePath };
} catch (error) {
return null;
}
})
);
const validFiles = fileContents.filter(f => f !== null);
const result = await this.detector.scanMultipleFiles(validFiles, {
concurrency: options.concurrency || 10,
onProgress: options.onProgress
});
result.targetPath = targetPath;
result.scanType = 'compromise-scan';
result.mode = this.options.mode;
return result;
}
async createBaseline(targetPath, options = {}) {
const outputPath = options.output || path.join(targetPath, '.neurolint', 'security-baseline.json');
await fs.mkdir(path.dirname(outputPath), { recursive: true });
const hashes = await HashUtils.hashDirectory(targetPath, {
exclude: this.options.exclude,
onProgress: options.onProgress
});
const baseline = {
version: LAYER_8_VERSION,
created: new Date().toISOString(),
projectRoot: path.resolve(targetPath),
fileCount: Object.keys(hashes).length,
files: hashes,
config: {
excludePatterns: this.options.exclude
}
};
await fs.writeFile(outputPath, JSON.stringify(baseline, null, 2), 'utf8');
return {
success: true,
baselinePath: outputPath,
fileCount: baseline.fileCount,
created: baseline.created
};
}
async compareBaseline(targetPath, baselinePath, options = {}) {
const baselineContent = await fs.readFile(baselinePath, 'utf8');
const baseline = JSON.parse(baselineContent);
const currentHashes = await HashUtils.hashDirectory(targetPath, {
exclude: this.options.exclude,
onProgress: options.onProgress
});
const comparison = HashUtils.compareHashes(baseline.files, currentHashes);
return {
success: true,
baselineDate: baseline.created,
comparison,
summary: {
added: comparison.added.length,
removed: comparison.removed.length,
modified: comparison.modified.length,
unchanged: comparison.unchanged.length,
hasChanges: comparison.hasChanges
}
};
}
async getFiles(targetPath, options = {}) {
const files = [];
const include = options.include || this.options.include;
const exclude = options.exclude || this.options.exclude;
const self = this;
async function walkDir(dir) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.relative(targetPath, fullPath).replace(/\\/g, '/');
const shouldExclude = exclude.some(pattern => {
// Normalize pattern for cross-platform compatibility
const normalizedPattern = pattern.replace(/\\/g, '/');
if (normalizedPattern.includes('**')) {
// Extract the core directory/file name from patterns like **/.neurolint/**
// This handles patterns like **/.neurolint/**, **/node_modules/**, etc.
const coreMatch = normalizedPattern.match(/\*\*\/([^/*]+)/);
if (coreMatch) {
const coreName = coreMatch[1];
// Check if the path contains this directory/file name as a path segment
// This works for both root-level and nested paths on all platforms
const pathParts = relativePath.split('/');
if (pathParts.some(part => part === coreName)) {
return true;
}
}
// Fallback: convert glob to regex for complex patterns
const simplePattern = normalizedPattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*');
// Test with optional leading content for root-level matches
return new RegExp(simplePattern).test(relativePath) ||
new RegExp('^' + simplePattern.replace(/^\.\*\//, '')).test(relativePath);
}
// Simple pattern: just check if path includes the pattern
const cleanPattern = normalizedPattern.replace(/\*\*/g, '').replace(/\*/g, '').replace(/\//g, '');
return relativePath.includes(cleanPattern);
});
if (shouldExclude) continue;
if (entry.isDirectory()) {
await walkDir(fullPath);
} else if (entry.isFile()) {
const ext = path.extname(entry.name);
const validExtensions = ['.js', '.jsx', '.ts', '.tsx', '.json', '.mjs', '.cjs'];
if (validExtensions.includes(ext)) {
files.push(fullPath);
}
}
}
} catch (error) {
if (self.errorAggregator) {
self.errorAggregator.addError(error, {
phase: 'file-discovery',
directory: dir
});
}
}
}
await walkDir(targetPath);
return files;
}
async incidentResponse(targetPath, options = {}) {
const startTime = Date.now();
const results = {
timestamp: new Date().toISOString(),
targetPath: path.resolve(targetPath),
mode: options.mode || 'full',
phases: {},
summary: {},
recommendations: []
};
const onProgress = options.onProgress || (() => {});
try {
onProgress({ phase: 'code-scan', status: 'starting', message: 'Scanning for indicators of compromise...' });
const scanResult = await this.scanCompromise(targetPath, {
verbose: options.verbose,
onProgress: (p) => onProgress({ phase: 'code-scan', ...p })
});
results.phases.codeScan = {
success: true,
findings: scanResult.findings || [],
stats: scanResult.stats
};
onProgress({ phase: 'code-scan', status: 'complete', findings: scanResult.findings?.length || 0 });
if (options.includeTimeline !== false) {
onProgress({ phase: 'timeline', status: 'starting', message: 'Reconstructing git timeline...' });
try {
const { TimelineReconstructor } = require('./forensics');
const timelineReconstructor = new TimelineReconstructor({
verbose: options.verbose,
lookbackDays: options.lookbackDays || 30,
maxCommits: options.maxCommits || 100
});
const timelineResult = timelineReconstructor.reconstructTimeline(targetPath, {
includeAll: options.includeAllCommits,
detectForcesPush: true
});
results.phases.timeline = {
success: timelineResult.success,
timeline: timelineResult.timeline || [],
findings: timelineResult.findings || [],
stats: timelineResult.stats
};
onProgress({ phase: 'timeline', status: 'complete', findings: timelineResult.findings?.length || 0 });
} catch (error) {
results.phases.timeline = {
success: false,
error: error.message,
findings: []
};
onProgress({ phase: 'timeline', status: 'error', error: error.message });
}
}
if (options.includeDependencies !== false) {
onProgress({ phase: 'dependencies', status: 'starting', message: 'Analyzing dependencies...' });
try {
const DependencyDiffer = require('./detectors/dependency-differ');
const dependencyDiffer = new DependencyDiffer({
verbose: options.verbose,
checkTyposquatting: true,
checkIntegrity: true
});
const depFindings = dependencyDiffer.analyze(targetPath, {
baseline: options.dependencyBaseline
});
results.phases.dependencies = {
success: true,
findings: depFindings || []
};
onProgress({ phase: 'dependencies', status: 'complete', findings: depFindings?.length || 0 });
} catch (error) {
results.phases.dependencies = {
success: false,
error: error.message,
findings: []
};
onProgress({ phase: 'dependencies', status: 'error', error: error.message });
}
}
if (options.includeBehavioral !== false) {
onProgress({ phase: 'behavioral', status: 'starting', message: 'Running behavioral analysis...' });
try {
const BehavioralAnalyzer = require('./detectors/behavioral-analyzer');
const behavioralAnalyzer = new BehavioralAnalyzer({
verbose: options.verbose,
includeContext: true
});
const files = await this.getFiles(targetPath, options);
const behavioralFindings = [];
const skippedFiles = [];
let analyzedCount = 0;
for (const filePath of files) {
try {
const fsSync = require('fs');
const code = fsSync.readFileSync(filePath, 'utf8');
const fileFindings = behavioralAnalyzer.analyze(code, filePath, options);
behavioralFindings.push(...fileFindings);
analyzedCount++;
} catch (e) {
skippedFiles.push({ file: filePath, error: e.message });
}
}
const hasPartialCoverage = skippedFiles.length > 0 && files.length > 0;
const coveragePercent = files.length > 0 ? Math.round((analyzedCount / files.length) * 100) : 100;
const minCoverageThreshold = 50;
const phaseSucceeded = coveragePercent >= minCoverageThreshold || files.length === 0;
results.phases.behavioral = {
success: phaseSucceeded,
error: !phaseSucceeded ? `Insufficient coverage: only ${coveragePercent}% of files could be analyzed` : undefined,
findings: behavioralFindings,
stats: {
filesAnalyzed: analyzedCount,
filesSkipped: skippedFiles.length,
coveragePercent
},
partialCoverage: hasPartialCoverage,
skippedFiles: skippedFiles.length > 0 ? skippedFiles.slice(0, 10) : undefined
};
onProgress({ phase: 'behavioral', status: phaseSucceeded ? 'complete' : 'partial', findings: behavioralFindings.length });
} catch (error) {
results.phases.behavioral = {
success: false,
error: error.message,
findings: []
};
onProgress({ phase: 'behavioral', status: 'error', error: error.message });
}
}
const allFindings = [
...(results.phases.codeScan?.findings || []),
...(results.phases.timeline?.findings || []),
...(results.phases.dependencies?.findings || []),
...(results.phases.behavioral?.findings || [])
];
const severityCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
for (const finding of allFindings) {
const sev = finding.severity?.toLowerCase() || 'info';
if (severityCounts.hasOwnProperty(sev)) {
severityCounts[sev]++;
}
}
let riskLevel = 'clean';
if (severityCounts.critical > 0) riskLevel = 'critical';
else if (severityCounts.high > 0) riskLevel = 'high';
else if (severityCounts.medium > 0) riskLevel = 'medium';
else if (severityCounts.low > 0) riskLevel = 'low';
const phasesCompleted = Object.keys(results.phases).filter(p => results.phases[p].success).length;
const phasesTotal = Object.keys(results.phases).length;
const phasesFailed = phasesTotal - phasesCompleted;
const failedPhaseNames = Object.keys(results.phases)
.filter(p => !results.phases[p].success)
.map(p => p);
let overallSuccess = true;
let incompleteReason = null;
if (phasesFailed > 0) {
overallSuccess = false;
incompleteReason = `Phase(s) failed: ${failedPhaseNames.join(', ')}`;
}
if (phasesFailed > 0 && riskLevel === 'clean') {
riskLevel = 'incomplete';
}
results.success = overallSuccess;
results.summary = {
totalFindings: allFindings.length,
severityCounts,
riskLevel,
executionTimeMs: Date.now() - startTime,
phasesCompleted,
phasesTotal,
phasesFailed,
incompleteReason
};
results.recommendations = this.generateIncidentRecommendations(results);
results.allFindings = allFindings;
return results;
} catch (error) {
return {
success: false,
error: error.message,
timestamp: results.timestamp,
targetPath: results.targetPath,
phases: results.phases,
summary: {
totalFindings: 0,
riskLevel: 'unknown',
executionTimeMs: Date.now() - startTime
},
recommendations: []
};
}
}
generateIncidentRecommendations(results) {
const recommendations = [];
const phases = results.phases;
if (phases.codeScan?.findings?.length > 0) {
const criticalCount = phases.codeScan.findings.filter(f => f.severity === 'critical').length;
if (criticalCount > 0) {
recommendations.push({
priority: 'immediate',
action: 'Isolate and investigate critical findings',
details: `${criticalCount} critical security issues detected in code. Immediate investigation required.`,
phase: 'code-scan'
});
}
}
if (phases.timeline?.findings?.length > 0) {
recommendations.push({
priority: 'high',
action: 'Review suspicious commits',
details: `${phases.timeline.findings.length} suspicious changes detected in git history. Verify all commits are legitimate.`,
phase: 'timeline'
});
}
if (phases.dependencies?.findings?.length > 0) {
const malicious = phases.dependencies.findings.filter(f =>
f.signatureName?.includes('Malicious') || f.signatureName?.includes('Typosquatting')
).length;
if (malicious > 0) {
recommendations.push({
priority: 'immediate',
action: 'Remove malicious or suspicious dependencies',
details: `${malicious} potentially malicious packages detected. Remove immediately and audit usage.`,
phase: 'dependencies'
});
}
}
if (phases.behavioral?.findings?.length > 0) {
const dangerousPatterns = phases.behavioral.findings.filter(f =>
f.severity === 'critical' || f.severity === 'high'
).length;
if (dangerousPatterns > 0) {
recommendations.push({
priority: 'high',
action: 'Review dangerous code patterns',
details: `${dangerousPatterns} dangerous behavioral patterns detected. Review for potential backdoors.`,
phase: 'behavioral'
});
}
}
for (const [phaseName, phaseData] of Object.entries(phases)) {
if (!phaseData.success && phaseData.error) {
recommendations.push({
priority: 'medium',
action: `Address ${phaseName} phase failure`,
details: `The ${phaseName} phase failed: ${phaseData.error}. Results may be incomplete.`,
phase: phaseName
});
} else if (phaseData.partialCoverage && phaseData.stats) {
recommendations.push({
priority: 'low',
action: `Review ${phaseName} partial coverage`,
details: `The ${phaseName} phase analyzed ${phaseData.stats.coveragePercent}% of files (${phaseData.stats.filesSkipped} skipped). Some files could not be analyzed.`,
phase: phaseName
});
}
}
if (recommendations.length === 0) {
recommendations.push({
priority: 'info',
action: 'Continue monitoring',
details: 'No immediate threats detected. Continue regular security monitoring.',
phase: 'general'
});
}
return recommendations.sort((a, b) => {
const priorityOrder = { immediate: 0, high: 1, medium: 2, low: 3, info: 4 };
return (priorityOrder[a.priority] || 5) - (priorityOrder[b.priority] || 5);
});
}
printIncidentReport(results, options = {}) {
const colors = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
const riskColors = {
critical: colors.red,
high: colors.red,
medium: colors.yellow,
low: colors.blue,
clean: colors.green,
unknown: colors.dim
};
console.log(`\n${colors.bold}${colors.cyan}=== INCIDENT RESPONSE REPORT ===${colors.reset}\n`);
console.log(`${colors.dim}Target: ${results.targetPath}${colors.reset}`);
console.log(`${colors.dim}Timestamp: ${results.timestamp}${colors.reset}`);
console.log(`${colors.dim}Execution time: ${results.summary.executionTimeMs}ms${colors.reset}\n`);
const riskColor = riskColors[results.summary.riskLevel] || colors.dim;
console.log(`${colors.bold}Risk Level: ${riskColor}${results.summary.riskLevel.toUpperCase()}${colors.reset}\n`);
console.log(`${colors.bold}Findings Summary:${colors.reset}`);
console.log(` Critical: ${colors.red}${results.summary.severityCounts.critical}${colors.reset}`);
console.log(` High: ${colors.red}${results.summary.severityCounts.high}${colors.reset}`);
console.log(` Medium: ${colors.yellow}${results.summary.severityCounts.medium}${colors.reset}`);
console.log(` Low: ${colors.blue}${results.summary.severityCounts.low}${colors.reset}`);
console.log(` Total: ${colors.bold}${results.summary.totalFindings}${colors.reset}\n`);
console.log(`${colors.bold}Phases Completed:${colors.reset} ${results.summary.phasesCompleted}/${results.summary.phasesTotal}\n`);
for (const [phaseName, phaseData] of Object.entries(results.phases)) {
const status = phaseData.success ? `${colors.green}[COMPLETE]${colors.reset}` : `${colors.red}[FAILED]${colors.reset}`;
const findingsCount = phaseData.findings?.length || 0;
console.log(` ${status} ${phaseName}: ${findingsCount} findings`);
if (phaseData.error) {
console.log(` ${colors.dim}Error: ${phaseData.error}${colors.reset}`);
}
}
if (results.recommendations.length > 0) {
console.log(`\n${colors.bold}${colors.yellow}Recommendations:${colors.reset}\n`);
for (const rec of results.recommendations) {
const priorityColor = rec.priority === 'immediate' ? colors.red :
rec.priority === 'high' ? colors.yellow :
colors.dim;
console.log(` ${priorityColor}[${rec.priority.toUpperCase()}]${colors.reset} ${rec.action}`);
console.log(` ${colors.dim}${rec.details}${colors.reset}\n`);
}
}
if (options.verbose && results.allFindings?.length > 0) {
console.log(`\n${colors.bold}Detailed Findings:${colors.reset}\n`);
for (const finding of results.allFindings.slice(0, 20)) {
const sevColor = finding.severity === 'critical' || finding.severity === 'high' ? colors.red :
finding.severity === 'medium' ? colors.yellow : colors.dim;
console.log(` ${sevColor}[${finding.severity?.toUpperCase() || 'INFO'}]${colors.reset} ${finding.signatureName || finding.signatureId}`);
if (finding.file) {
console.log(` ${colors.dim}File: ${finding.file}:${finding.line || 1}${colors.reset}`);
}
if (finding.description) {
console.log(` ${finding.description}`);
}
console.log('');
}
if (results.allFindings.length > 20) {
console.log(` ${colors.dim}... and ${results.allFindings.length - 20} more findings${colors.reset}\n`);
}
}
console.log(`${colors.dim}─${'─'.repeat(59)}${colors.reset}\n`);
}
generateIncidentJSONReport(results, options = {}) {
return JSON.stringify(results, null, options.prettyPrint !== false ? 2 : 0);
}
generateCLIReport(scanResult, options = {}) {
return this.cliReporter.generateReport(scanResult, options);
}
generateJSONReport(scanResult, options = {}) {
return this.jsonReporter.generateReport(scanResult, options);
}
printReport(scanResult, options = {}) {
return this.cliReporter.printReport(scanResult, options);
}
static getLayerInfo() {
return LAYER_INFO;
}
}
module.exports = Layer8SecurityForensics;
module.exports.LAYER_INFO = LAYER_INFO;
module.exports.DETECTION_MODES = DETECTION_MODES;
module.exports.SEVERITY_LEVELS = SEVERITY_LEVELS;