agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
450 lines (398 loc) • 18.1 kB
JavaScript
/**
* @file Vulnerability scanner
* @description Main scanner for detecting security vulnerabilities in content
*/
// Remove unused import since we're using patternMatcher directly
const { analyzeWithAST } = require('./astAnalyzer');
const { detectModernVulnerabilities } = require('./modernPatternDetector');
const { detectFrameworkSecurity } = require('./frameworkDetector');
const { generateContextualMessage } = require('./patternMatcher');
/**
* Check if a potential vulnerability is actually a test case, pattern definition, or documentation example
* @param {string} line - The line of code containing the potential vulnerability
* @param {string} content - Full file content for context
* @param {string} filePath - File path for additional context
* @returns {boolean} True if this appears to be a test case, pattern, or example - not real vulnerability
*/
function isPatternDefinition(line, content, filePath = '') {
// Enhanced test, demo, and debug file detection
const isTestFile = filePath.includes('.test.js') ||
filePath.includes('.spec.js') ||
filePath.includes('/test/') ||
filePath.includes('/tests/') ||
filePath.includes('temp-') ||
filePath.includes('test-') ||
filePath.includes('debug') ||
filePath.includes('demo') ||
filePath.includes('/demo/') ||
filePath.includes('sample') ||
filePath.includes('example') ||
content.includes('qtests') ||
content.includes('mocha') ||
content.includes('jest') ||
content.includes('@file Unit tests') ||
content.includes('createAssertions') ||
content.includes('debugging') ||
content.includes('demonstration');
// Test case indicators - stronger detection for test scenarios
const testIndicators = [
// Common test patterns
/\/\/.*test|\/\/.*example|\/\/.*vulnerability.*test/i,
/\/\/.*Security.*test|\/\/.*XSS.*vulnerability|\/\/.*SQL.*injection.*vulnerability/i,
/\/\/.*Another.*vulnerability|\/\/.*injection.*pattern/i,
// Test content creation
/testContent|testCode|xssCode|sqlCode|tempFile.*=|temp.*test/i,
/writeFileSync.*temp|createTemporary|Security.*test.*content/i,
// Test function contexts
/function.*test|it\(|describe\(|test\(/,
// Test assertions and descriptions
/assert\.|expect\(|should.*detect|vulnerability.*detection/i,
];
// Common pattern definition indicators (existing)
const patternIndicators = [
// Pattern object definitions
/pattern:\s*\//, // pattern: /regex/
/regex:\s*\//, // regex: /pattern/
// Variable names that suggest this is a pattern definition
/vulnerableRegexPatterns|securityPatterns|PATTERNS|pattern.*=.*\/|regex.*=.*\//,
// Comments indicating this is a detection pattern
/\/\/.*pattern|\/\/.*detection|\/\/.*regex.*vulnerable/i,
// Array of patterns
/\[\s*\/.*\/.*,/,
];
// Enhanced context indicators
const contextIndicators = [
// File contains pattern definitions
content.includes('pattern definitions') || content.includes('Pattern Constants'),
content.includes('vulnerableRegexPatterns') || content.includes('PATTERNS ='),
content.includes('detectReDoS') || content.includes('detectVulnerabilities'),
// Line is in an array or object literal for patterns
line.includes('pattern:') || line.includes('severity:'),
line.trim().startsWith('//') && line.includes('pattern'),
// Documentation examples
line.includes('// Instead of:') || line.includes('// Use:'),
// Test-specific context
content.includes('Unit tests') || content.includes('Test suite'),
content.includes('temporary test file') || content.includes('Create temporary'),
];
// Check surrounding lines for test context (look 2 lines above and below)
const lines = content.split('\n');
const currentLineIndex = lines.findIndex(l => l.includes(line.trim()));
if (currentLineIndex !== -1) {
const surroundingLines = lines.slice(Math.max(0, currentLineIndex - 2), currentLineIndex + 3);
const surroundingContext = surroundingLines.join('\n');
const surroundingTestPatterns = [
/\/\/.*test|\/\/.*example|\/\/.*vulnerability/i,
/writeFileSync|temp.*File|test.*Content/i,
/Security.*vulnerability|XSS.*vulnerability|SQL.*injection/i,
];
if (surroundingTestPatterns.some(pattern => pattern.test(surroundingContext))) {
return true;
}
}
// If it's a test file, be more lenient about flagging as test content
// Check for test-specific context patterns
if (isTestFile) {
// Strong test indicators that suggest this is intentional test code
const strongTestIndicators = [
/\/\/.*XSS.*vulnerability|\/\/.*Another.*XSS|\/\/.*SQL.*injection/i,
/\/\/.*vulnerability.*-.*direct|\/\/.*dangerous|\/\/.*pattern/i,
/tempFile|xssCode|sqlCode|testCode|renderUserContent/i,
/writeFileSync.*temp|createTemp/i,
];
// Check surrounding context for test patterns
const hasStrongTestContext = strongTestIndicators.some(indicator =>
indicator.test(line) || indicator.test(content)
);
if (hasStrongTestContext) {
return true; // This is definitely test content
}
// Fallback to existing logic for weaker indicators
return (testIndicators.some(indicator => indicator.test(line)) ||
patternIndicators.some(indicator => indicator.test(line))) &&
(line.includes('test') || line.includes('spec') || line.includes('mock') ||
line.includes('expect') || line.includes('assert') || line.includes('describe'));
}
// For non-test files, use stricter criteria
return patternIndicators.some(indicator => indicator.test(line)) ||
contextIndicators.some(indicator => indicator);
}
/**
* Scans content for security vulnerabilities
* @param {string} content - File content
* @param {string} language - Programming language
* @param {string} filePath - File path for enhanced output
* @returns {Object} Object containing vulnerabilities and framework findings
*/
function scanForVulnerabilities(content, language, filePath = '') {
const vulnerabilities = [];
const { matchPatterns } = require('./patternMatcher');
// Traditional pattern-based scanning - call once to avoid duplication
const patternMatches = matchPatterns(content);
// Filter out pattern definitions, test cases, and documentation examples
const lines = content.split('\n');
const filteredMatches = patternMatches.filter(m => {
const lineNumber = m.line || 1;
const line = lines[lineNumber - 1] || '';
return !isPatternDefinition(line, content, filePath);
});
// Deduplicate matches from overlapping patterns
const deduplicatedMatches = deduplicateVulnerabilities(filteredMatches);
// Check if this is a test/demo/debug file for confidence adjustment
const isTestOrDemoFile = filePath.includes('.test.js') ||
filePath.includes('.spec.js') ||
filePath.includes('/test/') ||
filePath.includes('/tests/') ||
filePath.includes('temp-') ||
filePath.includes('test-') ||
filePath.includes('debug') ||
filePath.includes('demo') ||
filePath.includes('/demo/') ||
filePath.includes('sample') ||
filePath.includes('example');
vulnerabilities.push(...deduplicatedMatches.map(m => {
let adjustedConfidence = m.confidence || 'medium';
let contextTags = [];
let severityAdjustment = '';
// Reduce confidence for vulnerabilities in test/demo files that slipped through
if (isTestOrDemoFile && adjustedConfidence === 'high') {
adjustedConfidence = 'medium';
contextTags.push('test_or_demo_file');
} else if (isTestOrDemoFile && adjustedConfidence === 'medium') {
adjustedConfidence = 'low';
contextTags.push('test_or_demo_file');
}
// Enhanced context detection for dogfood mode
const lineNumber = m.line || 1;
const line = lines[lineNumber - 1] || '';
// Check if this is intentional test code
if (isTestOrDemoFile) {
const testPatterns = [
/\/\/.*test|\/\/.*example|\/\/.*vulnerability/i,
/writeFileSync|temp.*File|test.*Content|xssCode|sqlCode/i,
/Security.*vulnerability|XSS.*vulnerability|SQL.*injection/i,
/buggyCode.*=|testFile.*=/i
];
if (testPatterns.some(pattern => pattern.test(line)) ||
testPatterns.some(pattern => pattern.test(content.substring(Math.max(0, content.indexOf(line) - 200), content.indexOf(line) + 200)))) {
contextTags.push('intentional_test_code');
adjustedConfidence = 'low';
severityAdjustment = '(likely false positive - test code)';
}
}
// Special handling for specific vulnerability types
if (m.type === 'weak_crypto' || m.type === 'weak_crypto_performance' || m.message?.includes('MD5') || line.includes('md5')) {
// MD5 for code deduplication is a code smell, not a security vulnerability
const md5Context = content.substring(Math.max(0, content.indexOf(line) - 100), content.indexOf(line) + 100);
if (md5Context.includes('duplicate') || md5Context.includes('hash') || md5Context.includes('similarity') ||
md5Context.includes('grouping') || md5Context.includes('deduplication')) {
contextTags.push('code_deduplication_usage');
severityAdjustment = '(code smell - MD5 for non-cryptographic use)';
adjustedConfidence = 'low';
}
}
return {
...m,
name: m.type || 'Unknown Vulnerability',
confidence: adjustedConfidence,
contextTags,
severityNote: severityAdjustment,
description: m.message || `${m.type.replace(/_/g, ' ')} vulnerability detected`,
file: filePath,
category: getCategoryForType(m.type),
remediation: getRemediationForType(m.type),
cweReference: getCWEReference(m.type),
code: m.match
};
}));
// AST-based analysis for JavaScript/TypeScript
if (language === 'javascript' || language === 'typescript') {
try {
const astVulns = analyzeWithAST(content);
vulnerabilities.push(...astVulns.map(v => ({
name: v.type,
type: v.type,
severity: v.confidence === 'high' ? 'HIGH' : 'MEDIUM',
category: getCategoryForType(v.type) || 'Code Analysis',
description: v.message,
remediation: getRemediationForType(v.type),
cweReference: getCWEReference(v.type),
code: v.code || 'AST-based detection',
line: v.line,
confidence: v.confidence,
file: filePath
})));
} catch (error) {
const quiet = process.env.QUIET_LOGS === '1' || process.env.ANALYSIS_OUTPUT === 'json';
if (!quiet) {
console.error('AST analysis error:', error.message);
}
}
}
// Modern vulnerability detection
try {
const modernVulns = detectModernVulnerabilities(content);
// Filter out pattern definitions from modern vulnerability detection
const filteredModernVulns = modernVulns.filter(v => {
const lineNumber = v.line || 1;
const line = lines[lineNumber - 1] || '';
return !isPatternDefinition(line, content);
});
vulnerabilities.push(...filteredModernVulns.map(v => ({
...v,
name: v.name || v.type || 'Modern Vulnerability',
description: v.description || v.message || `${(v.type || 'security').replace(/_/g, ' ')} vulnerability detected`,
file: filePath,
category: v.category || getCategoryForType(v.type),
remediation: v.remediation || getRemediationForType(v.type),
code: v.code || v.match
})));
} catch (error) {
const quiet = process.env.QUIET_LOGS === '1' || process.env.ANALYSIS_OUTPUT === 'json';
if (!quiet) {
console.error('Modern pattern detection error:', error.message);
}
}
// Framework security analysis
let frameworkFindings = {};
try {
frameworkFindings = detectFrameworkSecurity(content);
} catch (error) {
const quiet = process.env.QUIET_LOGS === '1' || process.env.ANALYSIS_OUTPUT === 'json';
if (!quiet) {
console.error('Framework detection error:', error.message);
}
}
// Deduplicate overlapping vulnerabilities
const deduplicatedVulns = deduplicateVulnerabilities(vulnerabilities);
return {
vulnerabilities: deduplicatedVulns,
frameworkFindings
};
}
/**
* Get CWE (Common Weakness Enumeration) reference for vulnerability type
* @param {string} type - Vulnerability type
* @returns {string} CWE reference
*/
function getCWEReference(type) {
const cweMap = {
'sql_injection': 'CWE-89',
'xss': 'CWE-79',
'code_injection': 'CWE-94',
'path_traversal': 'CWE-22',
'insecure_random': 'CWE-338',
'hardcoded_secret': 'CWE-798',
'ssrf': 'CWE-918',
'nosql_injection': 'CWE-943',
'json_vulnerabilities': 'CWE-502',
'weak_crypto': 'CWE-327',
'weak_crypto_security': 'CWE-327',
'timing_attacks': 'CWE-208',
'eval_usage': 'CWE-94',
'unsafe_parsing': 'CWE-20',
'unsafe_file_operations': 'CWE-22',
'weak_crypto_other': 'CWE-326',
'weak_encoding': 'CWE-327'
};
return cweMap[type] || 'CWE-Other';
}
/**
* Get category for vulnerability type
* @param {string} type - Vulnerability type
* @returns {string} Category name
*/
function getCategoryForType(type) {
const categoryMap = {
'sql_injection': 'Injection',
'xss': 'Cross-Site Scripting',
'code_injection': 'Code Injection',
'path_traversal': 'Path Traversal',
'insecure_random': 'Cryptography',
'hardcoded_secret': 'Secrets Management',
'ssrf': 'Server-Side Request Forgery',
'nosql_injection': 'NoSQL Injection',
'json_vulnerabilities': 'JSON Security',
'weak_crypto': 'Weak Cryptography',
'weak_crypto_security': 'Weak Cryptography',
'timing_attacks': 'Timing Attacks'
};
return categoryMap[type] || 'Security';
}
/**
* Get remediation advice for vulnerability type
* @param {string} type - Vulnerability type
* @returns {string} Remediation advice
*/
function getRemediationForType(type) {
const remediationMap = {
'sql_injection': 'Use parameterized queries or prepared statements instead of string concatenation',
'xss': 'Sanitize user input and use safe DOM manipulation methods',
'code_injection': 'Avoid eval() and dynamic code execution, validate all inputs',
'path_traversal': 'Validate file paths and use path.resolve() to prevent directory traversal',
'weak_crypto_security': 'Replace MD5/SHA1 with SHA-256, SHA-3, or bcrypt for security-critical operations',
'weak_crypto': 'Evaluate if cryptographic security is required; use stronger hash functions for security contexts',
'timing_attacks': 'Use crypto.timingSafeEqual() for password/token comparisons to prevent timing attacks',
'insecure_random': 'Use cryptographically secure random number generators like crypto.randomBytes()',
'hardcoded_secret': 'Move secrets to environment variables or secure secret management systems',
'ssrf': 'Validate and restrict URLs, use allow-lists for external requests',
'nosql_injection': 'Use parameterized queries and avoid string concatenation in NoSQL queries',
'json_vulnerabilities': 'Use safe JSON parsing with proper error handling and avoid __proto__ manipulation',
'weak_crypto': 'Use strong cryptographic algorithms like SHA-256 or better instead of MD5/SHA1',
'timing_attacks': 'Use constant-time comparison functions for password and secret validation'
};
return remediationMap[type] || 'Review and fix the identified security issue';
}
/**
* Deduplicate overlapping vulnerabilities based on location and match
* @param {Array} vulnerabilities - Array of vulnerability objects
* @returns {Array} Deduplicated vulnerability array
*/
function deduplicateVulnerabilities(vulnerabilities) {
const seen = new Map();
const deduplicated = [];
for (const vuln of vulnerabilities) {
// Enhanced deduplication key - consider line number and vulnerability type
// For overlapping patterns (like crypto.createHash vs .createHash), group by line+type
const key = `${vuln.line}-${vuln.type}`;
const existing = seen.get(key);
if (!existing) {
// First time seeing this location/type combination
seen.set(key, vuln);
deduplicated.push(vuln);
} else {
// Merge with existing if this one has higher severity or longer match
const newSeverity = getSeverityScore(vuln.severity || 'MEDIUM');
const existingSeverity = getSeverityScore(existing.severity || 'MEDIUM');
const newMatchLength = (vuln.match || '').length;
const existingMatchLength = (existing.match || '').length;
if (newSeverity > existingSeverity ||
(newSeverity === existingSeverity && newMatchLength > existingMatchLength)) {
// Replace with higher severity or more complete finding
const index = deduplicated.findIndex(v => v === existing);
if (index >= 0) {
deduplicated[index] = vuln;
seen.set(key, vuln);
}
}
}
}
return deduplicated;
}
/**
* Convert severity to numeric score for comparison
* @param {string} severity - Severity level
* @returns {number} Numeric severity score
*/
function getSeverityScore(severity) {
const scores = {
'LOW': 1,
'MEDIUM': 2,
'HIGH': 3,
'CRITICAL': 4
};
return scores[severity] || 0;
}
module.exports = {
scanForVulnerabilities
};