@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
360 lines (299 loc) • 10.2 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.
*/
;
const path = require('path');
const { IOC_SIGNATURES, SEVERITY_LEVELS, FILE_TYPE_ASSOCIATIONS } = require('../constants');
const { SafeRegex, executeWithTimeout, REGEX_TIMEOUT_MS } = require('../utils/safe-regex');
const ErrorAggregator = require('../utils/error-aggregator');
class SignatureAnalyzer {
constructor(options = {}) {
this.verbose = options.verbose || false;
this.signatures = IOC_SIGNATURES.signatures;
this.customSignatures = options.customSignatures || [];
this.excludePatterns = options.excludePatterns || [];
this.contextLines = options.contextLines || 3;
this.errorAggregator = new ErrorAggregator({ verbose: this.verbose });
if (this.customSignatures.length > 0) {
this.signatures = [...this.signatures, ...this.customSignatures];
}
}
analyze(code, filePath, options = {}) {
const findings = [];
const fileExt = path.extname(filePath).toLowerCase();
const fileName = path.basename(filePath);
const startTime = Date.now();
const normalizedPath = this.normalizePath(filePath);
if (this.shouldSkipFile(normalizedPath, fileName)) {
return {
findings: [],
scanned: false,
reason: 'File excluded from scanning',
executionTime: Date.now() - startTime
};
}
const lines = code.split('\n');
for (const signature of this.signatures) {
try {
if (signature.fileTypes && !signature.fileTypes.some(ft => filePath.endsWith(ft))) {
continue;
}
if (signature.pathPattern) {
const pathPatternMatches = signature.pathPattern.test(normalizedPath);
if (signature.id === 'NEUROLINT-IOC-024') {
if (!pathPatternMatches) {
continue;
}
} else {
if (!pathPatternMatches) {
continue;
}
}
}
const matches = this.findMatches(code, signature, lines);
for (const match of matches) {
if (this.isLikelyFalsePositive(match, code, normalizedPath, signature)) {
if (this.verbose) {
console.log(` [SKIP] False positive: ${signature.id} in ${filePath}`);
}
continue;
}
findings.push({
id: `finding-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
signatureId: signature.id,
signatureName: signature.name,
severity: signature.severity,
category: signature.category,
file: filePath,
line: match.line,
column: match.column,
matchedText: match.text.substring(0, 200),
context: this.getContext(lines, match.line, this.contextLines),
description: signature.description,
remediation: signature.remediation,
references: signature.references || [],
confidence: this.calculateConfidence(match, signature, code),
timestamp: new Date().toISOString()
});
}
} catch (error) {
this.errorAggregator.addError(error, {
phase: 'signature-analysis',
signatureId: signature.id,
file: filePath
});
}
}
return {
findings,
scanned: true,
signatureCount: this.signatures.length,
executionTime: Date.now() - startTime,
errors: this.errorAggregator.hasErrors() ? this.errorAggregator.getSummary() : null
};
}
normalizePath(filePath) {
return filePath.replace(/\\/g, '/');
}
findMatches(code, signature, lines) {
const matches = [];
if (signature.type === 'regex' && signature.pattern) {
try {
const result = executeWithTimeout(signature.pattern, code, REGEX_TIMEOUT_MS * 5);
if (result.timedOut) {
this.errorAggregator.addWarning(
`Regex timeout for ${signature.id}`,
{ pattern: signature.pattern.source?.substring(0, 50) }
);
return matches;
}
if (result.truncated) {
this.errorAggregator.addWarning(
`Input truncated for ${signature.id} (${result.originalLength} bytes)`,
{ file: 'unknown' }
);
}
for (const match of result.matches) {
const beforeMatch = code.substring(0, match.index);
const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
const lastNewline = beforeMatch.lastIndexOf('\n');
const column = match.index - lastNewline;
matches.push({
text: match.text,
index: match.index,
line: lineNumber,
column: column,
fullMatch: match
});
if (matches.length >= 100) {
break;
}
}
} catch (error) {
this.errorAggregator.addError(error, {
phase: 'regex-execution',
signatureId: signature.id
});
}
}
return matches;
}
getContext(lines, lineNumber, contextSize) {
const startLine = Math.max(0, lineNumber - contextSize - 1);
const endLine = Math.min(lines.length, lineNumber + contextSize);
const contextLines = [];
for (let i = startLine; i < endLine; i++) {
contextLines.push({
lineNumber: i + 1,
content: lines[i],
isMatch: i + 1 === lineNumber
});
}
return contextLines;
}
calculateConfidence(match, signature, code) {
let confidence = 0.8;
if (signature.severity === SEVERITY_LEVELS.CRITICAL) {
confidence += 0.1;
}
if (signature.contextRequired) {
confidence -= 0.2;
}
const suspiciousContextPatterns = [
/function\s+\w*(?:backdoor|hack|exploit|shell|payload)/i,
/\/\/\s*(?:todo|fixme|hack)/i,
/require\s*\(\s*['"](?:net|child_process|fs)['"]\s*\)/
];
for (const pattern of suspiciousContextPatterns) {
if (pattern.test(code)) {
confidence += 0.05;
}
}
return Math.min(0.99, Math.max(0.1, confidence));
}
shouldSkipFile(filePath, fileName) {
const normalizedPath = this.normalizePath(filePath);
const skipPatterns = [
/\.test\.(js|ts|jsx|tsx)$/,
/\.spec\.(js|ts|jsx|tsx)$/,
/__tests__/,
/\.stories\.(js|ts|jsx|tsx)$/,
/\.d\.ts$/,
/\.min\.(js|css)$/,
/node_modules/,
/\.git/,
/dist\//,
/build\//,
/coverage\//
];
for (const pattern of skipPatterns) {
if (pattern.test(normalizedPath)) {
return true;
}
}
return false;
}
isLikelyFalsePositive(match, code, filePath, signature) {
const normalizedPath = this.normalizePath(filePath);
if (signature.id === 'NEUROLINT-IOC-011') {
const matchText = match.text.replace(/['"]/g, '');
if (matchText.length < 500) {
return true;
}
if (matchText.includes('.') || matchText.includes('/') || matchText.includes('-')) {
return true;
}
const contextPatterns = [
/(?:jwt|token|key|secret|auth|session)/i,
/data:image\//i,
/sourceMappingURL/i
];
const lines = code.split('\n');
const matchLine = lines[match.line - 1] || '';
for (const pattern of contextPatterns) {
if (pattern.test(matchLine)) {
return true;
}
}
}
if (signature.id === 'NEUROLINT-IOC-007') {
const base64Pattern = /^[A-Za-z0-9+/=]+$/;
const matchText = match.text.replace(/['"]/g, '');
if (matchText.includes('.') || matchText.includes('/')) {
return true;
}
if (/^[A-Za-z0-9+/]+={0,2}$/.test(matchText)) {
try {
const decoded = Buffer.from(matchText, 'base64').toString('utf8');
if (/[\x00-\x08\x0e-\x1f]/.test(decoded)) {
return true;
}
} catch (e) {
return true;
}
}
}
if (signature.id === 'NEUROLINT-IOC-005') {
const legitPatterns = [
/scripts?\//i,
/cli\.(js|ts)/i,
/build\.(js|ts)/i,
/tools?\//i
];
for (const pattern of legitPatterns) {
if (pattern.test(normalizedPath)) {
return true;
}
}
}
try {
const commentPatterns = [
new RegExp(`^\\s*//.*${escapeRegex(match.text.substring(0, 20))}`),
new RegExp(`^\\s*/\\*.*${escapeRegex(match.text.substring(0, 20))}`),
new RegExp(`${escapeRegex(match.text.substring(0, 20))}.*\\*/\\s*$`)
];
const lines = code.split('\n');
const matchLine = lines[match.line - 1] || '';
for (const pattern of commentPatterns) {
if (pattern.test(matchLine)) {
return true;
}
}
} catch (e) {
}
return false;
}
getSignatureById(id) {
return this.signatures.find(s => s.id === id);
}
getSignaturesByCategory(category) {
return this.signatures.filter(s => s.category === category);
}
getSignaturesBySeverity(severity) {
return this.signatures.filter(s => s.severity === severity);
}
getErrors() {
return this.errorAggregator.toJSON();
}
clearErrors() {
this.errorAggregator.clear();
}
reset() {
this.errorAggregator.clear();
}
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
module.exports = SignatureAnalyzer;