mirror of
https://github.com/zadam/trilium.git
synced 2025-10-28 08:46:43 +01:00
912 lines
25 KiB
Markdown
Vendored
912 lines
25 KiB
Markdown
Vendored
# Security Testing Guide
|
|
|
|
This comprehensive guide covers security testing methodologies, tools, and procedures for ensuring the security of Trilium's codebase and deployments.
|
|
|
|
## Security Testing Overview
|
|
|
|
### Testing Pyramid for Security
|
|
|
|
```
|
|
Manual Security Testing
|
|
(Penetration Testing, Code Review)
|
|
┌─────────────────────────────┐
|
|
│ │
|
|
│ Integration Security │
|
|
│ (API, E2E Tests) │
|
|
└─────────────────────────────┘
|
|
┌─────────────────────────────────┐
|
|
│ │
|
|
│ Unit Security Tests │
|
|
│ (Input Validation, Crypto) │
|
|
└─────────────────────────────────────┘
|
|
┌─────────────────────────────────────────┐
|
|
│ │
|
|
│ Static Analysis │
|
|
│ (SAST, Dependency Scanning) │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Security Testing Goals
|
|
|
|
1. **Vulnerability Detection**: Identify security flaws before deployment
|
|
2. **Compliance Verification**: Ensure adherence to security standards
|
|
3. **Risk Assessment**: Evaluate potential security impact
|
|
4. **Defense Validation**: Verify security controls effectiveness
|
|
|
|
## Static Analysis Security Testing (SAST)
|
|
|
|
### Code Quality and Security Analysis
|
|
|
|
#### ESLint Security Rules
|
|
|
|
```json
|
|
{
|
|
"extends": [
|
|
"plugin:security/recommended",
|
|
"plugin:@typescript-eslint/recommended"
|
|
],
|
|
"rules": {
|
|
"security/detect-sql-injection": "error",
|
|
"security/detect-xss": "error",
|
|
"security/detect-eval-with-expression": "error",
|
|
"security/detect-non-literal-fs-filename": "warn",
|
|
"security/detect-unsafe-regex": "error",
|
|
"security/detect-buffer-noassert": "error",
|
|
"security/detect-child-process": "warn",
|
|
"security/detect-disable-mustache-escape": "error",
|
|
"security/detect-no-csrf-before-method-override": "error",
|
|
"security/detect-non-literal-require": "warn",
|
|
"security/detect-possible-timing-attacks": "error",
|
|
"security/detect-pseudoRandomBytes": "error"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### SonarQube Security Rules
|
|
|
|
```javascript
|
|
// Example sonar-project.properties configuration
|
|
sonar.projectKey=trilium-security
|
|
sonar.sources=src
|
|
sonar.exclusions=**/*.test.ts,**/*.spec.ts,**/node_modules/**
|
|
sonar.typescript.tsconfigPath=tsconfig.json
|
|
|
|
// Security-focused quality gates
|
|
sonar.qualitygate.wait=true
|
|
sonar.security.hotspots.maxAllowed=0
|
|
sonar.security.vulnerabilities.maxAllowed=0
|
|
```
|
|
|
|
#### Custom Security Linting Rules
|
|
|
|
```typescript
|
|
// Custom ESLint rule for Trilium-specific security patterns
|
|
module.exports = {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description: "Detect unencrypted storage of sensitive data"
|
|
}
|
|
},
|
|
create(context) {
|
|
return {
|
|
CallExpression(node) {
|
|
if (node.callee.name === 'setOption' &&
|
|
node.arguments[0].value.includes('password')) {
|
|
context.report({
|
|
node,
|
|
message: "Potential unencrypted password storage"
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
```
|
|
|
|
### Dependency Vulnerability Scanning
|
|
|
|
#### npm audit Integration
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# security-scan.sh - Comprehensive dependency scanning
|
|
|
|
echo "Running npm audit..."
|
|
npm audit --audit-level moderate
|
|
|
|
echo "Checking for known vulnerabilities..."
|
|
npm audit --json > audit-report.json
|
|
|
|
echo "Analyzing audit results..."
|
|
node scripts/analyze-audit.js audit-report.json
|
|
|
|
echo "Checking for outdated packages..."
|
|
npm outdated
|
|
|
|
echo "Running Snyk scan..."
|
|
npx snyk test --json > snyk-report.json
|
|
|
|
echo "Generating security report..."
|
|
node scripts/generate-security-report.js
|
|
```
|
|
|
|
#### Automated Vulnerability Monitoring
|
|
|
|
```typescript
|
|
// scripts/analyze-audit.js
|
|
interface VulnerabilityReport {
|
|
advisories: Record<string, {
|
|
severity: string;
|
|
title: string;
|
|
module_name: string;
|
|
vulnerable_versions: string;
|
|
patched_versions: string;
|
|
}>;
|
|
}
|
|
|
|
function analyzeAuditReport(report: VulnerabilityReport): void {
|
|
const criticalVulns = Object.values(report.advisories)
|
|
.filter(vuln => vuln.severity === 'critical');
|
|
|
|
if (criticalVulns.length > 0) {
|
|
console.error(`Found ${criticalVulns.length} critical vulnerabilities`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const highVulns = Object.values(report.advisories)
|
|
.filter(vuln => vuln.severity === 'high');
|
|
|
|
if (highVulns.length > 5) {
|
|
console.warn(`Found ${highVulns.length} high severity vulnerabilities`);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dynamic Application Security Testing (DAST)
|
|
|
|
### Automated Security Scanning
|
|
|
|
#### OWASP ZAP Integration
|
|
|
|
```yaml
|
|
# .github/workflows/security-scan.yml
|
|
name: Security Scan
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [ main ]
|
|
schedule:
|
|
- cron: '0 2 * * 1' # Weekly scan
|
|
|
|
jobs:
|
|
security-scan:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Start application
|
|
run: npm start &
|
|
|
|
- name: Wait for application
|
|
run: sleep 30
|
|
|
|
- name: Run OWASP ZAP scan
|
|
uses: zaproxy/action-full-scan@v0.4.0
|
|
with:
|
|
target: 'http://localhost:8080'
|
|
rules_file_name: '.zap/rules.tsv'
|
|
cmd_options: '-a'
|
|
|
|
- name: Upload ZAP results
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: zap-report
|
|
path: report_html.html
|
|
```
|
|
|
|
#### Custom Security Test Suite
|
|
|
|
```typescript
|
|
// test/security/security.test.ts
|
|
import request from 'supertest';
|
|
import app from '../../src/app';
|
|
|
|
describe('Security Tests', () => {
|
|
describe('XSS Protection', () => {
|
|
test('should sanitize script tags in note content', async () => {
|
|
const maliciousContent = '<script>alert("XSS")</script>Hello';
|
|
|
|
const response = await request(app)
|
|
.post('/api/notes')
|
|
.send({
|
|
title: 'Test Note',
|
|
content: maliciousContent
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.content).not.toContain('<script>');
|
|
expect(response.body.content).toContain('Hello');
|
|
});
|
|
|
|
test('should set appropriate security headers', async () => {
|
|
const response = await request(app)
|
|
.get('/')
|
|
.expect(200);
|
|
|
|
expect(response.headers['x-frame-options']).toBe('DENY');
|
|
expect(response.headers['x-content-type-options']).toBe('nosniff');
|
|
expect(response.headers['x-xss-protection']).toBe('1; mode=block');
|
|
});
|
|
});
|
|
|
|
describe('SQL Injection Protection', () => {
|
|
test('should reject SQL injection attempts', async () => {
|
|
const sqlInjection = "'; DROP TABLE notes; --";
|
|
|
|
const response = await request(app)
|
|
.get(`/api/search?query=${encodeURIComponent(sqlInjection)}`)
|
|
.expect(400);
|
|
|
|
expect(response.body.error).toContain('Invalid query');
|
|
});
|
|
});
|
|
|
|
describe('Authentication Security', () => {
|
|
test('should require authentication for protected endpoints', async () => {
|
|
await request(app)
|
|
.get('/api/notes')
|
|
.expect(401);
|
|
});
|
|
|
|
test('should validate session tokens', async () => {
|
|
await request(app)
|
|
.get('/api/notes')
|
|
.set('Cookie', 'session=invalid-token')
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe('CSRF Protection', () => {
|
|
test('should require CSRF token for state-changing operations', async () => {
|
|
const session = await createAuthenticatedSession();
|
|
|
|
await request(app)
|
|
.post('/api/notes')
|
|
.set('Cookie', session.cookie)
|
|
// Missing CSRF token
|
|
.send({ title: 'Test' })
|
|
.expect(403);
|
|
});
|
|
|
|
test('should accept valid CSRF tokens', async () => {
|
|
const session = await createAuthenticatedSession();
|
|
|
|
await request(app)
|
|
.post('/api/notes')
|
|
.set('Cookie', session.cookie)
|
|
.set('X-CSRF-Token', session.csrfToken)
|
|
.send({ title: 'Test' })
|
|
.expect(201);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### Performance Security Testing
|
|
|
|
#### Rate Limiting Tests
|
|
|
|
```typescript
|
|
describe('Rate Limiting Security', () => {
|
|
test('should rate limit login attempts', async () => {
|
|
const loginData = { password: 'wrong-password' };
|
|
|
|
// Attempt multiple failed logins
|
|
const promises = Array(10).fill(null).map(() =>
|
|
request(app)
|
|
.post('/api/login')
|
|
.send(loginData)
|
|
);
|
|
|
|
const responses = await Promise.all(promises);
|
|
const rateLimited = responses.filter(r => r.status === 429);
|
|
|
|
expect(rateLimited.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should rate limit API requests per IP', async () => {
|
|
const session = await createAuthenticatedSession();
|
|
|
|
const promises = Array(101).fill(null).map(() =>
|
|
request(app)
|
|
.get('/api/notes')
|
|
.set('Cookie', session.cookie)
|
|
.set('X-CSRF-Token', session.csrfToken)
|
|
);
|
|
|
|
const responses = await Promise.all(promises);
|
|
const rateLimited = responses.filter(r => r.status === 429);
|
|
|
|
expect(rateLimited.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
#### DoS Protection Tests
|
|
|
|
```typescript
|
|
describe('DoS Protection', () => {
|
|
test('should limit request payload size', async () => {
|
|
const largePayload = 'x'.repeat(10 * 1024 * 1024); // 10MB
|
|
|
|
await request(app)
|
|
.post('/api/notes')
|
|
.send({ title: 'Test', content: largePayload })
|
|
.expect(413); // Payload too large
|
|
});
|
|
|
|
test('should timeout long-running requests', async () => {
|
|
const start = Date.now();
|
|
|
|
try {
|
|
await request(app)
|
|
.get('/api/export/large-dataset')
|
|
.timeout(5000);
|
|
} catch (error) {
|
|
const duration = Date.now() - start;
|
|
expect(duration).toBeGreaterThan(5000);
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
## Cryptographic Security Testing
|
|
|
|
### Encryption Algorithm Tests
|
|
|
|
```typescript
|
|
describe('Encryption Security', () => {
|
|
describe('AES-128-CBC Implementation', () => {
|
|
test('should use different IVs for each encryption', () => {
|
|
const key = crypto.randomBytes(16);
|
|
const plaintext = 'sensitive data';
|
|
|
|
const encrypted1 = dataEncryption.encrypt(key, plaintext);
|
|
const encrypted2 = dataEncryption.encrypt(key, plaintext);
|
|
|
|
expect(encrypted1).not.toBe(encrypted2);
|
|
});
|
|
|
|
test('should detect tampered ciphertext', () => {
|
|
const key = crypto.randomBytes(16);
|
|
const plaintext = 'important data';
|
|
|
|
const encrypted = dataEncryption.encrypt(key, plaintext);
|
|
|
|
// Tamper with the ciphertext
|
|
const tamperedEncrypted = encrypted.slice(0, -4) + '0000';
|
|
|
|
expect(() => {
|
|
dataEncryption.decrypt(key, tamperedEncrypted);
|
|
}).toThrow();
|
|
});
|
|
|
|
test('should fail gracefully with wrong key', () => {
|
|
const key1 = crypto.randomBytes(16);
|
|
const key2 = crypto.randomBytes(16);
|
|
const plaintext = 'secret information';
|
|
|
|
const encrypted = dataEncryption.encrypt(key1, plaintext);
|
|
const result = dataEncryption.decrypt(key2, encrypted);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Key Derivation (Scrypt)', () => {
|
|
test('should use sufficient work factor', () => {
|
|
const password = 'test-password';
|
|
const salt = crypto.randomBytes(32);
|
|
|
|
const start = Date.now();
|
|
const derivedKey = myScrypt.getScryptHash(password, salt);
|
|
const duration = Date.now() - start;
|
|
|
|
// Should take at least 100ms (adjust based on requirements)
|
|
expect(duration).toBeGreaterThan(100);
|
|
expect(derivedKey).toHaveLength(32);
|
|
});
|
|
|
|
test('should produce different outputs with different salts', () => {
|
|
const password = 'same-password';
|
|
const salt1 = crypto.randomBytes(32);
|
|
const salt2 = crypto.randomBytes(32);
|
|
|
|
const hash1 = myScrypt.getScryptHash(password, salt1);
|
|
const hash2 = myScrypt.getScryptHash(password, salt2);
|
|
|
|
expect(hash1).not.toEqual(hash2);
|
|
});
|
|
});
|
|
|
|
describe('Random Number Generation', () => {
|
|
test('should use cryptographically secure randomness', () => {
|
|
const random1 = crypto.randomBytes(32);
|
|
const random2 = crypto.randomBytes(32);
|
|
|
|
expect(random1).not.toEqual(random2);
|
|
expect(random1).toHaveLength(32);
|
|
expect(random2).toHaveLength(32);
|
|
});
|
|
|
|
test('should have sufficient entropy', () => {
|
|
const samples = Array(1000).fill(null).map(() =>
|
|
crypto.randomBytes(4).readUInt32BE(0)
|
|
);
|
|
|
|
// Basic entropy test - check for duplicates
|
|
const uniqueValues = new Set(samples);
|
|
const uniqueRatio = uniqueValues.size / samples.length;
|
|
|
|
expect(uniqueRatio).toBeGreaterThan(0.99);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### TOTP Security Tests
|
|
|
|
```typescript
|
|
describe('TOTP Security', () => {
|
|
test('should generate valid TOTP secrets', () => {
|
|
const secret = totpService.createSecret();
|
|
|
|
expect(secret.success).toBe(true);
|
|
expect(secret.message).toMatch(/^[A-Z2-7]{32}$/); // Base32 format
|
|
});
|
|
|
|
test('should validate correct TOTP codes', () => {
|
|
const secret = 'JBSWY3DPEHPK3PXP'; // Test secret
|
|
const code = Totp.generate({ secret, time: Date.now() });
|
|
|
|
const isValid = totpService.validateTOTP(code);
|
|
expect(isValid).toBe(true);
|
|
});
|
|
|
|
test('should reject expired TOTP codes', () => {
|
|
const secret = 'JBSWY3DPEHPK3PXP';
|
|
const oldTime = Date.now() - (2 * 30 * 1000); // 2 time steps ago
|
|
const oldCode = Totp.generate({ secret, time: oldTime });
|
|
|
|
const isValid = totpService.validateTOTP(oldCode);
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
test('should prevent timing attacks', () => {
|
|
const validSecret = totpService.createSecret().message;
|
|
const validCode = Totp.generate({ secret: validSecret });
|
|
const invalidCode = '000000';
|
|
|
|
// Measure timing for valid vs invalid codes
|
|
const times = [];
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const start = process.hrtime.bigint();
|
|
totpService.validateTOTP(i % 2 === 0 ? validCode : invalidCode);
|
|
const end = process.hrtime.bigint();
|
|
times.push(Number(end - start));
|
|
}
|
|
|
|
const validTimes = times.filter((_, i) => i % 2 === 0);
|
|
const invalidTimes = times.filter((_, i) => i % 2 === 1);
|
|
|
|
const avgValidTime = validTimes.reduce((a, b) => a + b) / validTimes.length;
|
|
const avgInvalidTime = invalidTimes.reduce((a, b) => a + b) / invalidTimes.length;
|
|
|
|
// Timing difference should be minimal
|
|
const timingDiff = Math.abs(avgValidTime - avgInvalidTime);
|
|
expect(timingDiff).toBeLessThan(avgValidTime * 0.1); // Less than 10% difference
|
|
});
|
|
});
|
|
```
|
|
|
|
## Penetration Testing
|
|
|
|
### Automated Penetration Testing
|
|
|
|
#### Nuclei Security Scanner
|
|
|
|
```yaml
|
|
# .nuclei/config.yaml
|
|
projectfile: .nuclei/project.yaml
|
|
templatesDirectory: /nuclei-templates
|
|
|
|
# .nuclei/project.yaml
|
|
name: "trilium-security-scan"
|
|
authors: ["security-team"]
|
|
tags: ["web", "api", "auth"]
|
|
```
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# penetration-test.sh
|
|
|
|
echo "Starting penetration testing..."
|
|
|
|
# Install nuclei if not present
|
|
if ! command -v nuclei &> /dev/null; then
|
|
echo "Installing nuclei..."
|
|
go install -v github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest
|
|
fi
|
|
|
|
# Update templates
|
|
nuclei -update-templates
|
|
|
|
# Run security scans
|
|
echo "Running nuclei security scan..."
|
|
nuclei -u http://localhost:8080 \
|
|
-t /nuclei-templates/cves/ \
|
|
-t /nuclei-templates/vulnerabilities/ \
|
|
-t /nuclei-templates/security-misconfiguration/ \
|
|
-o nuclei-report.txt
|
|
|
|
# Custom Trilium-specific tests
|
|
echo "Running custom security tests..."
|
|
nuclei -u http://localhost:8080 \
|
|
-t .nuclei/trilium-tests/ \
|
|
-o custom-security-report.txt
|
|
|
|
echo "Penetration testing completed."
|
|
```
|
|
|
|
#### Custom Nuclei Templates
|
|
|
|
```yaml
|
|
# .nuclei/trilium-tests/trilium-auth-bypass.yaml
|
|
id: trilium-auth-bypass
|
|
|
|
info:
|
|
name: Trilium Authentication Bypass
|
|
author: security-team
|
|
severity: critical
|
|
description: Test for authentication bypass vulnerabilities
|
|
tags: trilium,auth
|
|
|
|
requests:
|
|
- method: GET
|
|
path:
|
|
- "{{BaseURL}}/api/notes"
|
|
- "{{BaseURL}}/api/options"
|
|
- "{{BaseURL}}/api/search"
|
|
|
|
matchers-condition: and
|
|
matchers:
|
|
- type: status
|
|
status:
|
|
- 200
|
|
|
|
- type: word
|
|
words:
|
|
- "noteId"
|
|
- "notes"
|
|
condition: or
|
|
```
|
|
|
|
### Manual Security Testing
|
|
|
|
#### Security Test Scenarios
|
|
|
|
```typescript
|
|
// Manual security testing checklist
|
|
interface SecurityTestScenario {
|
|
category: string;
|
|
description: string;
|
|
steps: string[];
|
|
expectedResult: string;
|
|
risk: 'low' | 'medium' | 'high' | 'critical';
|
|
}
|
|
|
|
const securityTestScenarios: SecurityTestScenario[] = [
|
|
{
|
|
category: 'Authentication',
|
|
description: 'Test password brute force protection',
|
|
steps: [
|
|
'Navigate to login page',
|
|
'Attempt 10 failed login attempts',
|
|
'Verify account lockout occurs',
|
|
'Wait for lockout period to expire',
|
|
'Verify legitimate login works after lockout'
|
|
],
|
|
expectedResult: 'Account should be locked after failed attempts',
|
|
risk: 'high'
|
|
},
|
|
{
|
|
category: 'Session Management',
|
|
description: 'Test session hijacking resistance',
|
|
steps: [
|
|
'Login and capture session cookie',
|
|
'Attempt to use session from different IP',
|
|
'Verify session validation',
|
|
'Test session timeout functionality'
|
|
],
|
|
expectedResult: 'Session should be invalidated or require additional verification',
|
|
risk: 'high'
|
|
},
|
|
{
|
|
category: 'Input Validation',
|
|
description: 'Test for XSS vulnerabilities',
|
|
steps: [
|
|
'Create note with script payload: <script>alert("XSS")</script>',
|
|
'Save and view the note',
|
|
'Check if script executes',
|
|
'Verify content is properly sanitized'
|
|
],
|
|
expectedResult: 'Script should not execute, content should be sanitized',
|
|
risk: 'medium'
|
|
}
|
|
];
|
|
```
|
|
|
|
#### Security Checklist
|
|
|
|
```typescript
|
|
interface SecurityChecklist {
|
|
item: string;
|
|
status: 'pass' | 'fail' | 'not_applicable';
|
|
notes?: string;
|
|
}
|
|
|
|
const securityChecklist: SecurityChecklist[] = [
|
|
{
|
|
item: 'HTTPS enforced in production',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Security headers properly configured',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Input validation on all endpoints',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'SQL injection protection implemented',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'XSS protection mechanisms active',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'CSRF protection enabled',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Strong password policy enforced',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'MFA available and working',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Session management secure',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Rate limiting implemented',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Error messages dont leak information',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'File upload restrictions in place',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Sensitive data encrypted at rest',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Audit logging comprehensive',
|
|
status: 'pass'
|
|
},
|
|
{
|
|
item: 'Dependencies up to date',
|
|
status: 'pass'
|
|
}
|
|
];
|
|
```
|
|
|
|
## Security Test Automation
|
|
|
|
### CI/CD Security Pipeline
|
|
|
|
```yaml
|
|
# .github/workflows/security-pipeline.yml
|
|
name: Security Pipeline
|
|
|
|
on:
|
|
push:
|
|
branches: [ main, develop ]
|
|
pull_request:
|
|
branches: [ main ]
|
|
|
|
jobs:
|
|
static-analysis:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run ESLint security rules
|
|
run: npm run lint:security
|
|
|
|
- name: Run dependency vulnerability scan
|
|
run: npm audit --audit-level moderate
|
|
|
|
- name: Run Snyk scan
|
|
uses: snyk/actions/node@master
|
|
env:
|
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
with:
|
|
args: --severity-threshold=high
|
|
|
|
unit-security-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run security unit tests
|
|
run: npm run test:security
|
|
|
|
- name: Generate coverage report
|
|
run: npm run coverage:security
|
|
|
|
integration-security-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Start test database
|
|
run: npm run db:test
|
|
|
|
- name: Run integration security tests
|
|
run: npm run test:security:integration
|
|
|
|
- name: Cleanup
|
|
run: npm run db:cleanup
|
|
|
|
dynamic-security-scan:
|
|
runs-on: ubuntu-latest
|
|
needs: [static-analysis, unit-security-tests]
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Start application
|
|
run: |
|
|
npm run build
|
|
npm start &
|
|
sleep 30
|
|
|
|
- name: Run OWASP ZAP scan
|
|
uses: zaproxy/action-full-scan@v0.4.0
|
|
with:
|
|
target: 'http://localhost:8080'
|
|
|
|
- name: Upload security reports
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: security-reports
|
|
path: |
|
|
report_html.html
|
|
report_json.json
|
|
```
|
|
|
|
### Security Metrics and Reporting
|
|
|
|
```typescript
|
|
// scripts/security-metrics.ts
|
|
interface SecurityMetrics {
|
|
vulnerabilities: {
|
|
critical: number;
|
|
high: number;
|
|
medium: number;
|
|
low: number;
|
|
};
|
|
testCoverage: {
|
|
securityTests: number;
|
|
totalTests: number;
|
|
percentage: number;
|
|
};
|
|
dependencies: {
|
|
total: number;
|
|
outdated: number;
|
|
vulnerable: number;
|
|
};
|
|
scanResults: {
|
|
lastScan: Date;
|
|
passed: boolean;
|
|
findings: number;
|
|
};
|
|
}
|
|
|
|
function generateSecurityReport(metrics: SecurityMetrics): void {
|
|
const report = `
|
|
# Security Report - ${new Date().toISOString()}
|
|
|
|
## Vulnerability Summary
|
|
- Critical: ${metrics.vulnerabilities.critical}
|
|
- High: ${metrics.vulnerabilities.high}
|
|
- Medium: ${metrics.vulnerabilities.medium}
|
|
- Low: ${metrics.vulnerabilities.low}
|
|
|
|
## Test Coverage
|
|
- Security Tests: ${metrics.testCoverage.securityTests}
|
|
- Total Tests: ${metrics.testCoverage.totalTests}
|
|
- Coverage: ${metrics.testCoverage.percentage}%
|
|
|
|
## Dependencies
|
|
- Total Dependencies: ${metrics.dependencies.total}
|
|
- Outdated: ${metrics.dependencies.outdated}
|
|
- Vulnerable: ${metrics.dependencies.vulnerable}
|
|
|
|
## Last Security Scan
|
|
- Date: ${metrics.scanResults.lastScan}
|
|
- Status: ${metrics.scanResults.passed ? 'PASSED' : 'FAILED'}
|
|
- Findings: ${metrics.scanResults.findings}
|
|
`;
|
|
|
|
console.log(report);
|
|
|
|
// Fail build if critical issues found
|
|
if (metrics.vulnerabilities.critical > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
```
|
|
|
|
This comprehensive security testing guide ensures that Trilium maintains a robust security posture through automated and manual testing procedures. Regular execution of these tests helps identify and remediate security issues before they can be exploited. |