@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
499 lines (429 loc) • 15.4 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 - SARIF Reporter
*
* Generates SARIF 2.1.0 format reports for GitHub Security tab integration.
* Schema: https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json
*
* IMPORTANT: Layer 8 is READ-ONLY by default. It detects but does not transform
* unless explicitly requested (quarantine mode). This follows the NeuroLint
* principle of "never break code".
*/
;
const path = require('path');
const { LAYER_8_VERSION, SEVERITY_LEVELS, IOC_CATEGORIES } = require('../constants');
const SeverityCalculator = require('../utils/severity-calculator');
class SARIFReporter {
constructor(options = {}) {
this.includeContext = options.includeContext !== false;
this.prettyPrint = options.prettyPrint !== false;
this.toolName = options.toolName || 'NeuroLint Layer 8 Security Forensics';
this.toolVersion = options.toolVersion || LAYER_8_VERSION;
this.informationUri = options.informationUri || 'https://github.com/neurolint/neurolint';
}
generateReport(scanResult, options = {}) {
const findings = scanResult.findings || [];
const rules = this.generateRules(findings);
const results = this.generateResults(findings, options);
const sarifReport = {
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
version: '2.1.0',
runs: [
{
tool: {
driver: {
name: this.toolName,
version: this.toolVersion,
informationUri: this.informationUri,
rules: rules,
properties: {
layer: 8,
category: 'Security Forensics',
capabilities: [
'post-exploitation-detection',
'ioc-scanning',
'behavioral-analysis',
'supply-chain-verification'
]
}
}
},
invocations: [
{
executionSuccessful: true,
startTimeUtc: scanResult.stats?.startTime || new Date().toISOString(),
endTimeUtc: scanResult.stats?.endTime || new Date().toISOString(),
workingDirectory: {
uri: this.toFileUri(options.targetPath || process.cwd())
},
properties: {
filesScanned: scanResult.stats?.filesScanned || 0,
filesSkipped: scanResult.stats?.filesSkipped || 0,
executionTimeMs: scanResult.stats?.totalExecutionTime || 0,
scanMode: options.mode || 'standard'
}
}
],
results: results,
automationDetails: {
id: `neurolint-security-scan/${Date.now()}`,
guid: this.generateGuid(),
correlationGuid: options.correlationId || this.generateGuid()
},
properties: {
severity: SeverityCalculator.calculateOverallSeverity(findings)
}
}
]
};
if (this.prettyPrint) {
return JSON.stringify(sarifReport, null, 2);
}
return JSON.stringify(sarifReport);
}
generateRules(findings) {
const rulesMap = new Map();
for (const finding of findings) {
if (!rulesMap.has(finding.signatureId)) {
rulesMap.set(finding.signatureId, {
id: finding.signatureId,
name: this.sanitizeRuleName(finding.signatureName || finding.signatureId),
shortDescription: {
text: finding.signatureName || finding.signatureId
},
fullDescription: {
text: finding.description || `Security issue detected: ${finding.signatureName}`
},
helpUri: this.getHelpUri(finding),
help: {
text: this.generateHelpText(finding),
markdown: this.generateHelpMarkdown(finding)
},
defaultConfiguration: {
level: this.mapSeverityToLevel(finding.severity),
enabled: true
},
properties: {
category: finding.category || 'security',
severity: finding.severity,
securitySeverity: this.getSecuritySeverityScore(finding.severity),
precision: this.getPrecision(finding),
tags: this.generateTags(finding)
}
});
}
}
return Array.from(rulesMap.values());
}
generateResults(findings, options = {}) {
return findings.map((finding, index) => {
const result = {
ruleId: finding.signatureId,
ruleIndex: this.getRuleIndex(finding.signatureId, findings),
level: this.mapSeverityToLevel(finding.severity),
message: {
text: this.formatMessage(finding),
markdown: this.formatMessageMarkdown(finding)
},
locations: [
{
physicalLocation: {
artifactLocation: {
uri: this.toRelativeUri(finding.file, options.targetPath),
uriBaseId: '%SRCROOT%'
},
region: {
startLine: finding.line || 1,
startColumn: finding.column || 1,
endLine: finding.endLine || finding.line || 1,
endColumn: finding.endColumn || (finding.column || 1) + (finding.matchedText?.length || 1),
snippet: finding.matchedText ? {
text: finding.matchedText.substring(0, 500)
} : undefined
},
contextRegion: this.includeContext && finding.context ? {
startLine: Math.max(1, (finding.line || 1) - 3),
endLine: (finding.line || 1) + 3,
snippet: {
text: finding.context.map(c => c.content).join('\n')
}
} : undefined
}
}
],
fingerprints: {
primaryLocationLineHash: this.generateFingerprint(finding)
},
partialFingerprints: {
'primaryLocationLineHash/v1': this.generatePartialFingerprint(finding)
},
properties: {
findingId: finding.id || `finding-${index + 1}`,
confidence: finding.confidence || 0.8,
category: finding.category,
references: finding.references || [],
remediation: finding.remediation,
timestamp: finding.timestamp || new Date().toISOString()
}
};
if (finding.fixes) {
result.fixes = this.generateFixes(finding);
}
if (finding.relatedLocations && finding.relatedLocations.length > 0) {
result.relatedLocations = finding.relatedLocations.map((loc, idx) => ({
id: idx,
physicalLocation: {
artifactLocation: {
uri: this.toRelativeUri(loc.file, options.targetPath),
uriBaseId: '%SRCROOT%'
},
region: {
startLine: loc.line || 1,
startColumn: loc.column || 1
}
},
message: {
text: loc.message || 'Related location'
}
}));
}
return result;
});
}
generateFixes(finding) {
if (!finding.fixes || finding.fixes.length === 0) return undefined;
return finding.fixes.map((fix, index) => ({
description: {
text: fix.description || `Fix ${index + 1}`
},
artifactChanges: [
{
artifactLocation: {
uri: this.toRelativeUri(finding.file),
uriBaseId: '%SRCROOT%'
},
replacements: fix.replacements || []
}
]
}));
}
mapSeverityToLevel(severity) {
const levelMap = {
critical: 'error',
high: 'error',
medium: 'warning',
low: 'note',
info: 'note'
};
return levelMap[severity] || 'warning';
}
getSecuritySeverityScore(severity) {
const scoreMap = {
critical: '9.0',
high: '7.0',
medium: '4.0',
low: '2.0',
info: '0.0'
};
return scoreMap[severity] || '4.0';
}
getPrecision(finding) {
if (finding.confidence >= 0.9) return 'very-high';
if (finding.confidence >= 0.75) return 'high';
if (finding.confidence >= 0.5) return 'medium';
return 'low';
}
generateTags(finding) {
const tags = ['security'];
if (finding.category) {
tags.push(finding.category);
}
if (finding.references) {
for (const ref of finding.references) {
if (ref.startsWith('CVE-')) {
tags.push(ref);
}
if (ref.startsWith('MITRE')) {
tags.push('mitre-attack');
}
if (ref.includes('OWASP')) {
tags.push('owasp');
}
}
}
const severityTag = `severity/${finding.severity}`;
tags.push(severityTag);
return tags;
}
getHelpUri(finding) {
if (finding.references && finding.references.length > 0) {
const cve = finding.references.find(r => r.startsWith('CVE-'));
if (cve) {
return `https://nvd.nist.gov/vuln/detail/${cve}`;
}
}
return `https://neurolint.dev/docs/security/${finding.signatureId}`;
}
generateHelpText(finding) {
let helpText = finding.description || '';
if (finding.remediation) {
helpText += `\n\nRemediation: ${finding.remediation}`;
}
if (finding.references && finding.references.length > 0) {
helpText += '\n\nReferences:';
for (const ref of finding.references) {
helpText += `\n- ${ref}`;
}
}
return helpText;
}
generateHelpMarkdown(finding) {
let markdown = `## ${finding.signatureName || finding.signatureId}\n\n`;
markdown += `${finding.description || 'Security issue detected.'}\n\n`;
if (finding.remediation) {
markdown += `### Remediation\n\n${finding.remediation}\n\n`;
}
if (finding.references && finding.references.length > 0) {
markdown += '### References\n\n';
for (const ref of finding.references) {
if (ref.startsWith('CVE-')) {
markdown += `- [${ref}](https://nvd.nist.gov/vuln/detail/${ref})\n`;
} else if (ref.startsWith('MITRE')) {
const technique = ref.replace('MITRE ', '');
markdown += `- [${ref}](https://attack.mitre.org/techniques/${technique}/)\n`;
} else {
markdown += `- ${ref}\n`;
}
}
}
return markdown;
}
formatMessage(finding) {
let message = finding.signatureName || finding.signatureId;
if (finding.description) {
message += `: ${finding.description}`;
}
if (finding.matchedText && finding.matchedText !== '[Behavioral Pattern]') {
const truncated = finding.matchedText.length > 100
? finding.matchedText.substring(0, 97) + '...'
: finding.matchedText;
message += ` (matched: "${truncated}")`;
}
return message;
}
formatMessageMarkdown(finding) {
let markdown = `**${finding.signatureName || finding.signatureId}**`;
if (finding.description) {
markdown += `\n\n${finding.description}`;
}
if (finding.matchedText && finding.matchedText !== '[Behavioral Pattern]') {
const truncated = finding.matchedText.length > 100
? finding.matchedText.substring(0, 97) + '...'
: finding.matchedText;
markdown += `\n\n\`\`\`\n${truncated}\n\`\`\``;
}
if (finding.remediation) {
markdown += `\n\n**Fix:** ${finding.remediation}`;
}
return markdown;
}
sanitizeRuleName(name) {
return name
.replace(/[^a-zA-Z0-9\s\-_]/g, '')
.replace(/\s+/g, '-')
.substring(0, 100);
}
toFileUri(filePath) {
if (!filePath) return 'file:///';
const normalized = filePath.replace(/\\/g, '/');
if (normalized.startsWith('/')) {
return `file://${normalized}`;
}
return `file:///${normalized}`;
}
toRelativeUri(filePath, basePath) {
if (!filePath) return '';
let normalized = filePath.replace(/\\/g, '/');
if (basePath) {
const normalizedBase = basePath.replace(/\\/g, '/');
if (normalized.startsWith(normalizedBase)) {
normalized = normalized.substring(normalizedBase.length);
if (normalized.startsWith('/')) {
normalized = normalized.substring(1);
}
}
}
return normalized;
}
getRuleIndex(signatureId, findings) {
const uniqueIds = [...new Set(findings.map(f => f.signatureId))];
return uniqueIds.indexOf(signatureId);
}
generateGuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
generateFingerprint(finding) {
const crypto = require('crypto');
const data = `${finding.signatureId}:${finding.file}:${finding.line}:${finding.column}`;
return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
}
generatePartialFingerprint(finding) {
const crypto = require('crypto');
const data = `${finding.signatureId}:${finding.file}`;
return crypto.createHash('sha256').update(data).digest('hex').substring(0, 16);
}
writeToFile(scanResult, filePath, options = {}) {
const fs = require('fs');
const report = this.generateReport(scanResult, options);
fs.writeFileSync(filePath, report, 'utf8');
return filePath;
}
generateForGitHub(scanResult, options = {}) {
return this.generateReport(scanResult, {
...options,
prettyPrint: false
});
}
validateSchema(report) {
try {
const parsed = typeof report === 'string' ? JSON.parse(report) : report;
const required = ['$schema', 'version', 'runs'];
for (const field of required) {
if (!(field in parsed)) {
return { valid: false, error: `Missing required field: ${field}` };
}
}
if (parsed.version !== '2.1.0') {
return { valid: false, error: `Invalid SARIF version: ${parsed.version}` };
}
if (!Array.isArray(parsed.runs) || parsed.runs.length === 0) {
return { valid: false, error: 'runs must be a non-empty array' };
}
const run = parsed.runs[0];
if (!run.tool || !run.tool.driver) {
return { valid: false, error: 'Missing tool.driver in run' };
}
return { valid: true };
} catch (error) {
return { valid: false, error: error.message };
}
}
}
module.exports = SARIFReporter;