// Copyright 2025 Chris Bunting
// Brief: Handles security vulnerability scanning and auditing
// Scope: Provides security analysis for dependencies including vulnerability detection
import { Vulnerability, SeverityLevel } from '@mcp-code-analysis/shared-types';
import { Logger } from '../utils/Logger.js';
export interface SecurityAudit {
package: string;
version: string;
vulnerabilities: Vulnerability[];
score: number;
recommendations: string[];
lastChecked: Date;
}
export interface AdvisorySource {
name: string;
url: string;
severity: SeverityLevel;
description: string;
}
export interface LicenseInfo {
name: string;
url?: string;
isPermissive: boolean;
isCopyleft: boolean;
requiresAttribution: boolean;
}
export class SecurityAuditor {
private logger: Logger;
private cache: Map<string, SecurityAudit>;
// @ts-ignore - Field reserved for future use
private _advisorySources: AdvisorySource[];
constructor(logger?: Logger) {
this.logger = logger || new Logger();
this.cache = new Map();
this._advisorySources = [
{
name: 'npm Advisory',
url: 'https://www.npmjs.com/advisories',
severity: SeverityLevel.ERROR,
description: 'Node Package Manager Security Advisory Database',
},
{
name: 'GitHub Advisory',
url: 'https://github.com/advisories',
severity: SeverityLevel.WARNING,
description: 'GitHub Security Advisory Database',
},
{
name: 'Snyk Vulnerability Database',
url: 'https://snyk.io/vuln/',
severity: SeverityLevel.WARNING,
description: 'Snyk Vulnerability Database',
},
{
name: 'PyPI Security Advisories',
url: 'https://pypi.org/security/',
severity: SeverityLevel.ERROR,
description: 'Python Package Index Security Advisories',
},
{
name: 'RustSec Advisory Database',
url: 'https://rustsec.org/',
severity: SeverityLevel.WARNING,
description: 'Rust Security Advisory Database',
},
];
}
async auditPackage(
packageName: string,
version: string,
packageManager: string
): Promise<SecurityAudit> {
this.logger.debug(`Auditing security for ${packageName}@${version}`);
const cacheKey = `${packageName}:${version}:${packageManager}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
const audit: SecurityAudit = {
package: packageName,
version,
vulnerabilities: [],
score: 0,
recommendations: [],
lastChecked: new Date(),
};
try {
// Check for vulnerabilities from various sources
const vulnerabilities = await this.checkVulnerabilities(packageName, version, packageManager);
audit.vulnerabilities = vulnerabilities;
// Calculate security score
audit.score = this.calculateSecurityScore(vulnerabilities);
// Generate recommendations
audit.recommendations = this.generateRecommendations(vulnerabilities, packageManager);
// Cache the result
this.cache.set(cacheKey, audit);
this.logger.debug(`Security audit completed for ${packageName}@${version}: score ${audit.score}`);
} catch (error) {
this.logger.error(`Error auditing security for ${packageName}@${version}:`, error);
audit.recommendations.push('Security audit failed. Please try again later.');
}
return audit;
}
private async checkVulnerabilities(
packageName: string,
version: string,
packageManager: string
): Promise<Vulnerability[]> {
const vulnerabilities: Vulnerability[] = [];
try {
// This is a simplified implementation
// In a real implementation, you would:
// 1. Query actual vulnerability databases (npm audit, Snyk, etc.)
// 2. Use APIs like GitHub Advisory, OSV, etc.
// 3. Check multiple sources for comprehensive coverage
// Simulate vulnerability detection for demo purposes
const mockVulnerabilities = await this.getMockVulnerabilities(packageName, version, packageManager);
vulnerabilities.push(...mockVulnerabilities);
// Filter by severity if needed
// This would be configurable based on user preferences
} catch (error) {
this.logger.error(`Error checking vulnerabilities for ${packageName}:`, error);
}
return vulnerabilities;
}
private async getMockVulnerabilities(
packageName: string,
version: string,
_packageManager: string
): Promise<Vulnerability[]> {
// This is a mock implementation for demonstration
// In a real implementation, this would query actual vulnerability databases
const vulnerabilities: Vulnerability[] = [];
// Simulate some known vulnerabilities for common packages
const knownVulnerabilities: Record<string, Array<{ versionRange: string; severity: SeverityLevel; description: string }>> = {
'lodash': [
{
versionRange: '<4.17.12',
severity: SeverityLevel.ERROR,
description: 'Prototype Pollution in lodash',
},
],
'express': [
{
versionRange: '<4.16.0',
severity: SeverityLevel.WARNING,
description: 'Open redirect vulnerability in Express',
},
],
'react': [
{
versionRange: '<16.9.0',
severity: SeverityLevel.WARNING,
description: 'XSS vulnerability in React',
},
],
'axios': [
{
versionRange: '<0.21.1',
severity: SeverityLevel.ERROR,
description: 'Server-Side Request Forgery in axios',
},
],
};
const packageVulnerabilities = knownVulnerabilities[packageName.toLowerCase()];
if (packageVulnerabilities) {
for (const vuln of packageVulnerabilities) {
// Check if the current version is in the vulnerable range
if (this.isVersionInRange(version, vuln.versionRange)) {
vulnerabilities.push({
id: `${packageName}-${vuln.description.replace(/\s+/g, '-').toLowerCase()}`,
severity: vuln.severity,
description: vuln.description,
fixedVersion: this.getFixedVersion(vuln.versionRange),
cve: this.generateMockCVE(packageName, vuln.description),
references: [
`https://www.npmjs.com/advisories/${packageName}-${vuln.description.replace(/\s+/g, '-')}`,
'https://cve.mitre.org/',
],
});
}
}
}
return vulnerabilities;
}
private isVersionInRange(version: string, range: string): boolean {
// Simple version range checking
// In a real implementation, use semver or similar library
try {
if (range.startsWith('<')) {
const maxVersion = range.substring(1);
return this.compareVersions(version, maxVersion) < 0;
} else if (range.startsWith('<=')) {
const maxVersion = range.substring(2);
return this.compareVersions(version, maxVersion) <= 0;
} else if (range.startsWith('>')) {
const minVersion = range.substring(1);
return this.compareVersions(version, minVersion) > 0;
} else if (range.startsWith('>=')) {
const minVersion = range.substring(2);
return this.compareVersions(version, minVersion) >= 0;
}
} catch (error) {
this.logger.error(`Error checking version range ${range}:`, error);
}
return false;
}
private compareVersions(version1: string, version2: string): number {
// Simple version comparison
// In a real implementation, use semver.compare
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 1;
if (v1Part < v2Part) return -1;
}
return 0;
}
private getFixedVersion(range: string): string {
// Extract the fixed version from the range
// This is a simplified implementation
if (range.startsWith('<')) {
return range.substring(1);
} else if (range.startsWith('<=')) {
return range.substring(2);
}
return 'latest';
}
private generateMockCVE(packageName: string, description: string): string {
// Generate a mock CVE ID for demonstration
const hash = packageName.length + description.length;
return `CVE-2024-${(hash % 9999).toString().padStart(4, '0')}`;
}
private calculateSecurityScore(vulnerabilities: Vulnerability[]): number {
if (vulnerabilities.length === 0) {
return 100; // Perfect score if no vulnerabilities
}
let score = 100;
const severityWeights = {
[SeverityLevel.ERROR]: 30,
[SeverityLevel.WARNING]: 15,
[SeverityLevel.INFO]: 5,
[SeverityLevel.HINT]: 2,
};
for (const vuln of vulnerabilities) {
score -= severityWeights[vuln.severity];
}
return Math.max(0, score); // Ensure score doesn't go below 0
}
private generateRecommendations(
vulnerabilities: Vulnerability[],
packageManager: string
): string[] {
const recommendations: string[] = [];
if (vulnerabilities.length === 0) {
recommendations.push('No security vulnerabilities found. Keep dependencies updated.');
return recommendations;
}
// Group vulnerabilities by severity
const bySeverity = vulnerabilities.reduce((acc, vuln) => {
if (!acc[vuln.severity]) {
acc[vuln.severity] = [];
}
acc[vuln.severity].push(vuln);
return acc;
}, {} as Record<SeverityLevel, Vulnerability[]>);
// Generate recommendations based on severity
if (bySeverity[SeverityLevel.ERROR]) {
recommendations.push(
`🚨 CRITICAL: ${bySeverity[SeverityLevel.ERROR].length} critical vulnerabilities found. Update immediately!`
);
}
if (bySeverity[SeverityLevel.WARNING]) {
recommendations.push(
`⚠️ WARNING: ${bySeverity[SeverityLevel.WARNING].length} vulnerabilities found. Update recommended.`
);
}
// Package manager specific recommendations
switch (packageManager) {
case 'npm':
case 'yarn':
recommendations.push('Run `npm audit fix` or `yarn audit` to automatically fix vulnerabilities.');
break;
case 'pip':
recommendations.push('Use `pip-audit` or `safety check` to scan for vulnerabilities.');
break;
case 'cargo':
recommendations.push('Use `cargo audit` to check for Rust crate vulnerabilities.');
break;
default:
recommendations.push('Use your package manager\'s security audit tools.');
}
// General recommendations
recommendations.push('Regularly update dependencies to patch security vulnerabilities.');
recommendations.push('Consider using automated security scanning in your CI/CD pipeline.');
recommendations.push('Monitor security advisories for your dependencies.');
return recommendations;
}
async checkLicenseCompatibility(
packageName: string,
version: string,
allowedLicenses: string[]
): Promise<{
compatible: boolean;
license: LicenseInfo | null;
issues: string[];
}> {
this.logger.debug(`Checking license compatibility for ${packageName}@${version}`);
const result = {
compatible: true,
license: null as LicenseInfo | null,
issues: [] as string[],
};
try {
// Mock license detection
const licenseInfo = await this.getLicenseInfo(packageName, version);
result.license = licenseInfo;
if (licenseInfo && !allowedLicenses.includes(licenseInfo.name)) {
result.compatible = false;
result.issues.push(`License "${licenseInfo.name}" is not in the allowed licenses list.`);
}
// Additional license checks
if (licenseInfo?.isCopyleft) {
result.issues.push('Package uses copyleft license which may require sharing your source code.');
}
} catch (error) {
this.logger.error(`Error checking license compatibility for ${packageName}:`, error);
result.issues.push('License check failed. Please verify manually.');
}
return result;
}
private async getLicenseInfo(_packageName: string, _version: string): Promise<LicenseInfo | null> {
// Mock license detection
// In a real implementation, this would query package registries or analyze package files
const knownLicenses: Record<string, LicenseInfo> = {
'MIT': {
name: 'MIT',
isPermissive: true,
isCopyleft: false,
requiresAttribution: true,
},
'Apache-2.0': {
name: 'Apache-2.0',
isPermissive: true,
isCopyleft: false,
requiresAttribution: true,
},
'BSD-3-Clause': {
name: 'BSD-3-Clause',
isPermissive: true,
isCopyleft: false,
requiresAttribution: true,
},
'GPL-3.0': {
name: 'GPL-3.0',
isPermissive: false,
isCopyleft: true,
requiresAttribution: true,
},
'LGPL-2.1': {
name: 'LGPL-2.1',
isPermissive: false,
isCopyleft: true,
requiresAttribution: true,
},
};
// Return a mock license for demonstration
const licenseKeys = Object.keys(knownLicenses);
const randomLicense = licenseKeys[Math.floor(Math.random() * licenseKeys.length)];
return knownLicenses[randomLicense] || null;
}
async generateSecurityReport(
audits: SecurityAudit[]
): Promise<{
summary: {
totalPackages: number;
vulnerablePackages: number;
criticalVulnerabilities: number;
averageScore: number;
};
recommendations: string[];
vulnerablePackages: Array<{
package: string;
version: string;
score: number;
vulnerabilityCount: number;
}>;
}> {
this.logger.debug('Generating security report');
const summary = {
totalPackages: audits.length,
vulnerablePackages: 0,
criticalVulnerabilities: 0,
averageScore: 0,
};
const vulnerablePackages: Array<{
package: string;
version: string;
score: number;
vulnerabilityCount: number;
}> = [];
let totalScore = 0;
for (const audit of audits) {
totalScore += audit.score;
if (audit.vulnerabilities.length > 0) {
summary.vulnerablePackages++;
vulnerablePackages.push({
package: audit.package,
version: audit.version,
score: audit.score,
vulnerabilityCount: audit.vulnerabilities.length,
});
// Count critical vulnerabilities
const criticalCount = audit.vulnerabilities.filter(v => v.severity === SeverityLevel.ERROR).length;
summary.criticalVulnerabilities += criticalCount;
}
}
summary.averageScore = audits.length > 0 ? totalScore / audits.length : 100;
// Sort vulnerable packages by score (lowest first)
vulnerablePackages.sort((a, b) => a.score - b.score);
// Generate overall recommendations
const recommendations: string[] = [];
if (summary.vulnerablePackages > 0) {
recommendations.push(
`${summary.vulnerablePackages} packages have security vulnerabilities that need attention.`
);
}
if (summary.criticalVulnerabilities > 0) {
recommendations.push(
`🚨 ${summary.criticalVulnerabilities} critical vulnerabilities require immediate action.`
);
}
if (summary.averageScore < 80) {
recommendations.push('Overall security score is below 80. Consider updating dependencies.');
}
recommendations.push('Implement regular security scanning in your development workflow.');
recommendations.push('Set up automated alerts for new vulnerabilities in your dependencies.');
return {
summary,
recommendations,
vulnerablePackages,
};
}
clearCache(): void {
this.cache.clear();
this.logger.debug('Security auditor cache cleared');
}
}