tlc-claude-code 1.4.8 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIS Docker Benchmark Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
checkDockerfileCompliance,
|
|
7
|
+
checkComposeCompliance,
|
|
8
|
+
checkRuntimeCompliance,
|
|
9
|
+
generateComplianceReport,
|
|
10
|
+
createCisBenchmark,
|
|
11
|
+
CIS_CHECKS,
|
|
12
|
+
} from './cis-benchmark.js';
|
|
13
|
+
|
|
14
|
+
describe('cis-benchmark', () => {
|
|
15
|
+
describe('checkDockerfileCompliance', () => {
|
|
16
|
+
it('checks for USER directive (CIS 4.1)', () => {
|
|
17
|
+
const dockerfile = 'FROM node:20\nCMD ["node", "app.js"]';
|
|
18
|
+
const result = checkDockerfileCompliance(dockerfile);
|
|
19
|
+
expect(result.findings.some(f => f.cis === '4.1')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('passes with USER directive', () => {
|
|
23
|
+
const dockerfile = 'FROM node:20\nUSER node\nCMD ["node", "app.js"]';
|
|
24
|
+
const result = checkDockerfileCompliance(dockerfile);
|
|
25
|
+
expect(result.findings.some(f => f.cis === '4.1')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('checks for HEALTHCHECK (CIS 4.6)', () => {
|
|
29
|
+
const dockerfile = 'FROM node:20\nUSER node';
|
|
30
|
+
const result = checkDockerfileCompliance(dockerfile);
|
|
31
|
+
expect(result.findings.some(f => f.cis === '4.6')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('checks for content trust labels', () => {
|
|
35
|
+
const dockerfile = 'FROM node:20\nUSER node';
|
|
36
|
+
const result = checkDockerfileCompliance(dockerfile);
|
|
37
|
+
expect(result.findings.some(f => f.cis === '4.8')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('checkComposeCompliance', () => {
|
|
42
|
+
it('checks privileged mode (CIS 5.4)', () => {
|
|
43
|
+
const compose = { services: { app: { privileged: true } } };
|
|
44
|
+
const result = checkComposeCompliance(compose);
|
|
45
|
+
expect(result.findings.some(f => f.cis === '5.4')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('checks capabilities (CIS 5.3)', () => {
|
|
49
|
+
const compose = { services: { app: { image: 'node:20' } } };
|
|
50
|
+
const result = checkComposeCompliance(compose);
|
|
51
|
+
expect(result.findings.some(f => f.cis === '5.3')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('checks resource limits (CIS 5.10)', () => {
|
|
55
|
+
const compose = { services: { app: { image: 'node:20', cap_drop: ['ALL'] } } };
|
|
56
|
+
const result = checkComposeCompliance(compose);
|
|
57
|
+
expect(result.findings.some(f => f.cis === '5.10')).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('passes with proper security config', () => {
|
|
61
|
+
const compose = {
|
|
62
|
+
services: {
|
|
63
|
+
app: {
|
|
64
|
+
image: 'node:20',
|
|
65
|
+
cap_drop: ['ALL'],
|
|
66
|
+
read_only: true,
|
|
67
|
+
user: '1000:1000',
|
|
68
|
+
security_opt: ['no-new-privileges:true'],
|
|
69
|
+
deploy: { resources: { limits: { memory: '512M' } } },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const result = checkComposeCompliance(compose);
|
|
74
|
+
expect(result.findings.filter(f => f.severity === 'high').length).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('checkRuntimeCompliance', () => {
|
|
79
|
+
it('checks for PID limits (CIS 5.11)', () => {
|
|
80
|
+
const compose = { services: { app: { image: 'node:20' } } };
|
|
81
|
+
const result = checkRuntimeCompliance(compose);
|
|
82
|
+
expect(result.findings.some(f => f.cis === '5.11')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('checks network mode (CIS 5.13)', () => {
|
|
86
|
+
const compose = { services: { app: { network_mode: 'host' } } };
|
|
87
|
+
const result = checkRuntimeCompliance(compose);
|
|
88
|
+
expect(result.findings.some(f => f.cis === '5.13')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('checks for restart policy (CIS 5.14)', () => {
|
|
92
|
+
const compose = { services: { app: { restart: 'always' } } };
|
|
93
|
+
const result = checkRuntimeCompliance(compose);
|
|
94
|
+
expect(result.findings.some(f => f.cis === '5.14')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('generateComplianceReport', () => {
|
|
99
|
+
it('generates Level 1 compliance report', () => {
|
|
100
|
+
const dockerfile = 'FROM node:20\nUSER node\nHEALTHCHECK CMD curl -f http://localhost/';
|
|
101
|
+
const compose = {
|
|
102
|
+
services: { app: { cap_drop: ['ALL'], user: '1000', deploy: { resources: { limits: { memory: '512M' } } } } },
|
|
103
|
+
};
|
|
104
|
+
const report = generateComplianceReport({ dockerfile, compose });
|
|
105
|
+
expect(report.level1Score).toBeDefined();
|
|
106
|
+
expect(report.level1Score).toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('categorizes findings by CIS section', () => {
|
|
110
|
+
const dockerfile = 'FROM node:20';
|
|
111
|
+
const compose = { services: { app: { privileged: true } } };
|
|
112
|
+
const report = generateComplianceReport({ dockerfile, compose });
|
|
113
|
+
expect(report.bySection).toBeDefined();
|
|
114
|
+
expect(report.bySection['4']).toBeDefined();
|
|
115
|
+
expect(report.bySection['5']).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('createCisBenchmark', () => {
|
|
120
|
+
it('creates benchmark checker', () => {
|
|
121
|
+
const checker = createCisBenchmark();
|
|
122
|
+
expect(checker.checkDockerfile).toBeDefined();
|
|
123
|
+
expect(checker.checkCompose).toBeDefined();
|
|
124
|
+
expect(checker.generateReport).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('calculates overall score', () => {
|
|
128
|
+
const checker = createCisBenchmark();
|
|
129
|
+
const dockerfile = 'FROM node:20\nUSER node\nHEALTHCHECK CMD true';
|
|
130
|
+
const compose = { services: { app: { cap_drop: ['ALL'] } } };
|
|
131
|
+
const result = checker.audit({ dockerfile, compose });
|
|
132
|
+
expect(result.score).toBeDefined();
|
|
133
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
134
|
+
expect(result.score).toBeLessThanOrEqual(100);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardened Docker Compose Templates
|
|
3
|
+
*
|
|
4
|
+
* CIS Docker Benchmark compliant compose configurations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default security settings for all services
|
|
9
|
+
*/
|
|
10
|
+
export const SECURITY_DEFAULTS = {
|
|
11
|
+
cap_drop: ['ALL'],
|
|
12
|
+
security_opt: ['no-new-privileges:true'],
|
|
13
|
+
read_only: true,
|
|
14
|
+
restart: 'on-failure:5',
|
|
15
|
+
deploy: {
|
|
16
|
+
resources: {
|
|
17
|
+
limits: {
|
|
18
|
+
memory: '512M',
|
|
19
|
+
cpus: '0.5',
|
|
20
|
+
},
|
|
21
|
+
reservations: {
|
|
22
|
+
memory: '128M',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate production-ready docker-compose configuration
|
|
30
|
+
*/
|
|
31
|
+
export function generateProductionCompose(options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
serverImage = 'tlc-server:latest',
|
|
34
|
+
dashboardImage = 'tlc-dashboard:latest',
|
|
35
|
+
includeDb = false,
|
|
36
|
+
includeRedis = false,
|
|
37
|
+
domain = 'localhost',
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
const compose = {
|
|
41
|
+
version: '3.8',
|
|
42
|
+
services: {
|
|
43
|
+
server: {
|
|
44
|
+
image: serverImage,
|
|
45
|
+
...SECURITY_DEFAULTS,
|
|
46
|
+
cap_add: ['NET_BIND_SERVICE'],
|
|
47
|
+
environment: {
|
|
48
|
+
NODE_ENV: 'production',
|
|
49
|
+
PORT: '5001',
|
|
50
|
+
},
|
|
51
|
+
networks: ['frontend', 'backend'],
|
|
52
|
+
healthcheck: {
|
|
53
|
+
test: ['CMD', 'node', '-e', "require('http').get('http://localhost:5001/health')"],
|
|
54
|
+
interval: '30s',
|
|
55
|
+
timeout: '10s',
|
|
56
|
+
retries: 3,
|
|
57
|
+
},
|
|
58
|
+
tmpfs: ['/tmp'],
|
|
59
|
+
},
|
|
60
|
+
dashboard: {
|
|
61
|
+
image: dashboardImage,
|
|
62
|
+
...SECURITY_DEFAULTS,
|
|
63
|
+
cap_add: ['CHOWN', 'SETGID', 'SETUID'],
|
|
64
|
+
ports: ['80:80', '443:443'],
|
|
65
|
+
networks: ['frontend'],
|
|
66
|
+
depends_on: ['server'],
|
|
67
|
+
healthcheck: {
|
|
68
|
+
test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:80/'],
|
|
69
|
+
interval: '30s',
|
|
70
|
+
timeout: '10s',
|
|
71
|
+
retries: 3,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
networks: {
|
|
76
|
+
frontend: {
|
|
77
|
+
driver: 'bridge',
|
|
78
|
+
},
|
|
79
|
+
backend: {
|
|
80
|
+
driver: 'bridge',
|
|
81
|
+
internal: true,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (includeDb) {
|
|
87
|
+
compose.services.postgres = {
|
|
88
|
+
image: 'postgres:16-alpine',
|
|
89
|
+
cap_drop: ['ALL'],
|
|
90
|
+
cap_add: ['CHOWN', 'SETGID', 'SETUID', 'DAC_OVERRIDE', 'FOWNER'],
|
|
91
|
+
security_opt: ['no-new-privileges:true'],
|
|
92
|
+
read_only: false, // Database needs write access
|
|
93
|
+
restart: 'on-failure:5',
|
|
94
|
+
environment: {
|
|
95
|
+
POSTGRES_DB: '${POSTGRES_DB}',
|
|
96
|
+
POSTGRES_USER: '${POSTGRES_USER}',
|
|
97
|
+
POSTGRES_PASSWORD_FILE: '/run/secrets/db_password',
|
|
98
|
+
},
|
|
99
|
+
volumes: ['postgres_data:/var/lib/postgresql/data'],
|
|
100
|
+
networks: ['internal'],
|
|
101
|
+
deploy: {
|
|
102
|
+
resources: {
|
|
103
|
+
limits: {
|
|
104
|
+
memory: '1G',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
healthcheck: {
|
|
109
|
+
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}'],
|
|
110
|
+
interval: '10s',
|
|
111
|
+
timeout: '5s',
|
|
112
|
+
retries: 5,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
compose.networks.internal = {
|
|
116
|
+
driver: 'bridge',
|
|
117
|
+
internal: true,
|
|
118
|
+
};
|
|
119
|
+
compose.volumes = { postgres_data: {} };
|
|
120
|
+
compose.services.server.networks.push('internal');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (includeRedis) {
|
|
124
|
+
compose.services.redis = {
|
|
125
|
+
image: 'redis:7-alpine',
|
|
126
|
+
cap_drop: ['ALL'],
|
|
127
|
+
security_opt: ['no-new-privileges:true'],
|
|
128
|
+
read_only: true,
|
|
129
|
+
restart: 'on-failure:5',
|
|
130
|
+
command: ['redis-server', '--appendonly', 'yes', '--maxmemory', '256mb'],
|
|
131
|
+
volumes: ['redis_data:/data'],
|
|
132
|
+
networks: ['internal'],
|
|
133
|
+
deploy: {
|
|
134
|
+
resources: {
|
|
135
|
+
limits: {
|
|
136
|
+
memory: '512M',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
if (!compose.networks.internal) {
|
|
142
|
+
compose.networks.internal = { driver: 'bridge', internal: true };
|
|
143
|
+
}
|
|
144
|
+
if (!compose.volumes) compose.volumes = {};
|
|
145
|
+
compose.volumes.redis_data = {};
|
|
146
|
+
if (!compose.services.server.networks.includes('internal')) {
|
|
147
|
+
compose.services.server.networks.push('internal');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return compose;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate security overlay for additional hardening
|
|
156
|
+
*/
|
|
157
|
+
export function generateSecurityCompose(options = {}) {
|
|
158
|
+
const { apparmor = false, seccompProfile = 'default' } = options;
|
|
159
|
+
|
|
160
|
+
const securityOpt = ['no-new-privileges:true', `seccomp:${seccompProfile}`];
|
|
161
|
+
if (apparmor) {
|
|
162
|
+
securityOpt.push('apparmor:docker-default');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
version: '3.8',
|
|
167
|
+
services: {
|
|
168
|
+
server: {
|
|
169
|
+
security_opt: securityOpt,
|
|
170
|
+
pids_limit: 100,
|
|
171
|
+
ulimits: {
|
|
172
|
+
nofile: {
|
|
173
|
+
soft: 65535,
|
|
174
|
+
hard: 65535,
|
|
175
|
+
},
|
|
176
|
+
nproc: {
|
|
177
|
+
soft: 1024,
|
|
178
|
+
hard: 2048,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
privileged: false,
|
|
182
|
+
},
|
|
183
|
+
dashboard: {
|
|
184
|
+
security_opt: securityOpt,
|
|
185
|
+
pids_limit: 50,
|
|
186
|
+
ulimits: {
|
|
187
|
+
nofile: {
|
|
188
|
+
soft: 32768,
|
|
189
|
+
hard: 32768,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
privileged: false,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generate development docker-compose configuration
|
|
200
|
+
*/
|
|
201
|
+
export function generateDevCompose(options = {}) {
|
|
202
|
+
const { serverPort = 5001, dashboardPort = 3000 } = options;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
version: '3.8',
|
|
206
|
+
services: {
|
|
207
|
+
server: {
|
|
208
|
+
build: {
|
|
209
|
+
context: './server',
|
|
210
|
+
dockerfile: 'Dockerfile.dev',
|
|
211
|
+
},
|
|
212
|
+
cap_drop: ['ALL'],
|
|
213
|
+
cap_add: ['NET_BIND_SERVICE'],
|
|
214
|
+
security_opt: ['no-new-privileges:true'],
|
|
215
|
+
ports: [`${serverPort}:5001`],
|
|
216
|
+
volumes: ['./server:/app:cached', '/app/node_modules'],
|
|
217
|
+
environment: {
|
|
218
|
+
NODE_ENV: 'development',
|
|
219
|
+
DEBUG: '*',
|
|
220
|
+
},
|
|
221
|
+
networks: ['dev'],
|
|
222
|
+
},
|
|
223
|
+
dashboard: {
|
|
224
|
+
build: {
|
|
225
|
+
context: './dashboard-web',
|
|
226
|
+
dockerfile: 'Dockerfile.dev',
|
|
227
|
+
},
|
|
228
|
+
cap_drop: ['ALL'],
|
|
229
|
+
security_opt: ['no-new-privileges:true'],
|
|
230
|
+
ports: [`${dashboardPort}:3000`],
|
|
231
|
+
volumes: ['./dashboard-web:/app:cached', '/app/node_modules'],
|
|
232
|
+
environment: {
|
|
233
|
+
NODE_ENV: 'development',
|
|
234
|
+
VITE_API_URL: `http://localhost:${serverPort}`,
|
|
235
|
+
},
|
|
236
|
+
networks: ['dev'],
|
|
237
|
+
depends_on: ['server'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
networks: {
|
|
241
|
+
dev: {
|
|
242
|
+
driver: 'bridge',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Deep merge compose files (overlay pattern)
|
|
250
|
+
*/
|
|
251
|
+
export function mergeComposeFiles(base, overlay) {
|
|
252
|
+
const result = JSON.parse(JSON.stringify(base));
|
|
253
|
+
|
|
254
|
+
// Merge services
|
|
255
|
+
if (overlay.services) {
|
|
256
|
+
for (const [name, svc] of Object.entries(overlay.services)) {
|
|
257
|
+
if (result.services[name]) {
|
|
258
|
+
result.services[name] = mergeService(result.services[name], svc);
|
|
259
|
+
} else {
|
|
260
|
+
result.services[name] = svc;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Merge networks
|
|
266
|
+
if (overlay.networks) {
|
|
267
|
+
result.networks = { ...result.networks, ...overlay.networks };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Merge volumes
|
|
271
|
+
if (overlay.volumes) {
|
|
272
|
+
result.volumes = { ...result.volumes, ...overlay.volumes };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Merge secrets
|
|
276
|
+
if (overlay.secrets) {
|
|
277
|
+
result.secrets = { ...result.secrets, ...overlay.secrets };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Merge a single service configuration
|
|
285
|
+
*/
|
|
286
|
+
function mergeService(base, overlay) {
|
|
287
|
+
const result = { ...base };
|
|
288
|
+
|
|
289
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
// Arrays from overlay replace base arrays
|
|
292
|
+
result[key] = value;
|
|
293
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
294
|
+
// Objects are deep merged
|
|
295
|
+
result[key] = mergeService(result[key] || {}, value);
|
|
296
|
+
} else {
|
|
297
|
+
// Primitives are replaced
|
|
298
|
+
result[key] = value;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generate complete production stack
|
|
307
|
+
*/
|
|
308
|
+
export function generateFullStack(options = {}) {
|
|
309
|
+
const base = generateProductionCompose(options);
|
|
310
|
+
const security = generateSecurityCompose(options);
|
|
311
|
+
return mergeComposeFiles(base, security);
|
|
312
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardened Docker Compose Templates Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
generateProductionCompose,
|
|
7
|
+
generateSecurityCompose,
|
|
8
|
+
generateDevCompose,
|
|
9
|
+
mergeComposeFiles,
|
|
10
|
+
SECURITY_DEFAULTS,
|
|
11
|
+
} from './compose-templates.js';
|
|
12
|
+
import { checkComposeCompliance, checkRuntimeCompliance } from './cis-benchmark.js';
|
|
13
|
+
describe('compose-templates', () => {
|
|
14
|
+
describe('generateProductionCompose', () => {
|
|
15
|
+
it('generates valid compose structure', () => {
|
|
16
|
+
const compose = generateProductionCompose();
|
|
17
|
+
expect(compose.version).toBeDefined();
|
|
18
|
+
expect(compose.services).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('includes server service', () => {
|
|
22
|
+
const compose = generateProductionCompose();
|
|
23
|
+
expect(compose.services.server).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('includes dashboard service', () => {
|
|
27
|
+
const compose = generateProductionCompose();
|
|
28
|
+
expect(compose.services.dashboard).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('sets resource limits on all services', () => {
|
|
32
|
+
const compose = generateProductionCompose();
|
|
33
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
34
|
+
const hasLimits = svc.deploy?.resources?.limits || svc.mem_limit;
|
|
35
|
+
expect(hasLimits, `Service ${name} should have memory limits`).toBeTruthy();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('uses read_only filesystem where applicable', () => {
|
|
40
|
+
const compose = generateProductionCompose();
|
|
41
|
+
// Non-database services should be read-only
|
|
42
|
+
expect(compose.services.server.read_only).toBe(true);
|
|
43
|
+
expect(compose.services.dashboard.read_only).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('drops ALL capabilities', () => {
|
|
47
|
+
const compose = generateProductionCompose();
|
|
48
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
49
|
+
expect(svc.cap_drop, `Service ${name} should drop ALL`).toContain('ALL');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('sets no-new-privileges', () => {
|
|
54
|
+
const compose = generateProductionCompose();
|
|
55
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
56
|
+
const hasNoNewPriv = svc.security_opt?.some(o => o.includes('no-new-privileges'));
|
|
57
|
+
expect(hasNoNewPriv, `Service ${name} should set no-new-privileges`).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('uses internal network for database', () => {
|
|
62
|
+
const compose = generateProductionCompose({ includeDb: true });
|
|
63
|
+
if (compose.services.postgres) {
|
|
64
|
+
const networks = compose.services.postgres.networks || [];
|
|
65
|
+
expect(networks).toContain('internal');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not expose database ports to host', () => {
|
|
70
|
+
const compose = generateProductionCompose({ includeDb: true });
|
|
71
|
+
if (compose.services.postgres) {
|
|
72
|
+
expect(compose.services.postgres.ports).toBeUndefined();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('passes CIS compliance checks', () => {
|
|
77
|
+
const compose = generateProductionCompose();
|
|
78
|
+
const result = checkComposeCompliance(compose);
|
|
79
|
+
const criticalFindings = result.findings.filter(f => f.severity === 'critical');
|
|
80
|
+
expect(criticalFindings.length).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('passes runtime compliance checks', () => {
|
|
84
|
+
const compose = generateProductionCompose();
|
|
85
|
+
const result = checkRuntimeCompliance(compose);
|
|
86
|
+
const highFindings = result.findings.filter(f => f.severity === 'high');
|
|
87
|
+
expect(highFindings.length).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('generateSecurityCompose', () => {
|
|
92
|
+
it('generates overlay for security hardening', () => {
|
|
93
|
+
const compose = generateSecurityCompose();
|
|
94
|
+
expect(compose.version).toBeDefined();
|
|
95
|
+
expect(compose.services).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('adds seccomp profiles', () => {
|
|
99
|
+
const compose = generateSecurityCompose();
|
|
100
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
101
|
+
const hasSeccomp = svc.security_opt?.some(o => o.includes('seccomp'));
|
|
102
|
+
expect(hasSeccomp, `Service ${name} should have seccomp`).toBe(true);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('adds AppArmor profiles when specified', () => {
|
|
107
|
+
const compose = generateSecurityCompose({ apparmor: true });
|
|
108
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
109
|
+
const hasApparmor = svc.security_opt?.some(o => o.includes('apparmor'));
|
|
110
|
+
expect(hasApparmor, `Service ${name} should have apparmor`).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sets PID limits', () => {
|
|
115
|
+
const compose = generateSecurityCompose();
|
|
116
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
117
|
+
expect(svc.pids_limit, `Service ${name} should have pids_limit`).toBeDefined();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('sets ulimits', () => {
|
|
122
|
+
const compose = generateSecurityCompose();
|
|
123
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
124
|
+
expect(svc.ulimits, `Service ${name} should have ulimits`).toBeDefined();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('disables privileged mode', () => {
|
|
129
|
+
const compose = generateSecurityCompose();
|
|
130
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
131
|
+
expect(svc.privileged, `Service ${name} should not be privileged`).toBeFalsy();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('generateDevCompose', () => {
|
|
137
|
+
it('generates development configuration', () => {
|
|
138
|
+
const compose = generateDevCompose();
|
|
139
|
+
expect(compose.services).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('mounts source code volumes', () => {
|
|
143
|
+
const compose = generateDevCompose();
|
|
144
|
+
expect(compose.services.server.volumes).toBeDefined();
|
|
145
|
+
expect(compose.services.server.volumes.some(v => v.includes(':'))).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('exposes ports for development', () => {
|
|
149
|
+
const compose = generateDevCompose();
|
|
150
|
+
expect(compose.services.server.ports).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('still maintains basic security', () => {
|
|
154
|
+
const compose = generateDevCompose();
|
|
155
|
+
// Even dev should drop ALL capabilities
|
|
156
|
+
for (const [name, svc] of Object.entries(compose.services)) {
|
|
157
|
+
if (svc.cap_drop) {
|
|
158
|
+
expect(svc.cap_drop).toContain('ALL');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('mergeComposeFiles', () => {
|
|
165
|
+
it('merges base and overlay', () => {
|
|
166
|
+
const base = {
|
|
167
|
+
version: '3.8',
|
|
168
|
+
services: { app: { image: 'node:20' } },
|
|
169
|
+
};
|
|
170
|
+
const overlay = {
|
|
171
|
+
services: { app: { read_only: true } },
|
|
172
|
+
};
|
|
173
|
+
const merged = mergeComposeFiles(base, overlay);
|
|
174
|
+
expect(merged.services.app.image).toBe('node:20');
|
|
175
|
+
expect(merged.services.app.read_only).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('overlay arrays replace base arrays', () => {
|
|
179
|
+
const base = {
|
|
180
|
+
services: { app: { cap_drop: ['NET_RAW'] } },
|
|
181
|
+
};
|
|
182
|
+
const overlay = {
|
|
183
|
+
services: { app: { cap_drop: ['ALL'] } },
|
|
184
|
+
};
|
|
185
|
+
const merged = mergeComposeFiles(base, overlay);
|
|
186
|
+
expect(merged.services.app.cap_drop).toEqual(['ALL']);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('merges networks', () => {
|
|
190
|
+
const base = {
|
|
191
|
+
networks: { frontend: {} },
|
|
192
|
+
};
|
|
193
|
+
const overlay = {
|
|
194
|
+
networks: { backend: { internal: true } },
|
|
195
|
+
};
|
|
196
|
+
const merged = mergeComposeFiles(base, overlay);
|
|
197
|
+
expect(merged.networks.frontend).toBeDefined();
|
|
198
|
+
expect(merged.networks.backend).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('SECURITY_DEFAULTS', () => {
|
|
203
|
+
it('defines default security settings', () => {
|
|
204
|
+
expect(SECURITY_DEFAULTS.cap_drop).toContain('ALL');
|
|
205
|
+
expect(SECURITY_DEFAULTS.security_opt).toBeDefined();
|
|
206
|
+
expect(SECURITY_DEFAULTS.read_only).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('includes memory limits', () => {
|
|
210
|
+
expect(SECURITY_DEFAULTS.deploy.resources.limits.memory).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('includes restart policy', () => {
|
|
214
|
+
expect(SECURITY_DEFAULTS.restart).toBe('on-failure:5');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('integration with validators', () => {
|
|
219
|
+
it('production compose passes all CIS checks', () => {
|
|
220
|
+
const compose = generateProductionCompose();
|
|
221
|
+
const composeResult = checkComposeCompliance(compose);
|
|
222
|
+
const runtimeResult = checkRuntimeCompliance(compose);
|
|
223
|
+
|
|
224
|
+
const allFindings = [...composeResult.findings, ...runtimeResult.findings];
|
|
225
|
+
const criticalFindings = allFindings.filter(f => f.severity === 'critical');
|
|
226
|
+
expect(criticalFindings.length).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|