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,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container Runtime Security Validator Module
|
|
3
|
+
*
|
|
4
|
+
* Validates docker-compose and container runtime configurations.
|
|
5
|
+
* Based on CIS Docker Benchmark and OWASP Docker Security guidelines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Severity levels for findings
|
|
12
|
+
*/
|
|
13
|
+
export const SEVERITY = {
|
|
14
|
+
CRITICAL: 'critical',
|
|
15
|
+
HIGH: 'high',
|
|
16
|
+
MEDIUM: 'medium',
|
|
17
|
+
LOW: 'low',
|
|
18
|
+
INFO: 'info',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dangerous Linux capabilities
|
|
23
|
+
*/
|
|
24
|
+
const DANGEROUS_CAPABILITIES = [
|
|
25
|
+
'SYS_ADMIN',
|
|
26
|
+
'NET_ADMIN',
|
|
27
|
+
'SYS_PTRACE',
|
|
28
|
+
'SYS_MODULE',
|
|
29
|
+
'DAC_READ_SEARCH',
|
|
30
|
+
'SYS_RAWIO',
|
|
31
|
+
'SYS_BOOT',
|
|
32
|
+
'SYS_TIME',
|
|
33
|
+
'MKNOD',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Safe capabilities that can be added
|
|
38
|
+
*/
|
|
39
|
+
const SAFE_CAPABILITIES = [
|
|
40
|
+
'NET_BIND_SERVICE',
|
|
41
|
+
'CHOWN',
|
|
42
|
+
'SETUID',
|
|
43
|
+
'SETGID',
|
|
44
|
+
'FOWNER',
|
|
45
|
+
'DAC_OVERRIDE',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Database image patterns
|
|
50
|
+
*/
|
|
51
|
+
const DATABASE_IMAGES = [
|
|
52
|
+
/postgres/i,
|
|
53
|
+
/mysql/i,
|
|
54
|
+
/mariadb/i,
|
|
55
|
+
/mongo/i,
|
|
56
|
+
/redis/i,
|
|
57
|
+
/elasticsearch/i,
|
|
58
|
+
/memcached/i,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Patterns for secrets in environment values
|
|
63
|
+
*/
|
|
64
|
+
const SECRET_PATTERNS = [
|
|
65
|
+
/password\s*[=:]\s*[^$\s{][^\s]*/i,
|
|
66
|
+
/secret\s*[=:]\s*[^$\s{][^\s]*/i,
|
|
67
|
+
/api[_-]?key\s*[=:]\s*[^$\s{][^\s]*/i,
|
|
68
|
+
/token\s*[=:]\s*[^$\s{][^\s]*/i,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a service is a database
|
|
73
|
+
* @param {string} name - Service name
|
|
74
|
+
* @param {Object} service - Service config
|
|
75
|
+
* @returns {boolean} True if database
|
|
76
|
+
*/
|
|
77
|
+
function isDatabase(name, service) {
|
|
78
|
+
if (DATABASE_IMAGES.some(p => p.test(service.image || ''))) return true;
|
|
79
|
+
if (/db|database|postgres|mysql|mongo|redis/i.test(name)) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse docker-compose YAML content
|
|
85
|
+
* @param {string} content - YAML content
|
|
86
|
+
* @returns {Object} Parsed compose config
|
|
87
|
+
*/
|
|
88
|
+
export function parseCompose(content) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = yaml.load(content);
|
|
91
|
+
return {
|
|
92
|
+
version: parsed.version || null,
|
|
93
|
+
services: parsed.services || {},
|
|
94
|
+
networks: parsed.networks || {},
|
|
95
|
+
volumes: parsed.volumes || {},
|
|
96
|
+
secrets: parsed.secrets || {},
|
|
97
|
+
};
|
|
98
|
+
} catch (e) {
|
|
99
|
+
throw new Error(`Failed to parse docker-compose: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate a single service configuration
|
|
105
|
+
* @param {string} name - Service name
|
|
106
|
+
* @param {Object} service - Service configuration
|
|
107
|
+
* @param {Object} options - Validation options
|
|
108
|
+
* @returns {Object} Validation result
|
|
109
|
+
*/
|
|
110
|
+
export function validateService(name, service, options = {}) {
|
|
111
|
+
const findings = [];
|
|
112
|
+
const isDb = isDatabase(name, service);
|
|
113
|
+
|
|
114
|
+
// Check privileged mode
|
|
115
|
+
if (service.privileged === true) {
|
|
116
|
+
findings.push({
|
|
117
|
+
rule: 'no-privileged',
|
|
118
|
+
severity: SEVERITY.CRITICAL,
|
|
119
|
+
service: name,
|
|
120
|
+
message: `Service '${name}' uses privileged mode. This grants full root access.`,
|
|
121
|
+
fix: 'Remove privileged: true. Use specific capabilities instead.',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check capabilities
|
|
126
|
+
const capDrop = service.cap_drop || [];
|
|
127
|
+
const capAdd = service.cap_add || [];
|
|
128
|
+
|
|
129
|
+
if (!capDrop.includes('ALL')) {
|
|
130
|
+
findings.push({
|
|
131
|
+
rule: 'require-cap-drop-all',
|
|
132
|
+
severity: SEVERITY.HIGH,
|
|
133
|
+
service: name,
|
|
134
|
+
message: `Service '${name}' should drop all capabilities and add only required ones.`,
|
|
135
|
+
fix: "Add 'cap_drop: [ALL]' to the service.",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check for dangerous capabilities
|
|
140
|
+
const dangerousCaps = capAdd.filter(cap => DANGEROUS_CAPABILITIES.includes(cap));
|
|
141
|
+
if (dangerousCaps.length > 0) {
|
|
142
|
+
findings.push({
|
|
143
|
+
rule: 'dangerous-capabilities',
|
|
144
|
+
severity: SEVERITY.HIGH,
|
|
145
|
+
service: name,
|
|
146
|
+
message: `Service '${name}' adds dangerous capabilities: ${dangerousCaps.join(', ')}`,
|
|
147
|
+
fix: 'Remove dangerous capabilities or document why they are required.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check network mode
|
|
152
|
+
if (service.network_mode === 'host') {
|
|
153
|
+
findings.push({
|
|
154
|
+
rule: 'no-host-network',
|
|
155
|
+
severity: SEVERITY.HIGH,
|
|
156
|
+
service: name,
|
|
157
|
+
message: `Service '${name}' uses host network mode. This bypasses network isolation.`,
|
|
158
|
+
fix: 'Use custom bridge networks instead of host network.',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check read-only filesystem
|
|
163
|
+
if (!isDb && service.read_only !== true) {
|
|
164
|
+
findings.push({
|
|
165
|
+
rule: 'recommend-read-only',
|
|
166
|
+
severity: SEVERITY.MEDIUM,
|
|
167
|
+
service: name,
|
|
168
|
+
message: `Service '${name}' should use read-only root filesystem.`,
|
|
169
|
+
fix: "Add 'read_only: true' and mount writable volumes for needed paths.",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check user
|
|
174
|
+
if (!service.user) {
|
|
175
|
+
findings.push({
|
|
176
|
+
rule: 'recommend-user',
|
|
177
|
+
severity: SEVERITY.MEDIUM,
|
|
178
|
+
service: name,
|
|
179
|
+
message: `Service '${name}' should specify a non-root user.`,
|
|
180
|
+
fix: "Add 'user: \"1000:1000\"' or similar non-root user.",
|
|
181
|
+
});
|
|
182
|
+
} else if (service.user === 'root' || service.user === '0' || service.user === '0:0') {
|
|
183
|
+
findings.push({
|
|
184
|
+
rule: 'no-root-user',
|
|
185
|
+
severity: SEVERITY.HIGH,
|
|
186
|
+
service: name,
|
|
187
|
+
message: `Service '${name}' runs as root user.`,
|
|
188
|
+
fix: 'Change to a non-root user.',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check security_opt
|
|
193
|
+
const securityOpt = service.security_opt || [];
|
|
194
|
+
const hasNoNewPrivileges = securityOpt.some(opt =>
|
|
195
|
+
opt.includes('no-new-privileges')
|
|
196
|
+
);
|
|
197
|
+
const hasSeccomp = securityOpt.some(opt =>
|
|
198
|
+
opt.includes('seccomp')
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (!hasNoNewPrivileges) {
|
|
202
|
+
findings.push({
|
|
203
|
+
rule: 'recommend-no-new-privileges',
|
|
204
|
+
severity: SEVERITY.MEDIUM,
|
|
205
|
+
service: name,
|
|
206
|
+
message: `Service '${name}' should prevent privilege escalation.`,
|
|
207
|
+
fix: "Add 'security_opt: [no-new-privileges:true]'.",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!hasSeccomp) {
|
|
212
|
+
findings.push({
|
|
213
|
+
rule: 'recommend-seccomp',
|
|
214
|
+
severity: SEVERITY.LOW,
|
|
215
|
+
service: name,
|
|
216
|
+
message: `Service '${name}' should use seccomp profile.`,
|
|
217
|
+
fix: "Add 'security_opt: [seccomp:default]' or custom profile.",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check resource limits
|
|
222
|
+
const hasDeployLimits = service.deploy?.resources?.limits;
|
|
223
|
+
const hasMemLimit = service.mem_limit || service.memory;
|
|
224
|
+
if (!hasDeployLimits && !hasMemLimit) {
|
|
225
|
+
findings.push({
|
|
226
|
+
rule: 'recommend-resource-limits',
|
|
227
|
+
severity: SEVERITY.MEDIUM,
|
|
228
|
+
service: name,
|
|
229
|
+
message: `Service '${name}' should have resource limits.`,
|
|
230
|
+
fix: 'Add deploy.resources.limits or mem_limit to prevent resource exhaustion.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { findings };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate a full docker-compose configuration
|
|
239
|
+
* @param {string} content - YAML content
|
|
240
|
+
* @param {Object} options - Validation options
|
|
241
|
+
* @returns {Object} Validation result
|
|
242
|
+
*/
|
|
243
|
+
export function validateCompose(content, options = {}) {
|
|
244
|
+
const parsed = parseCompose(content);
|
|
245
|
+
const findings = [];
|
|
246
|
+
const recommendations = [];
|
|
247
|
+
|
|
248
|
+
// Validate each service
|
|
249
|
+
for (const [name, service] of Object.entries(parsed.services)) {
|
|
250
|
+
const serviceResult = validateService(name, service, options);
|
|
251
|
+
findings.push(...serviceResult.findings);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check for custom networks
|
|
255
|
+
const hasCustomNetworks = Object.keys(parsed.networks).length > 0;
|
|
256
|
+
const servicesUseNetworks = Object.values(parsed.services).some(s => s.networks);
|
|
257
|
+
|
|
258
|
+
if (!hasCustomNetworks && !servicesUseNetworks) {
|
|
259
|
+
findings.push({
|
|
260
|
+
rule: 'use-custom-networks',
|
|
261
|
+
severity: SEVERITY.MEDIUM,
|
|
262
|
+
message: 'No custom networks defined. Services will use default bridge network.',
|
|
263
|
+
fix: 'Define custom networks for network segmentation.',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check database network isolation
|
|
268
|
+
for (const [name, service] of Object.entries(parsed.services)) {
|
|
269
|
+
if (isDatabase(name, service)) {
|
|
270
|
+
const serviceNetworks = service.networks || [];
|
|
271
|
+
let hasInternalNetwork = false;
|
|
272
|
+
|
|
273
|
+
for (const netName of serviceNetworks) {
|
|
274
|
+
const netConfig = parsed.networks[netName];
|
|
275
|
+
if (netConfig?.internal === true) {
|
|
276
|
+
hasInternalNetwork = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (serviceNetworks.length > 0 && !hasInternalNetwork) {
|
|
282
|
+
findings.push({
|
|
283
|
+
rule: 'database-internal-network',
|
|
284
|
+
severity: SEVERITY.MEDIUM,
|
|
285
|
+
service: name,
|
|
286
|
+
message: `Database '${name}' should use internal network (not externally accessible).`,
|
|
287
|
+
fix: 'Add "internal: true" to database network configuration.',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check for secrets in environment
|
|
294
|
+
for (const [name, service] of Object.entries(parsed.services)) {
|
|
295
|
+
const env = service.environment || {};
|
|
296
|
+
const envList = Array.isArray(env) ? env : Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
297
|
+
|
|
298
|
+
for (const envVar of envList) {
|
|
299
|
+
// Check for hardcoded secrets
|
|
300
|
+
if (SECRET_PATTERNS.some(p => p.test(envVar))) {
|
|
301
|
+
findings.push({
|
|
302
|
+
rule: 'no-secrets-in-env',
|
|
303
|
+
severity: SEVERITY.CRITICAL,
|
|
304
|
+
service: name,
|
|
305
|
+
message: `Service '${name}' has possible hardcoded secret in environment.`,
|
|
306
|
+
fix: 'Use Docker secrets or external secret management.',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Recommend Docker secrets for password-like vars
|
|
312
|
+
const hasPasswordEnv = envList.some(e =>
|
|
313
|
+
/password|secret|key|token/i.test(e)
|
|
314
|
+
);
|
|
315
|
+
const hasSecretsDefined = Object.keys(parsed.secrets || {}).length > 0;
|
|
316
|
+
if (hasPasswordEnv && !hasSecretsDefined) {
|
|
317
|
+
const existingRec = recommendations.find(r => r.rule === 'recommend-docker-secrets');
|
|
318
|
+
if (!existingRec) {
|
|
319
|
+
findings.push({
|
|
320
|
+
rule: 'recommend-docker-secrets',
|
|
321
|
+
severity: SEVERITY.LOW,
|
|
322
|
+
message: 'Sensitive environment variables detected. Consider using Docker secrets.',
|
|
323
|
+
fix: 'Define secrets in docker-compose and mount them in services.',
|
|
324
|
+
});
|
|
325
|
+
recommendations.push({ rule: 'recommend-docker-secrets' });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Calculate score
|
|
331
|
+
const score = calculateScore(findings);
|
|
332
|
+
|
|
333
|
+
// Generate recommendations
|
|
334
|
+
const allRecommendations = generateRecommendations(findings);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
findings,
|
|
338
|
+
parsed,
|
|
339
|
+
score,
|
|
340
|
+
recommendations: allRecommendations,
|
|
341
|
+
summary: {
|
|
342
|
+
total: findings.length,
|
|
343
|
+
critical: findings.filter(f => f.severity === SEVERITY.CRITICAL).length,
|
|
344
|
+
high: findings.filter(f => f.severity === SEVERITY.HIGH).length,
|
|
345
|
+
medium: findings.filter(f => f.severity === SEVERITY.MEDIUM).length,
|
|
346
|
+
low: findings.filter(f => f.severity === SEVERITY.LOW).length,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Calculate security score (0-100)
|
|
353
|
+
* @param {Array} findings - Validation findings
|
|
354
|
+
* @returns {number} Security score
|
|
355
|
+
*/
|
|
356
|
+
function calculateScore(findings) {
|
|
357
|
+
let score = 100;
|
|
358
|
+
|
|
359
|
+
for (const finding of findings) {
|
|
360
|
+
switch (finding.severity) {
|
|
361
|
+
case SEVERITY.CRITICAL:
|
|
362
|
+
score -= 25;
|
|
363
|
+
break;
|
|
364
|
+
case SEVERITY.HIGH:
|
|
365
|
+
score -= 15;
|
|
366
|
+
break;
|
|
367
|
+
case SEVERITY.MEDIUM:
|
|
368
|
+
score -= 10;
|
|
369
|
+
break;
|
|
370
|
+
case SEVERITY.LOW:
|
|
371
|
+
score -= 5;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return Math.max(0, score);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Generate recommendations from findings
|
|
381
|
+
* @param {Array} findings - Validation findings
|
|
382
|
+
* @returns {Array} Recommendations
|
|
383
|
+
*/
|
|
384
|
+
function generateRecommendations(findings) {
|
|
385
|
+
const recommendations = [];
|
|
386
|
+
const seenRules = new Set();
|
|
387
|
+
|
|
388
|
+
for (const finding of findings) {
|
|
389
|
+
if (!seenRules.has(finding.rule) && finding.fix) {
|
|
390
|
+
recommendations.push({
|
|
391
|
+
rule: finding.rule,
|
|
392
|
+
severity: finding.severity,
|
|
393
|
+
message: finding.message,
|
|
394
|
+
fix: finding.fix,
|
|
395
|
+
});
|
|
396
|
+
seenRules.add(finding.rule);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return recommendations.sort((a, b) => {
|
|
401
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
402
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Create a configurable runtime validator
|
|
408
|
+
* @param {Object} config - Validator configuration
|
|
409
|
+
* @returns {Object} Validator instance
|
|
410
|
+
*/
|
|
411
|
+
export function createRuntimeValidator(config = {}) {
|
|
412
|
+
const { rules = {} } = config;
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
/**
|
|
416
|
+
* Validate docker-compose content
|
|
417
|
+
* @param {string} content - YAML content
|
|
418
|
+
* @returns {Object} Validation result
|
|
419
|
+
*/
|
|
420
|
+
validate(content) {
|
|
421
|
+
const result = validateCompose(content);
|
|
422
|
+
|
|
423
|
+
// Filter findings based on rule configuration
|
|
424
|
+
result.findings = result.findings.filter(finding => {
|
|
425
|
+
const ruleConfig = rules[finding.rule];
|
|
426
|
+
if (ruleConfig === 'off' || ruleConfig === false) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Recalculate score
|
|
433
|
+
result.score = calculateScore(result.findings);
|
|
434
|
+
result.summary = {
|
|
435
|
+
total: result.findings.length,
|
|
436
|
+
critical: result.findings.filter(f => f.severity === SEVERITY.CRITICAL).length,
|
|
437
|
+
high: result.findings.filter(f => f.severity === SEVERITY.HIGH).length,
|
|
438
|
+
medium: result.findings.filter(f => f.severity === SEVERITY.MEDIUM).length,
|
|
439
|
+
low: result.findings.filter(f => f.severity === SEVERITY.LOW).length,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return result;
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Validate from file path
|
|
447
|
+
* @param {string} filePath - Path to docker-compose.yml
|
|
448
|
+
* @param {Function} readFile - File reader function
|
|
449
|
+
* @returns {Promise<Object>} Validation result
|
|
450
|
+
*/
|
|
451
|
+
async validateFile(filePath, readFile) {
|
|
452
|
+
const content = await readFile(filePath, 'utf8');
|
|
453
|
+
return this.validate(content);
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|