smart-ast-analyzer
Version:
Advanced AST-based project analysis tool with deep complexity analysis, security scanning, and optional AI enhancement
498 lines (440 loc) • 16.5 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
const { execSync } = require('child_process');
class DependencyScanner {
constructor(projectPath) {
this.projectPath = projectPath;
this.vulnerabilityDatabase = this.loadVulnerabilityDatabase();
this.packageManagers = ['npm', 'yarn', 'pnpm'];
}
async scan() {
const results = {
packageManager: await this.detectPackageManager(),
dependencies: await this.analyzeDependencies(),
vulnerabilities: [],
outdatedPackages: [],
securityMetrics: {},
recommendations: []
};
// Perform security scanning
results.vulnerabilities = await this.scanForVulnerabilities(results.dependencies);
results.outdatedPackages = await this.findOutdatedPackages(results.dependencies);
results.securityMetrics = this.calculateSecurityMetrics(results);
results.recommendations = this.generateRecommendations(results);
return results;
}
async detectPackageManager() {
const detectors = {
'package-lock.json': 'npm',
'yarn.lock': 'yarn',
'pnpm-lock.yaml': 'pnpm',
'composer.lock': 'composer',
'Pipfile.lock': 'pipenv',
'poetry.lock': 'poetry',
'Cargo.lock': 'cargo'
};
for (const [lockFile, manager] of Object.entries(detectors)) {
const lockPath = path.join(this.projectPath, lockFile);
try {
await fs.access(lockPath);
return {
name: manager,
lockFile: lockFile,
version: await this.getPackageManagerVersion(manager)
};
} catch (error) {
// Lock file doesn't exist
}
}
return { name: 'unknown', lockFile: null, version: null };
}
async getPackageManagerVersion(manager) {
try {
const versionCmd = {
npm: 'npm --version',
yarn: 'yarn --version',
pnpm: 'pnpm --version',
composer: 'composer --version',
pipenv: 'pipenv --version',
poetry: 'poetry --version',
cargo: 'cargo --version'
};
const command = versionCmd[manager];
if (command) {
const output = execSync(command, { encoding: 'utf8', timeout: 5000 });
return output.trim();
}
} catch (error) {
// Package manager not available
}
return null;
}
async analyzeDependencies() {
const dependencies = {
production: {},
development: {},
peer: {},
optional: {},
total: 0
};
try {
const packageJsonPath = path.join(this.projectPath, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
dependencies.production = packageJson.dependencies || {};
dependencies.development = packageJson.devDependencies || {};
dependencies.peer = packageJson.peerDependencies || {};
dependencies.optional = packageJson.optionalDependencies || {};
dependencies.total = Object.keys(dependencies.production).length +
Object.keys(dependencies.development).length +
Object.keys(dependencies.peer).length +
Object.keys(dependencies.optional).length;
// Get installed versions from node_modules or lock file
dependencies.installed = await this.getInstalledVersions();
} catch (error) {
console.warn('Failed to analyze dependencies:', error.message);
}
return dependencies;
}
async getInstalledVersions() {
const installed = {};
try {
// Try to read from package-lock.json first
const lockPath = path.join(this.projectPath, 'package-lock.json');
const lockFile = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
if (lockFile.dependencies) {
Object.entries(lockFile.dependencies).forEach(([name, info]) => {
installed[name] = {
version: info.version,
resolved: info.resolved,
integrity: info.integrity,
dependencies: info.dependencies
};
});
}
} catch (error) {
// Fallback to checking node_modules
try {
const nodeModulesPath = path.join(this.projectPath, 'node_modules');
const packages = await fs.readdir(nodeModulesPath);
for (const pkg of packages.slice(0, 50)) { // Limit to avoid performance issues
if (pkg.startsWith('.') || pkg.startsWith('@')) continue;
try {
const pkgJsonPath = path.join(nodeModulesPath, pkg, 'package.json');
const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf-8'));
installed[pkg] = {
version: pkgJson.version,
description: pkgJson.description,
homepage: pkgJson.homepage
};
} catch (e) {
// Skip packages without readable package.json
}
}
} catch (error) {
// No node_modules directory
}
}
return installed;
}
async scanForVulnerabilities(dependencies) {
const vulnerabilities = [];
// Check known vulnerable packages
const allPackages = {
...dependencies.production,
...dependencies.development
};
for (const [packageName, versionRange] of Object.entries(allPackages)) {
const packageVulns = await this.checkPackageVulnerabilities(packageName, versionRange);
vulnerabilities.push(...packageVulns);
}
// Try to use npm audit if available
try {
const auditResults = await this.runNpmAudit();
vulnerabilities.push(...auditResults);
} catch (error) {
console.warn('npm audit failed:', error.message);
}
return this.deduplicateVulnerabilities(vulnerabilities);
}
async checkPackageVulnerabilities(packageName, versionRange) {
const vulnerabilities = [];
const knownVulns = this.vulnerabilityDatabase[packageName];
if (knownVulns) {
knownVulns.forEach(vuln => {
if (this.isVersionAffected(versionRange, vuln.affectedVersions)) {
vulnerabilities.push({
package: packageName,
installedVersion: versionRange,
vulnerability: vuln.id,
severity: vuln.severity,
title: vuln.title,
description: vuln.description,
references: vuln.references,
patchedVersions: vuln.patchedVersions,
recommendation: vuln.recommendation,
cwe: vuln.cwe,
cvss: vuln.cvss
});
}
});
}
return vulnerabilities;
}
isVersionAffected(installedVersion, affectedVersions) {
// Simplified version checking - in a real implementation,
// you'd use a proper semver library
if (!affectedVersions || affectedVersions.length === 0) return false;
// Remove version range operators for basic comparison
const cleanVersion = installedVersion.replace(/[^0-9.]/g, '');
return affectedVersions.some(range => {
const cleanRange = range.replace(/[^0-9.]/g, '');
return cleanVersion === cleanRange;
});
}
async runNpmAudit() {
const vulnerabilities = [];
try {
const auditOutput = execSync('npm audit --json', {
cwd: this.projectPath,
encoding: 'utf8',
timeout: 30000,
stdio: 'pipe'
});
const auditData = JSON.parse(auditOutput);
if (auditData.vulnerabilities) {
Object.entries(auditData.vulnerabilities).forEach(([packageName, vuln]) => {
vulnerabilities.push({
package: packageName,
vulnerability: vuln.name,
severity: vuln.severity,
title: vuln.title,
description: vuln.info,
references: vuln.references || [],
via: vuln.via,
effects: vuln.effects,
range: vuln.range,
nodes: vuln.nodes,
fixAvailable: vuln.fixAvailable
});
});
}
} catch (error) {
// npm audit failed - this is common and not necessarily an error
if (error.status === 1) {
// npm audit returns exit code 1 when vulnerabilities are found
try {
const auditData = JSON.parse(error.stdout);
// Process the audit data even when exit code is 1
if (auditData.vulnerabilities) {
Object.entries(auditData.vulnerabilities).forEach(([packageName, vuln]) => {
vulnerabilities.push({
package: packageName,
vulnerability: vuln.name || 'Unknown',
severity: vuln.severity || 'unknown',
title: vuln.title || `Vulnerability in ${packageName}`,
description: vuln.info || 'No description available',
fixAvailable: vuln.fixAvailable
});
});
}
} catch (parseError) {
// Could not parse audit output
}
}
}
return vulnerabilities;
}
async findOutdatedPackages(dependencies) {
const outdated = [];
try {
const outdatedOutput = execSync('npm outdated --json', {
cwd: this.projectPath,
encoding: 'utf8',
timeout: 30000,
stdio: 'pipe'
});
const outdatedData = JSON.parse(outdatedOutput);
Object.entries(outdatedData).forEach(([packageName, info]) => {
outdated.push({
package: packageName,
current: info.current,
wanted: info.wanted,
latest: info.latest,
dependent: info.dependent,
type: info.type,
homepage: info.homepage
});
});
} catch (error) {
// npm outdated command failed or returned non-zero exit code
// This is normal when packages are outdated
}
return outdated;
}
deduplicateVulnerabilities(vulnerabilities) {
const seen = new Set();
return vulnerabilities.filter(vuln => {
const key = `${vuln.package}-${vuln.vulnerability}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
calculateSecurityMetrics(results) {
const metrics = {
totalPackages: results.dependencies.total,
vulnerablePackages: results.vulnerabilities.length,
criticalVulnerabilities: results.vulnerabilities.filter(v => v.severity === 'critical').length,
highVulnerabilities: results.vulnerabilities.filter(v => v.severity === 'high').length,
mediumVulnerabilities: results.vulnerabilities.filter(v => v.severity === 'moderate' || v.severity === 'medium').length,
lowVulnerabilities: results.vulnerabilities.filter(v => v.severity === 'low').length,
outdatedPackages: results.outdatedPackages.length,
securityScore: 0,
riskLevel: 'low'
};
// Calculate security score (0-100)
let score = 100;
score -= metrics.criticalVulnerabilities * 25;
score -= metrics.highVulnerabilities * 15;
score -= metrics.mediumVulnerabilities * 10;
score -= metrics.lowVulnerabilities * 5;
score -= metrics.outdatedPackages * 2;
metrics.securityScore = Math.max(0, score);
// Determine risk level
if (metrics.criticalVulnerabilities > 0 || metrics.securityScore < 50) {
metrics.riskLevel = 'critical';
} else if (metrics.highVulnerabilities > 0 || metrics.securityScore < 70) {
metrics.riskLevel = 'high';
} else if (metrics.mediumVulnerabilities > 0 || metrics.securityScore < 85) {
metrics.riskLevel = 'medium';
} else {
metrics.riskLevel = 'low';
}
return metrics;
}
generateRecommendations(results) {
const recommendations = [];
// Critical vulnerabilities
if (results.securityMetrics.criticalVulnerabilities > 0) {
recommendations.push({
priority: 'critical',
type: 'vulnerability',
title: 'Fix Critical Vulnerabilities Immediately',
description: `${results.securityMetrics.criticalVulnerabilities} critical vulnerabilities found that require immediate attention.`,
action: 'Update affected packages or find alternatives',
packages: results.vulnerabilities
.filter(v => v.severity === 'critical')
.map(v => v.package)
});
}
// High vulnerabilities
if (results.securityMetrics.highVulnerabilities > 0) {
recommendations.push({
priority: 'high',
type: 'vulnerability',
title: 'Address High Severity Vulnerabilities',
description: `${results.securityMetrics.highVulnerabilities} high severity vulnerabilities found.`,
action: 'Plan security updates in next sprint',
packages: results.vulnerabilities
.filter(v => v.severity === 'high')
.map(v => v.package)
});
}
// Outdated packages
if (results.outdatedPackages.length > 10) {
recommendations.push({
priority: 'medium',
type: 'maintenance',
title: 'Update Outdated Dependencies',
description: `${results.outdatedPackages.length} packages are outdated and may have security issues.`,
action: 'Regular dependency updates',
automation: 'Consider using Dependabot or Renovate'
});
}
// Package manager security
if (results.packageManager.name === 'unknown') {
recommendations.push({
priority: 'low',
type: 'configuration',
title: 'Use Package Manager Lock Files',
description: 'Lock files ensure reproducible builds and security.',
action: 'Commit package-lock.json or yarn.lock to version control'
});
}
// Security automation
recommendations.push({
priority: 'low',
type: 'process',
title: 'Implement Security Automation',
description: 'Automate dependency security scanning in CI/CD pipeline.',
action: 'Add npm audit or Snyk to CI/CD',
tools: ['npm audit', 'Snyk', 'OWASP Dependency Check', 'GitHub Security Advisories']
});
return recommendations;
}
loadVulnerabilityDatabase() {
// In a real implementation, this would load from a comprehensive database
// like the National Vulnerability Database (NVD) or GitHub Security Advisories
return {
'lodash': [
{
id: 'GHSA-p6mc-m468-83gw',
severity: 'high',
title: 'Prototype Pollution in lodash',
description: 'Lodash versions prior to 4.17.12 are vulnerable to Prototype Pollution.',
affectedVersions: ['<4.17.12'],
patchedVersions: ['>=4.17.12'],
references: ['https://github.com/advisories/GHSA-p6mc-m468-83gw'],
cwe: 'CWE-1321',
cvss: 7.0,
recommendation: 'Update to lodash version 4.17.12 or later'
}
],
'axios': [
{
id: 'GHSA-wf5p-g6vw-rhxx',
severity: 'high',
title: 'Axios CSRF vulnerability',
description: 'Axios before 0.21.1 is vulnerable to Server-Side Request Forgery.',
affectedVersions: ['<0.21.1'],
patchedVersions: ['>=0.21.1'],
references: ['https://github.com/advisories/GHSA-wf5p-g6vw-rhxx'],
cwe: 'CWE-918',
cvss: 8.1,
recommendation: 'Update to axios version 0.21.1 or later'
}
],
'minimist': [
{
id: 'GHSA-vh95-rmgr-6w4m',
severity: 'moderate',
title: 'Prototype Pollution in minimist',
description: 'minimist before 1.2.2 is vulnerable to prototype pollution.',
affectedVersions: ['<1.2.2'],
patchedVersions: ['>=1.2.2'],
references: ['https://github.com/advisories/GHSA-vh95-rmgr-6w4m'],
cwe: 'CWE-1321',
cvss: 5.6,
recommendation: 'Update to minimist version 1.2.2 or later'
}
],
'node-fetch': [
{
id: 'GHSA-w7rc-rwvf-8q5r',
severity: 'high',
title: 'node-fetch forwards secure headers to untrusted sites',
description: 'node-fetch before 2.6.7 and 3.x before 3.2.4 vulnerable to exposure of sensitive information.',
affectedVersions: ['<2.6.7', '>=3.0.0 <3.2.4'],
patchedVersions: ['>=2.6.7', '>=3.2.4'],
references: ['https://github.com/advisories/GHSA-w7rc-rwvf-8q5r'],
cwe: 'CWE-200',
cvss: 6.1,
recommendation: 'Update to node-fetch version 2.6.7/3.2.4 or later'
}
]
};
}
}
module.exports = DependencyScanner;