mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
364 lines • 16.8 kB
JavaScript
/**
* Dependency Scanner
* Checks for vulnerable dependencies and runs npm audit
*/
import fs from 'fs-extra';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export class DependencyScanner {
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot;
}
async scan(vulnerabilities) {
await this.checkDependencyVulnerabilities(vulnerabilities);
await this.checkPackageJsonSecurity(vulnerabilities);
}
async runNpmAudit(vulnerabilities) {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
// Only run if package.json exists
if (!(await fs.pathExists(packageJsonPath))) {
return undefined;
}
try {
// Run npm audit with JSON output
const { stdout } = await execAsync('npm audit --json', {
cwd: this.projectRoot,
timeout: 30000 // 30 second timeout
});
const auditResult = JSON.parse(stdout);
// Process vulnerabilities
const npmVulnerabilities = [];
if (auditResult.vulnerabilities) {
for (const [packageName, vuln] of Object.entries(auditResult.vulnerabilities)) {
const vulnerability = vuln;
npmVulnerabilities.push({
name: packageName,
severity: vulnerability.severity || 'unknown',
isDirect: vulnerability.isDirect || false,
via: vulnerability.via || [],
effects: vulnerability.effects || [],
range: vulnerability.range || '',
nodes: vulnerability.nodes || [],
fixAvailable: vulnerability.fixAvailable || false
});
// Add to main vulnerabilities list
vulnerabilities.push({
file: 'package.json',
severity: this.mapNpmSeverity(vulnerability.severity),
type: 'npm_vulnerability',
message: `${packageName}: ${vulnerability.title || 'Security vulnerability detected'}`,
packageName: packageName,
packageVersion: vulnerability.range,
cve: vulnerability.cves?.join(', '),
recommendation: vulnerability.fixAvailable ? 'Update available' : 'No automated fix available'
});
}
}
const summary = auditResult.metadata?.vulnerabilities || {};
return {
totalVulnerabilities: summary.total || 0,
critical: summary.critical || 0,
high: summary.high || 0,
moderate: summary.moderate || 0,
low: summary.low || 0,
info: summary.info || 0,
auditReportMajorVersion: auditResult.auditReportVersion || 2,
vulnerabilities: npmVulnerabilities
};
}
catch (error) {
// NPM audit failed (could be no vulnerabilities or npm not available)
// This is not necessarily an error condition
return undefined;
}
}
async checkDependencyVulnerabilities(vulnerabilities) {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
try {
const packageJson = await fs.readJson(packageJsonPath);
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
// Check for known vulnerable packages and patterns
await this.checkKnownVulnerablePackages(allDeps, vulnerabilities);
await this.checkOutdatedPackages(allDeps, vulnerabilities);
await this.checkSuspiciousPackages(allDeps, vulnerabilities);
}
catch (error) {
// Ignore JSON parsing errors
}
}
}
async checkPackageJsonSecurity(vulnerabilities) {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
try {
const packageJson = await fs.readJson(packageJsonPath);
// Check for dangerous scripts
if (packageJson.scripts) {
for (const [scriptName, scriptValue] of Object.entries(packageJson.scripts)) {
if (typeof scriptValue === 'string') {
this.checkDangerousScript(scriptName, scriptValue, vulnerabilities);
}
}
}
// Check for missing security-related scripts
this.checkSecurityScripts(packageJson.scripts || {}, vulnerabilities);
// Check for postinstall scripts (can be security risk)
if (packageJson.scripts?.postinstall) {
vulnerabilities.push({
file: 'package.json',
severity: 'medium',
type: 'postinstall_script',
message: 'postinstall script detected - ensure it\'s from a trusted source',
recommendation: 'Review postinstall script for malicious behavior'
});
}
}
catch (error) {
// Ignore JSON parsing errors
}
}
}
async checkKnownVulnerablePackages(deps, vulnerabilities) {
// Known vulnerable packages with specific versions
const knownVulnerable = [
{ package: 'event-stream', version: '3.3.6', cve: 'CVE-2018-16487' },
{ package: 'lodash', maxVersion: '4.17.20', cve: 'CVE-2021-23337' },
{ package: 'axios', maxVersion: '0.21.0', cve: 'CVE-2020-28168' },
{ package: 'minimist', maxVersion: '1.2.5', cve: 'CVE-2021-44906' },
{ package: 'node-fetch', maxVersion: '2.6.6', cve: 'CVE-2022-0235' },
{ package: 'tar', maxVersion: '6.1.11', cve: 'CVE-2021-37701' },
{ package: 'glob-parent', maxVersion: '5.1.1', cve: 'CVE-2020-28469' },
{ package: 'express-fileupload', maxVersion: '1.1.8', cve: 'CVE-2020-7699' }
];
for (const [depName, depVersion] of Object.entries(deps)) {
// Check against known vulnerable packages
for (const vuln of knownVulnerable) {
if (depName === vuln.package) {
const version = this.extractVersion(depVersion);
if (vuln.version && version === vuln.version) {
vulnerabilities.push({
file: 'package.json',
severity: 'critical',
type: 'known_vulnerability',
message: `${depName}@${version} has known vulnerability`,
cve: vuln.cve,
packageName: depName,
packageVersion: version,
recommendation: `Update ${depName} to latest version`
});
}
else if (vuln.maxVersion && this.isVersionLessThan(version, vuln.maxVersion)) {
vulnerabilities.push({
file: 'package.json',
severity: 'high',
type: 'known_vulnerability',
message: `${depName}@${version} may have vulnerability (below ${vuln.maxVersion})`,
cve: vuln.cve,
packageName: depName,
packageVersion: version,
recommendation: `Update ${depName} to version > ${vuln.maxVersion}`
});
}
}
}
}
}
async checkOutdatedPackages(deps, vulnerabilities) {
for (const [dep, version] of Object.entries(deps)) {
if (typeof version === 'string') {
// Check for very old versions
if (version.startsWith('^0.') || version.startsWith('~0.')) {
vulnerabilities.push({
file: 'package.json',
severity: 'medium',
type: 'outdated_dependency',
message: `Dependency ${dep}@${version} appears to be very old (pre-1.0)`,
packageName: dep,
packageVersion: version,
recommendation: 'Consider updating to a stable version'
});
}
// Check for git dependencies (security risk)
if (version.includes('git://') || version.includes('git+')) {
vulnerabilities.push({
file: 'package.json',
severity: 'high',
type: 'git_dependency',
message: `Dependency ${dep} uses git URL which may be insecure`,
packageName: dep,
packageVersion: version,
recommendation: 'Use npm registry versions instead of git URLs'
});
}
// Check for file dependencies (could be malicious)
if (version.startsWith('file:')) {
vulnerabilities.push({
file: 'package.json',
severity: 'medium',
type: 'local_dependency',
message: `Dependency ${dep} uses local file path`,
packageName: dep,
packageVersion: version,
recommendation: 'Ensure local dependency is from trusted source'
});
}
}
}
}
async checkSuspiciousPackages(deps, vulnerabilities) {
// Patterns that might indicate typosquatting or malicious packages
const suspiciousPatterns = [
{ pattern: /^node_modules$/, message: 'Package name "node_modules" is suspicious' },
{ pattern: /^\.\./, message: 'Package name starting with ".." is suspicious' },
{ pattern: /\s/, message: 'Package name contains whitespace' },
{ pattern: /[<>|&;`]/, message: 'Package name contains shell metacharacters' }
];
// Common typosquatting targets
const commonTypos = {
'reacts': 'react',
'rect': 'react',
'expres': 'express',
'expresss': 'express',
'loadash': 'lodash',
'lodsh': 'lodash',
'momnet': 'moment',
'axois': 'axios',
'axious': 'axios'
};
for (const depName of Object.keys(deps)) {
// Check suspicious patterns
for (const { pattern, message } of suspiciousPatterns) {
if (pattern.test(depName)) {
vulnerabilities.push({
file: 'package.json',
severity: 'high',
type: 'suspicious_package',
message: `${message}: ${depName}`,
packageName: depName,
recommendation: 'Verify this package is legitimate'
});
}
}
// Check for typosquatting
const lowerDepName = depName.toLowerCase();
if (commonTypos[lowerDepName]) {
vulnerabilities.push({
file: 'package.json',
severity: 'critical',
type: 'typosquatting',
message: `Possible typosquatting: "${depName}" (did you mean "${commonTypos[lowerDepName]}"?)`,
packageName: depName,
recommendation: `Replace with legitimate package: ${commonTypos[lowerDepName]}`
});
}
}
}
checkDangerousScript(scriptName, scriptValue, vulnerabilities) {
// Check for dangerous commands
const dangerousCommands = [
{ pattern: /rm\s+-rf\s+\//, severity: 'critical', message: 'Dangerous rm -rf / command' },
{ pattern: /rm\s+-rf\s+~/, severity: 'high', message: 'Dangerous rm -rf ~ command' },
{ pattern: /curl.*\|\s*sh/, severity: 'critical', message: 'Piping curl to shell is dangerous' },
{ pattern: /wget.*\|\s*sh/, severity: 'critical', message: 'Piping wget to shell is dangerous' },
{ pattern: /eval\s*\(/, severity: 'high', message: 'eval() usage in script' },
{ pattern: /sudo\s+/, severity: 'medium', message: 'Script requires sudo privileges' }
];
for (const { pattern, severity, message } of dangerousCommands) {
if (pattern.test(scriptValue)) {
vulnerabilities.push({
file: 'package.json',
severity,
type: 'dangerous_script',
message: `Script "${scriptName}": ${message}`,
recommendation: 'Review and modify script to remove dangerous commands'
});
}
}
// Check for obfuscated scripts
if (this.isObfuscated(scriptValue)) {
vulnerabilities.push({
file: 'package.json',
severity: 'high',
type: 'obfuscated_script',
message: `Script "${scriptName}" appears to be obfuscated`,
recommendation: 'Review script for malicious behavior'
});
}
}
checkSecurityScripts(scripts, vulnerabilities) {
// Recommend security-related scripts
const recommendedScripts = [
{ name: 'audit', command: 'npm audit', purpose: 'Run security audit' },
{ name: 'audit:fix', command: 'npm audit fix', purpose: 'Fix security vulnerabilities' }
];
for (const { name, command, purpose } of recommendedScripts) {
if (!scripts[name]) {
vulnerabilities.push({
file: 'package.json',
severity: 'low',
type: 'missing_security_script',
message: `Missing recommended script: "${name}" for ${purpose}`,
recommendation: `Add script: "${name}": "${command}"`
});
}
}
}
mapNpmSeverity(npmSeverity) {
switch (npmSeverity?.toLowerCase()) {
case 'critical':
return 'critical';
case 'high':
return 'high';
case 'moderate':
case 'medium':
return 'medium';
case 'low':
case 'info':
default:
return 'low';
}
}
extractVersion(versionString) {
// Remove version prefixes
return versionString.replace(/^[\^~>=<]+/, '');
}
isVersionLessThan(version1, version2) {
// Simple version comparison (not perfect but good enough)
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part < v2Part)
return true;
if (v1Part > v2Part)
return false;
}
return false;
}
isObfuscated(script) {
// Check for common obfuscation patterns
const obfuscationPatterns = [
/\\x[0-9a-f]{2}/i, // Hex encoding
/\\u[0-9a-f]{4}/i, // Unicode encoding
/atob\s*\(/, // Base64 decoding
/fromCharCode/, // Character code conversion
/\b[a-z]{1}\s*=\s*[a-z]{1}\s*\+\s*[a-z]{1}/i, // Single letter variable math
/[a-zA-Z0-9+/]{50,}={0,2}/ // Long base64 strings
];
let matches = 0;
for (const pattern of obfuscationPatterns) {
if (pattern.test(script))
matches++;
}
return matches >= 2; // Multiple obfuscation indicators
}
}
//# sourceMappingURL=DependencyScanner.js.map