@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
380 lines (300 loc) • 13.1 kB
text/typescript
import { execSync } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SecurityAuditOptions, SecurityAuditor } from './security-auditor';
// Mock file system and child_process
vi.mock('fs');
vi.mock('child_process');
vi.mock('glob', () => ({
globSync: vi.fn(),
}));
const mockExistsSync = vi.mocked(existsSync);
const mockReadFileSync = vi.mocked(readFileSync);
const mockExecSync = vi.mocked(execSync);
describe('SecurityAuditor', () => {
let auditor: SecurityAuditor;
let options: SecurityAuditOptions;
beforeEach(() => {
options = {
projectPath: '/test/project',
enableDependencyCheck: true,
enableCodeAnalysis: true,
enableConfigurationCheck: true,
};
auditor = new SecurityAuditor(options);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('audit', () => {
it('should perform a complete security audit', async () => {
// Mock package.json
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(JSON.stringify({
dependencies: { lodash: '^4.17.20' },
devDependencies: { axios: '^0.21.1' }
}));
// Mock npm audit
mockExecSync.mockReturnValue(JSON.stringify({
vulnerabilities: {
lodash: {
severity: 'high',
via: [{
source: 'CVE-2021-23337',
title: 'Prototype Pollution',
url: 'https://example.com'
}]
}
}
}));
// Mock glob for source files
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/test.ts']);
// Mock source file with vulnerability
mockReadFileSync.mockImplementation((path: any) => {
if (path.includes('package.json')) {
return JSON.stringify({
dependencies: { lodash: '^4.17.20' },
devDependencies: { axios: '^0.21.1' }
});
}
if (path.includes('test.ts')) {
return 'element.innerHTML = userInput; // XSS vulnerability';
}
return '';
});
const result = await auditor.audit();
// The audit will find multiple vulnerabilities:
// 1. npm audit vulnerability (lodash)
// 2. known vulnerable packages (lodash, axios)
// 3. XSS vulnerability in code
// 4. configuration issues (no security headers, no HTTPS)
expect(result.vulnerabilities.length).toBeGreaterThan(0);
expect(result.summary.total).toBeGreaterThan(0);
expect(result.owaspCompliance.score).toBeLessThan(100);
expect(result.timestamp).toBeInstanceOf(Date);
// Check that we have at least one dependency and one code vulnerability
const hasDepVuln = result.vulnerabilities.some(v => v.type === 'dependency');
const hasCodeVuln = result.vulnerabilities.some(v => v.type === 'xss');
expect(hasDepVuln).toBe(true);
expect(hasCodeVuln).toBe(true);
});
it('should handle npm audit failures gracefully', async () => {
mockExistsSync.mockImplementation((path: any) => {
return path.includes('package.json');
});
mockReadFileSync.mockImplementation((path: any) => {
if (path.includes('package.json')) {
return JSON.stringify({
dependencies: { lodash: '^4.17.20' },
scripts: { dev: 'ordojs dev' }
});
}
return '';
});
// Mock npm audit failure
mockExecSync.mockImplementation(() => {
throw new Error('npm audit failed');
});
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue([]);
const result = await auditor.audit();
// Should find known vulnerable packages and configuration issues
expect(result.vulnerabilities.length).toBeGreaterThan(0);
const hasDepVuln = result.vulnerabilities.some(v => v.type === 'dependency' && v.description.includes('lodash'));
expect(hasDepVuln).toBe(true);
});
});
describe('XSS vulnerability detection', () => {
it('should detect innerHTML usage without sanitization', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/component.ts']);
mockExistsSync.mockReturnValue(false); // No package.json
mockReadFileSync.mockReturnValue('element.innerHTML = userInput;');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('xss');
expect(result.vulnerabilities[0].severity).toBe('high');
expect(result.vulnerabilities[0].description).toContain('innerHTML');
});
it('should detect eval usage', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/dangerous.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('eval(userCode);');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('xss');
expect(result.vulnerabilities[0].severity).toBe('critical');
expect(result.vulnerabilities[0].description).toContain('eval');
});
it('should detect unescaped template literals in HTML', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/template.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('const html = `<div>${userInput}</div>`;');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('xss');
expect(result.vulnerabilities[0].severity).toBe('medium');
});
});
describe('SQL injection detection', () => {
it('should detect string concatenation in SQL queries', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/database.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('const query = "SELECT * FROM users WHERE id = " + userId;');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('injection');
expect(result.vulnerabilities[0].severity).toBe('high');
expect(result.vulnerabilities[0].description).toContain('SQL injection');
});
});
describe('CSRF vulnerability detection', () => {
it('should detect forms without CSRF protection', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/form.html']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('<form method="post" action="/submit">');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('csrf');
expect(result.vulnerabilities[0].severity).toBe('medium');
});
});
describe('Cryptographic vulnerabilities', () => {
it('should detect weak cryptographic algorithms', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/crypto.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('crypto.createHash("md5").update(data);');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('other');
expect(result.vulnerabilities[0].severity).toBe('medium');
expect(result.vulnerabilities[0].description).toContain('cryptographic');
});
});
describe('Hardcoded secrets detection', () => {
it('should detect hardcoded passwords', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/config.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('const password = "secret123";');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('other');
expect(result.vulnerabilities[0].severity).toBe('high');
expect(result.vulnerabilities[0].description).toContain('Hardcoded secret');
});
it('should detect hardcoded API keys', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/api.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('const apiKey = "sk-1234567890abcdef";');
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].description).toContain('Hardcoded secret');
});
});
describe('Configuration audit', () => {
it('should detect missing security headers configuration', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue([]);
mockExistsSync.mockImplementation((path: any) => {
return path.includes('ordojs.config.ts');
});
mockReadFileSync.mockImplementation((path: any) => {
if (path.includes('ordojs.config.ts')) {
return 'export default { build: { outDir: "dist" } };';
}
return '';
});
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('configuration');
expect(result.vulnerabilities[0].description).toContain('security headers');
});
it('should detect missing HTTPS configuration', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue([]);
mockExistsSync.mockImplementation((path: any) => {
return path.includes('package.json');
});
mockReadFileSync.mockReturnValue(JSON.stringify({
scripts: {
dev: 'ordojs dev',
build: 'ordojs build'
}
}));
const result = await auditor.audit();
expect(result.vulnerabilities).toHaveLength(1);
expect(result.vulnerabilities[0].type).toBe('configuration');
expect(result.vulnerabilities[0].description).toContain('HTTPS');
});
});
describe('OWASP compliance', () => {
it('should calculate OWASP compliance score', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue([]);
mockExistsSync.mockReturnValue(false);
const result = await auditor.audit();
expect(result.owaspCompliance).toBeDefined();
expect(result.owaspCompliance.score).toBeGreaterThanOrEqual(0);
expect(result.owaspCompliance.score).toBeLessThanOrEqual(100);
expect(result.owaspCompliance.categories).toBeDefined();
});
it('should mark OWASP categories as non-compliant when vulnerabilities exist', async () => {
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue(['src/xss.ts']);
mockExistsSync.mockReturnValue(false);
mockReadFileSync.mockReturnValue('element.innerHTML = userInput;');
const result = await auditor.audit();
expect(result.owaspCompliance.categories['A03:2021 – Injection']).toBe(false);
});
});
describe('options handling', () => {
it('should respect enableDependencyCheck option', async () => {
const auditorWithoutDeps = new SecurityAuditor({
...options,
enableDependencyCheck: false,
});
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue([]);
mockExistsSync.mockReturnValue(false);
const result = await auditorWithoutDeps.audit();
expect(mockExecSync).not.toHaveBeenCalled();
expect(result.vulnerabilities).toHaveLength(0);
});
it('should respect enableCodeAnalysis option', async () => {
const auditorWithoutCode = new SecurityAuditor({
...options,
enableCodeAnalysis: false,
});
mockExistsSync.mockReturnValue(false);
const result = await auditorWithoutCode.audit();
const { globSync } = await import('glob');
expect(vi.mocked(globSync)).not.toHaveBeenCalled();
});
it('should respect custom include/exclude patterns', async () => {
const customAuditor = new SecurityAuditor({
...options,
includePatterns: ['**/*.custom'],
excludePatterns: ['**/ignore/**'],
});
const { globSync } = await import('glob');
vi.mocked(globSync).mockReturnValue([]);
mockExistsSync.mockReturnValue(false);
await customAuditor.audit();
expect(vi.mocked(globSync)).toHaveBeenCalledWith(
['**/*.custom'],
expect.objectContaining({
ignore: ['**/ignore/**'],
})
);
});
});
});