@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
285 lines • 11.7 kB
JavaScript
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