UNPKG

@ordojs/security

Version:

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

285 lines 11.7 kB
import { execSync } from 'child_process'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; export class VulnerabilityScanner { options; constructor(options) { this.options = { includeDevDependencies: true, skipAuditFix: true, auditLevel: 'low', timeout: 30000, ...options, }; } async scanDependencies() { 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 = []; // 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, }, }; } async runNpmAudit() { 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) { // 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; } } parseNpmAuditResult(auditData) { const vulnerabilities = []; if (auditData.vulnerabilities) { Object.entries(auditData.vulnerabilities).forEach(([packageName, vulnData]) => { if (vulnData.via && Array.isArray(vulnData.via)) { vulnData.via.forEach((via) => { 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; } async runManualAudit(packageJson) { const vulnerabilities = []; 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]) => { 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; } async getKnownVulnerabilities() { // 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', }, }, ]; } isVersionVulnerable(installedVersion, vulnerableRange) { // 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; } } countDependencies(packageJson) { const deps = Object.keys(packageJson.dependencies || {}).length; const devDeps = this.options.includeDevDependencies ? Object.keys(packageJson.devDependencies || {}).length : 0; return deps + devDeps; } calculateSummary(vulnerabilities) { return vulnerabilities.reduce((acc, vuln) => { acc.total++; acc[vuln.vulnerability.severity]++; return acc; }, { total: 0, critical: 0, high: 0, medium: 0, low: 0 }); } mapSeverity(severity) { switch (severity?.toLowerCase()) { case 'critical': return 'critical'; case 'high': return 'high'; case 'moderate': return 'medium'; case 'low': return 'low'; default: return 'medium'; } } async generateFixScript() { const auditResult = await this.scanDependencies(); const fixes = []; 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'); } } //# sourceMappingURL=vulnerability-scanner.js.map