@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
367 lines (331 loc) • 11.2 kB
text/typescript
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');
}
}