pury
Version:
🛡️ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis
369 lines • 16.1 kB
JavaScript
import { FindingType, Severity } from '../types/index.js';
import { logger } from '../utils/logger.js';
export class DependencyAnalyzer {
vulnerabilityDatabase = new Map();
suspiciousPackages = new Set();
constructor() {
this.loadKnownVulnerabilities();
this.loadSuspiciousPackages();
}
async analyze(files, onProgress) {
const startTime = Date.now();
const findings = [];
let filesAnalyzed = 0;
// Find package.json files
const packageFiles = files.filter(file => file.relativePath.endsWith('package.json') ||
file.relativePath.endsWith('composer.json') ||
file.relativePath.endsWith('requirements.txt') ||
file.relativePath.endsWith('Gemfile') ||
file.relativePath.endsWith('go.mod'));
// Also check for lock files and analyze their structure
const lockFiles = files.filter(file => file.relativePath.includes('package-lock.json') ||
file.relativePath.includes('yarn.lock') ||
file.relativePath.includes('composer.lock'));
const allDependencyFiles = [...packageFiles, ...lockFiles];
for (let i = 0; i < allDependencyFiles.length; i++) {
const file = allDependencyFiles[i];
onProgress?.(i + 1, allDependencyFiles.length, file.path);
try {
let fileFindings = [];
if (packageFiles.includes(file)) {
fileFindings = await this.analyzePackageFile(file);
}
else if (lockFiles.includes(file)) {
fileFindings = await this.analyzeLockFile(file);
}
findings.push(...fileFindings);
filesAnalyzed++;
}
catch (error) {
logger.warn(`Failed to analyze dependency file ${file.path}: ${error.message}`);
}
}
const processingTime = Date.now() - startTime;
logger.debug(`Dependency analysis completed in ${processingTime}ms`);
return {
findings,
processingTime,
filesAnalyzed
};
}
async analyzePackageFile(file) {
const findings = [];
try {
if (file.relativePath.endsWith('package.json')) {
const packageData = JSON.parse(file.content);
findings.push(...this.analyzeNodePackage(packageData, file));
}
else if (file.relativePath.endsWith('requirements.txt')) {
findings.push(...this.analyzePythonRequirements(file));
}
else if (file.relativePath.endsWith('composer.json')) {
findings.push(...this.analyzeComposerPackage(file));
}
}
catch (error) {
findings.push({
id: `dep-parse-${Date.now()}`,
type: FindingType.VULNERABILITY,
severity: Severity.LOW,
title: 'Package File Parse Error',
description: `Failed to parse package file: ${error.message}`,
file: file.path,
suggestion: 'Ensure the package file has valid JSON/format syntax'
});
}
return findings;
}
analyzeNodePackage(packageData, file) {
const findings = [];
// Check all dependencies
const allDeps = {
...packageData.dependencies,
...packageData.devDependencies
};
for (const [packageName, version] of Object.entries(allDeps || {})) {
// Check for suspicious packages
if (this.suspiciousPackages.has(packageName)) {
findings.push({
id: `dep-suspicious-${Date.now()}-${packageName}`,
type: FindingType.MALWARE,
severity: Severity.HIGH,
title: 'Suspicious Package Detected',
description: `Package "${packageName}" is known to be suspicious or malicious`,
file: file.path,
evidence: `"${packageName}": "${version}"`,
suggestion: 'Remove this package and find a trusted alternative',
references: ['https://github.com/advisories']
});
}
// Check for typosquatting
const typoSquattingResult = this.checkTypoSquatting(packageName);
if (typoSquattingResult) {
findings.push({
id: `dep-typo-${Date.now()}-${packageName}`,
type: FindingType.MALWARE,
severity: Severity.MEDIUM,
title: 'Potential Typosquatting',
description: `Package "${packageName}" may be a typosquatted version of "${typoSquattingResult.legitimate}"`,
file: file.path,
evidence: `"${packageName}": "${version}"`,
suggestion: `Consider using the legitimate package "${typoSquattingResult.legitimate}" instead`
});
}
// Check for known vulnerabilities
const vulnerabilities = this.vulnerabilityDatabase.get(packageName);
if (vulnerabilities) {
for (const vuln of vulnerabilities) {
if (this.isVersionAffected(version, vuln.affectedVersions)) {
findings.push({
id: `dep-vuln-${vuln.id}`,
type: FindingType.VULNERABILITY,
severity: this.mapSeverity(vuln.severity),
title: `Known Vulnerability: ${vuln.title}`,
description: vuln.description,
file: file.path,
evidence: `"${packageName}": "${version}"`,
suggestion: vuln.patchedVersions
? `Update to version ${vuln.patchedVersions} or later`
: 'Update to the latest version or find an alternative',
references: vuln.reference ? [vuln.reference] : []
});
}
}
}
// Check for outdated packages (simplified)
if (this.isLikelyOutdated(version)) {
findings.push({
id: `dep-outdated-${Date.now()}-${packageName}`,
type: FindingType.VULNERABILITY,
severity: Severity.LOW,
title: 'Potentially Outdated Package',
description: `Package "${packageName}" appears to use an old version format`,
file: file.path,
evidence: `"${packageName}": "${version}"`,
suggestion: 'Check for updates and security patches for this package'
});
}
}
// Check for missing security configurations
if (!packageData.dependencies?.helmet && !packageData.devDependencies?.helmet) {
// This is just an example - in practice, you'd have more sophisticated checks
if (Object.keys(packageData.dependencies || {}).some(dep => ['express', 'koa', 'fastify'].includes(dep))) {
findings.push({
id: `dep-security-${Date.now()}`,
type: FindingType.VULNERABILITY,
severity: Severity.LOW,
title: 'Missing Security Middleware',
description: 'Web application detected without security middleware like Helmet',
file: file.path,
suggestion: 'Consider adding security middleware to protect against common vulnerabilities'
});
}
}
return findings;
}
analyzePythonRequirements(file) {
const findings = [];
const lines = file.content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i]?.trim();
if (!line || line.startsWith('#'))
continue;
const match = /^([a-zA-Z0-9_-]+)([>=<!=]+)(.+)$/.exec(line);
if (match) {
const [, packageName] = match;
if (packageName && this.suspiciousPackages.has(packageName)) {
findings.push({
id: `dep-suspicious-py-${Date.now()}-${packageName}`,
type: FindingType.MALWARE,
severity: Severity.HIGH,
title: 'Suspicious Python Package',
description: `Package "${packageName}" is known to be suspicious`,
file: file.path,
line: i + 1,
evidence: line,
suggestion: 'Remove this package and find a trusted alternative'
});
}
}
}
return findings;
}
analyzeComposerPackage(_file) {
const findings = [];
// Similar analysis for PHP Composer packages
// Implementation would follow similar patterns to Node.js analysis
return findings;
}
async analyzeLockFile(file) {
const findings = [];
try {
if (file.relativePath.includes('package-lock.json')) {
const lockData = JSON.parse(file.content);
// Analyze lock file for integrity issues
findings.push(...this.analyzePackageLockIntegrity(lockData, file));
}
}
catch (error) {
findings.push({
id: `lock-parse-${Date.now()}`,
type: FindingType.VULNERABILITY,
severity: Severity.LOW,
title: 'Lock File Parse Error',
description: `Failed to parse lock file: ${error.message}`,
file: file.path,
suggestion: 'Regenerate the lock file or check for corruption'
});
}
return findings;
}
analyzePackageLockIntegrity(lockData, file) {
const findings = [];
// Check for missing integrity hashes (simplified check)
if (lockData.packages) {
let packagesWithoutIntegrity = 0;
let totalPackages = 0;
for (const [packagePath, packageInfo] of Object.entries(lockData.packages)) {
if (packagePath === '')
continue; // Skip root package
totalPackages++;
if (!packageInfo || !packageInfo?.integrity) {
packagesWithoutIntegrity++;
}
}
if (packagesWithoutIntegrity > 0) {
findings.push({
id: `lock-integrity-${Date.now()}`,
type: FindingType.VULNERABILITY,
severity: Severity.MEDIUM,
title: 'Missing Package Integrity Hashes',
description: `${packagesWithoutIntegrity} out of ${totalPackages} packages are missing integrity hashes`,
file: file.path,
suggestion: 'Regenerate package-lock.json with npm install to ensure integrity hashes'
});
}
}
return findings;
}
checkTypoSquatting(packageName) {
// Simple typosquatting detection - in practice, this would use a comprehensive database
const commonPackages = [
'express',
'react',
'lodash',
'axios',
'moment',
'redux',
'webpack',
'babel',
'eslint',
'prettier',
'typescript',
'jest',
'mocha',
'chai'
];
for (const legitimate of commonPackages) {
if (this.isLikelyTypo(packageName, legitimate)) {
return { legitimate };
}
}
return null;
}
isLikelyTypo(test, target) {
if (test === target)
return false;
// Simple Levenshtein distance check
const distance = this.levenshteinDistance(test, target);
return distance === 1 && test.length >= 3; // Allow 1 character difference for packages >= 3 chars
}
levenshteinDistance(str1, str2) {
const matrix = Array.from({ length: str2.length + 1 }, (_, i) => [i]);
matrix[0] = Array.from({ length: str1.length + 1 }, (_, i) => i);
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
const cost = str1[j - 1] === str2[i - 1] ? 0 : 1;
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
return matrix[str2.length][str1.length];
}
isVersionAffected(installedVersion, affectedVersions) {
// Simplified version comparison - in practice, use semver library
// This would need proper semver range checking
return affectedVersions.includes(installedVersion.replace(/[^0-9.]/g, ''));
}
isLikelyOutdated(version) {
// Simple check for obviously old versions
const oldVersionPatterns = [
/^0\./, // Version starting with 0.x
/^1\.[0-5]\./, // Very old 1.x versions
/\d{4}$/ // Versions ending with year (like 2018)
];
return oldVersionPatterns.some(pattern => pattern.test(version));
}
mapSeverity(severity) {
switch (severity.toLowerCase()) {
case 'critical':
return Severity.CRITICAL;
case 'high':
return Severity.HIGH;
case 'moderate':
return Severity.MEDIUM;
case 'low':
return Severity.LOW;
default:
return Severity.MEDIUM;
}
}
loadKnownVulnerabilities() {
// In a real implementation, this would load from a vulnerability database
// For now, we'll add some example vulnerabilities
this.vulnerabilityDatabase.set('lodash', [
{
id: 'CVE-2020-8203',
severity: 'high',
title: 'Prototype Pollution',
description: 'Lodash versions prior to 4.17.19 are vulnerable to prototype pollution',
affectedVersions: '<4.17.19',
patchedVersions: '>=4.17.19',
reference: 'https://nvd.nist.gov/vuln/detail/CVE-2020-8203'
}
]);
this.vulnerabilityDatabase.set('axios', [
{
id: 'CVE-2020-28168',
severity: 'moderate',
title: 'Server-Side Request Forgery',
description: 'Axios versions prior to 0.21.1 are vulnerable to SSRF',
affectedVersions: '<0.21.1',
patchedVersions: '>=0.21.1',
reference: 'https://nvd.nist.gov/vuln/detail/CVE-2020-28168'
}
]);
}
loadSuspiciousPackages() {
// In practice, this would load from a curated list of known malicious packages
const suspiciousList = [
'event-stream', // Historical malicious package
'getcookies', // Typosquatting example
'http-fetch', // Suspicious package example
'node-env' // Another example
];
this.suspiciousPackages = new Set(suspiciousList);
}
addSuspiciousPackage(packageName) {
this.suspiciousPackages.add(packageName);
}
addVulnerability(packageName, vulnerability) {
if (!this.vulnerabilityDatabase.has(packageName)) {
this.vulnerabilityDatabase.set(packageName, []);
}
this.vulnerabilityDatabase.get(packageName).push(vulnerability);
}
}
//# sourceMappingURL=dependency-analyzer.js.map