UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

367 lines (331 loc) 11.2 kB
import { execSync } from 'child_process'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; export interface VulnerabilityReport { package: string; version: string; vulnerability: { id: string; title: string; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; references: string[]; cwe?: string[]; cvss?: { score: number; vector: string; }; }; fixAvailable: { available: boolean; version?: string; path?: string; }; paths: string[]; } export interface DependencyAuditResult { vulnerabilities: VulnerabilityReport[]; summary: { total: number; critical: number; high: number; medium: number; low: number; }; metadata: { totalDependencies: number; auditedAt: Date; tool: string; projectPath: string; }; } export interface ScanOptions { projectPath: string; includeDevDependencies?: boolean; skipAuditFix?: boolean; auditLevel?: 'low' | 'moderate' | 'high' | 'critical'; timeout?: number; } export class VulnerabilityScanner { private options: ScanOptions; constructor(options: ScanOptions) { this.options = { includeDevDependencies: true, skipAuditFix: true, auditLevel: 'low', timeout: 30000, ...options, }; } async scanDependencies(): Promise<DependencyAuditResult> { const packageJsonPath = join(this.options.projectPath, 'package.json'); if (!existsSync(packageJsonPath)) { throw new Error('package.json not found in project path'); } const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); const totalDependencies = this.countDependencies(packageJson); let vulnerabilities: VulnerabilityReport[] = []; // Try npm audit first try { vulnerabilities = await this.runNpmAudit(); } catch (error) { console.warn('npm audit failed, falling back to manual checks:', error); vulnerabilities = await this.runManualAudit(packageJson); } const summary = this.calculateSummary(vulnerabilities); return { vulnerabilities, summary, metadata: { totalDependencies, auditedAt: new Date(), tool: 'npm-audit', projectPath: this.options.projectPath, }, }; } private async runNpmAudit(): Promise<VulnerabilityReport[]> { try { const command = `npm audit --json ${this.options.auditLevel !== 'low' ? `--audit-level=${this.options.auditLevel}` : ''}`; const result = execSync(command, { cwd: this.options.projectPath, encoding: 'utf-8', timeout: this.options.timeout, stdio: 'pipe', }); const auditData = JSON.parse(result); return this.parseNpmAuditResult(auditData); } catch (error: any) { // npm audit returns non-zero exit code when vulnerabilities are found if (error.stdout) { try { const auditData = JSON.parse(error.stdout); return this.parseNpmAuditResult(auditData); } catch (parseError) { throw new Error(`Failed to parse npm audit output: ${parseError}`); } } throw error; } } private parseNpmAuditResult(auditData: any): VulnerabilityReport[] { const vulnerabilities: VulnerabilityReport[] = []; if (auditData.vulnerabilities) { Object.entries(auditData.vulnerabilities).forEach(([packageName, vulnData]: [string, any]) => { if (vulnData.via && Array.isArray(vulnData.via)) { vulnData.via.forEach((via: any) => { if (typeof via === 'object' && via.source) { vulnerabilities.push({ package: packageName, version: vulnData.range || 'unknown', vulnerability: { id: via.source.toString(), title: via.title || 'Unknown vulnerability', severity: this.mapSeverity(vulnData.severity), description: via.title || 'No description available', references: via.url ? [via.url] : [], ...(via.cwe && { cwe: [via.cwe.toString()] }), ...(via.cvss && { cvss: { score: via.cvss.score || 0, vector: via.cvss.vectorString || '', } }), }, fixAvailable: { available: !!vulnData.fixAvailable, version: vulnData.fixAvailable?.version, path: vulnData.fixAvailable?.path, }, paths: vulnData.effects || [], }); } }); } }); } return vulnerabilities; } private async runManualAudit(packageJson: any): Promise<VulnerabilityReport[]> { const vulnerabilities: VulnerabilityReport[] = []; const dependencies = { ...(packageJson.dependencies || {}), ...(this.options.includeDevDependencies ? packageJson.devDependencies || {} : {}), }; // Known vulnerable packages database (simplified) const knownVulnerabilities = await this.getKnownVulnerabilities(); Object.entries(dependencies).forEach(([pkg, version]: [string, any]) => { const vulns = knownVulnerabilities.filter(v => v.package === pkg); vulns.forEach(vuln => { if (this.isVersionVulnerable(version, vuln.affectedVersions)) { vulnerabilities.push({ package: pkg, version: version, vulnerability: vuln.vulnerability, fixAvailable: vuln.fixAvailable, paths: [pkg], }); } }); }); return vulnerabilities; } private async getKnownVulnerabilities(): Promise<Array<{ package: string; affectedVersions: string; vulnerability: VulnerabilityReport['vulnerability']; fixAvailable: VulnerabilityReport['fixAvailable']; }>> { // This would typically fetch from a vulnerability database // For now, return a static list of known vulnerabilities return [ { package: 'lodash', affectedVersions: '<4.17.21', vulnerability: { id: 'CVE-2021-23337', title: 'Prototype Pollution in lodash', severity: 'high', description: 'lodash versions prior to 4.17.21 are vulnerable to Command Injection via template.', references: ['https://nvd.nist.gov/vuln/detail/CVE-2021-23337'], cwe: ['CWE-94'], cvss: { score: 7.2, vector: 'CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H', }, }, fixAvailable: { available: true, version: '4.17.21', }, }, { package: 'axios', affectedVersions: '<0.21.2', vulnerability: { id: 'CVE-2021-3749', title: 'SSRF in axios', severity: 'medium', description: 'axios is vulnerable to Server-Side Request Forgery (SSRF)', references: ['https://nvd.nist.gov/vuln/detail/CVE-2021-3749'], cwe: ['CWE-918'], cvss: { score: 5.3, vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N', }, }, fixAvailable: { available: true, version: '0.21.2', }, }, { package: 'express', affectedVersions: '<4.17.3', vulnerability: { id: 'CVE-2022-24999', title: 'Open Redirect in express', severity: 'medium', description: 'Express.js is vulnerable to Open Redirect attacks', references: ['https://nvd.nist.gov/vuln/detail/CVE-2022-24999'], cwe: ['CWE-601'], cvss: { score: 6.1, vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N', }, }, fixAvailable: { available: true, version: '4.17.3', }, }, { package: 'jsonwebtoken', affectedVersions: '<8.5.1', vulnerability: { id: 'CVE-2022-23529', title: 'Algorithm confusion in jsonwebtoken', severity: 'high', description: 'jsonwebtoken is vulnerable to algorithm confusion attacks', references: ['https://nvd.nist.gov/vuln/detail/CVE-2022-23529'], cwe: ['CWE-327'], cvss: { score: 7.6, vector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L', }, }, fixAvailable: { available: true, version: '8.5.1', }, }, ]; } private isVersionVulnerable(installedVersion: string, vulnerableRange: string): boolean { // Simplified version comparison - in production, use semver library const cleanVersion = installedVersion.replace(/[^0-9.]/g, ''); const cleanRange = vulnerableRange.replace('<', ''); try { const installed = cleanVersion.split('.').map(Number); const vulnerable = cleanRange.split('.').map(Number); for (let i = 0; i < Math.max(installed.length, vulnerable.length); i++) { const installedPart = installed[i] || 0; const vulnerablePart = vulnerable[i] || 0; if (installedPart < vulnerablePart) return true; if (installedPart > vulnerablePart) return false; } return false; // Equal versions } catch (error) { console.warn(`Failed to compare versions: ${installedVersion} vs ${vulnerableRange}`); return false; } } private countDependencies(packageJson: any): number { const deps = Object.keys(packageJson.dependencies || {}).length; const devDeps = this.options.includeDevDependencies ? Object.keys(packageJson.devDependencies || {}).length : 0; return deps + devDeps; } private calculateSummary(vulnerabilities: VulnerabilityReport[]): DependencyAuditResult['summary'] { return vulnerabilities.reduce( (acc, vuln) => { acc.total++; acc[vuln.vulnerability.severity]++; return acc; }, { total: 0, critical: 0, high: 0, medium: 0, low: 0 } ); } private mapSeverity(severity: string): 'low' | 'medium' | 'high' | 'critical' { switch (severity?.toLowerCase()) { case 'critical': return 'critical'; case 'high': return 'high'; case 'moderate': return 'medium'; case 'low': return 'low'; default: return 'medium'; } } async generateFixScript(): Promise<string> { const auditResult = await this.scanDependencies(); const fixes: string[] = []; auditResult.vulnerabilities.forEach(vuln => { if (vuln.fixAvailable.available && vuln.fixAvailable.version) { fixes.push(`npm install ${vuln.package}@${vuln.fixAvailable.version}`); } }); if (fixes.length === 0) { return '# No automatic fixes available'; } return [ '#!/bin/bash', '# Automated vulnerability fixes', '# Generated by OrdoJS Security Scanner', '', ...fixes, '', '# Run audit again to verify fixes', 'npm audit', ].join('\n'); } }