tlc-claude-code 1.4.7 → 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/docker-compose.dev.yml +6 -3
- 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,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Headers Generator Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for generating secure HTTP headers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
generateSecurityHeaders,
|
|
10
|
+
generateCsp,
|
|
11
|
+
generateHsts,
|
|
12
|
+
generatePermissionsPolicy,
|
|
13
|
+
createHeadersGenerator,
|
|
14
|
+
} from './headers-generator.js';
|
|
15
|
+
|
|
16
|
+
describe('headers-generator', () => {
|
|
17
|
+
describe('generateSecurityHeaders', () => {
|
|
18
|
+
it('generates all required security headers', () => {
|
|
19
|
+
const headers = generateSecurityHeaders();
|
|
20
|
+
|
|
21
|
+
expect(headers).toHaveProperty('Content-Security-Policy');
|
|
22
|
+
expect(headers).toHaveProperty('Strict-Transport-Security');
|
|
23
|
+
expect(headers).toHaveProperty('X-Content-Type-Options');
|
|
24
|
+
expect(headers).toHaveProperty('X-Frame-Options');
|
|
25
|
+
expect(headers).toHaveProperty('Referrer-Policy');
|
|
26
|
+
expect(headers).toHaveProperty('Permissions-Policy');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('includes Cross-Origin policies', () => {
|
|
30
|
+
const headers = generateSecurityHeaders();
|
|
31
|
+
|
|
32
|
+
expect(headers).toHaveProperty('Cross-Origin-Opener-Policy');
|
|
33
|
+
expect(headers).toHaveProperty('Cross-Origin-Embedder-Policy');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('sets X-Content-Type-Options to nosniff', () => {
|
|
37
|
+
const headers = generateSecurityHeaders();
|
|
38
|
+
|
|
39
|
+
expect(headers['X-Content-Type-Options']).toBe('nosniff');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('sets X-Frame-Options to DENY by default', () => {
|
|
43
|
+
const headers = generateSecurityHeaders();
|
|
44
|
+
|
|
45
|
+
expect(headers['X-Frame-Options']).toBe('DENY');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('allows X-Frame-Options SAMEORIGIN when configured', () => {
|
|
49
|
+
const headers = generateSecurityHeaders({
|
|
50
|
+
frameOptions: 'SAMEORIGIN',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('sets Referrer-Policy to strict-origin-when-cross-origin', () => {
|
|
57
|
+
const headers = generateSecurityHeaders();
|
|
58
|
+
|
|
59
|
+
expect(headers['Referrer-Policy']).toBe('strict-origin-when-cross-origin');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('allows custom Referrer-Policy', () => {
|
|
63
|
+
const headers = generateSecurityHeaders({
|
|
64
|
+
referrerPolicy: 'no-referrer',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(headers['Referrer-Policy']).toBe('no-referrer');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('generateCsp', () => {
|
|
72
|
+
it('generates CSP with strict defaults', () => {
|
|
73
|
+
const csp = generateCsp();
|
|
74
|
+
|
|
75
|
+
expect(csp).toContain("default-src 'self'");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not include unsafe-inline by default', () => {
|
|
79
|
+
const csp = generateCsp();
|
|
80
|
+
|
|
81
|
+
expect(csp).not.toContain("'unsafe-inline'");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('does not include unsafe-eval by default', () => {
|
|
85
|
+
const csp = generateCsp();
|
|
86
|
+
|
|
87
|
+
expect(csp).not.toContain("'unsafe-eval'");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('includes script-src with nonce support', () => {
|
|
91
|
+
const csp = generateCsp({ useNonce: true, nonce: 'abc123' });
|
|
92
|
+
|
|
93
|
+
expect(csp).toContain("'nonce-abc123'");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('allows configured script sources', () => {
|
|
97
|
+
const csp = generateCsp({
|
|
98
|
+
scriptSrc: ['https://cdn.example.com'],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(csp).toContain('https://cdn.example.com');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('includes style-src', () => {
|
|
105
|
+
const csp = generateCsp();
|
|
106
|
+
|
|
107
|
+
expect(csp).toContain('style-src');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('includes img-src', () => {
|
|
111
|
+
const csp = generateCsp();
|
|
112
|
+
|
|
113
|
+
expect(csp).toContain('img-src');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('includes connect-src for API calls', () => {
|
|
117
|
+
const csp = generateCsp({
|
|
118
|
+
connectSrc: ['https://api.example.com'],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(csp).toContain("connect-src 'self' https://api.example.com");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes frame-ancestors none by default', () => {
|
|
125
|
+
const csp = generateCsp();
|
|
126
|
+
|
|
127
|
+
expect(csp).toContain("frame-ancestors 'none'");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('allows frame-ancestors self', () => {
|
|
131
|
+
const csp = generateCsp({
|
|
132
|
+
frameAncestors: ["'self'"],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(csp).toContain("frame-ancestors 'self'");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('includes upgrade-insecure-requests', () => {
|
|
139
|
+
const csp = generateCsp();
|
|
140
|
+
|
|
141
|
+
expect(csp).toContain('upgrade-insecure-requests');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('includes block-all-mixed-content', () => {
|
|
145
|
+
const csp = generateCsp();
|
|
146
|
+
|
|
147
|
+
expect(csp).toContain('block-all-mixed-content');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('includes report-uri when configured', () => {
|
|
151
|
+
const csp = generateCsp({
|
|
152
|
+
reportUri: 'https://example.com/csp-report',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(csp).toContain('report-uri https://example.com/csp-report');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('includes report-to when configured', () => {
|
|
159
|
+
const csp = generateCsp({
|
|
160
|
+
reportTo: 'csp-endpoint',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(csp).toContain('report-to csp-endpoint');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('generates valid CSP for SPA with inline scripts', () => {
|
|
167
|
+
const csp = generateCsp({
|
|
168
|
+
mode: 'spa',
|
|
169
|
+
useNonce: true,
|
|
170
|
+
nonce: 'random123',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(csp).toContain("'nonce-random123'");
|
|
174
|
+
expect(csp).toContain("'strict-dynamic'");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('generateHsts', () => {
|
|
179
|
+
it('sets max-age to 1 year (31536000 seconds)', () => {
|
|
180
|
+
const hsts = generateHsts();
|
|
181
|
+
|
|
182
|
+
expect(hsts).toContain('max-age=31536000');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('includes includeSubDomains by default', () => {
|
|
186
|
+
const hsts = generateHsts();
|
|
187
|
+
|
|
188
|
+
expect(hsts).toContain('includeSubDomains');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('allows disabling includeSubDomains', () => {
|
|
192
|
+
const hsts = generateHsts({ includeSubDomains: false });
|
|
193
|
+
|
|
194
|
+
expect(hsts).not.toContain('includeSubDomains');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('includes preload when configured', () => {
|
|
198
|
+
const hsts = generateHsts({ preload: true });
|
|
199
|
+
|
|
200
|
+
expect(hsts).toContain('preload');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('allows custom max-age', () => {
|
|
204
|
+
const hsts = generateHsts({ maxAge: 86400 });
|
|
205
|
+
|
|
206
|
+
expect(hsts).toContain('max-age=86400');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('rejects max-age less than minimum for preload', () => {
|
|
210
|
+
expect(() => {
|
|
211
|
+
generateHsts({ preload: true, maxAge: 86400 });
|
|
212
|
+
}).toThrow();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('generatePermissionsPolicy', () => {
|
|
217
|
+
it('disables camera by default', () => {
|
|
218
|
+
const policy = generatePermissionsPolicy();
|
|
219
|
+
|
|
220
|
+
expect(policy).toContain('camera=()');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('disables microphone by default', () => {
|
|
224
|
+
const policy = generatePermissionsPolicy();
|
|
225
|
+
|
|
226
|
+
expect(policy).toContain('microphone=()');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('disables geolocation by default', () => {
|
|
230
|
+
const policy = generatePermissionsPolicy();
|
|
231
|
+
|
|
232
|
+
expect(policy).toContain('geolocation=()');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('disables payment by default', () => {
|
|
236
|
+
const policy = generatePermissionsPolicy();
|
|
237
|
+
|
|
238
|
+
expect(policy).toContain('payment=()');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('allows enabling specific features for self', () => {
|
|
242
|
+
const policy = generatePermissionsPolicy({
|
|
243
|
+
camera: ['self'],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(policy).toContain('camera=(self)');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('allows enabling features for specific origins', () => {
|
|
250
|
+
const policy = generatePermissionsPolicy({
|
|
251
|
+
camera: ['https://video.example.com'],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(policy).toContain('camera=("https://video.example.com")');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('includes interest-cohort opt-out', () => {
|
|
258
|
+
const policy = generatePermissionsPolicy();
|
|
259
|
+
|
|
260
|
+
expect(policy).toContain('interest-cohort=()');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('createHeadersGenerator', () => {
|
|
265
|
+
it('creates reusable generator with config', () => {
|
|
266
|
+
const generator = createHeadersGenerator({
|
|
267
|
+
csp: {
|
|
268
|
+
scriptSrc: ['https://cdn.example.com'],
|
|
269
|
+
},
|
|
270
|
+
hsts: {
|
|
271
|
+
preload: true,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const headers = generator.generate();
|
|
276
|
+
|
|
277
|
+
expect(headers['Content-Security-Policy']).toContain('https://cdn.example.com');
|
|
278
|
+
expect(headers['Strict-Transport-Security']).toContain('preload');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('supports per-route overrides', () => {
|
|
282
|
+
const generator = createHeadersGenerator({
|
|
283
|
+
csp: {
|
|
284
|
+
scriptSrc: ["'self'"],
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const defaultHeaders = generator.generate();
|
|
289
|
+
const adminHeaders = generator.generate({
|
|
290
|
+
route: '/admin',
|
|
291
|
+
overrides: {
|
|
292
|
+
csp: {
|
|
293
|
+
scriptSrc: ["'self'", "'unsafe-inline'"], // Needed for admin panel
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(defaultHeaders['Content-Security-Policy']).not.toContain("'unsafe-inline'");
|
|
299
|
+
expect(adminHeaders['Content-Security-Policy']).toContain("'unsafe-inline'");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('generates nonce for each request', () => {
|
|
303
|
+
const generator = createHeadersGenerator({
|
|
304
|
+
csp: {
|
|
305
|
+
useNonce: true,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const headers1 = generator.generate();
|
|
310
|
+
const headers2 = generator.generate();
|
|
311
|
+
|
|
312
|
+
const nonce1 = headers1['Content-Security-Policy'].match(/'nonce-([^']+)'/)?.[1];
|
|
313
|
+
const nonce2 = headers2['Content-Security-Policy'].match(/'nonce-([^']+)'/)?.[1];
|
|
314
|
+
|
|
315
|
+
expect(nonce1).toBeDefined();
|
|
316
|
+
expect(nonce2).toBeDefined();
|
|
317
|
+
expect(nonce1).not.toBe(nonce2);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('provides nonce for script injection', () => {
|
|
321
|
+
const generator = createHeadersGenerator({
|
|
322
|
+
csp: {
|
|
323
|
+
useNonce: true,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const { headers, nonce } = generator.generateWithNonce();
|
|
328
|
+
|
|
329
|
+
expect(nonce).toBeDefined();
|
|
330
|
+
expect(headers['Content-Security-Policy']).toContain(`'nonce-${nonce}'`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('supports report-only mode', () => {
|
|
334
|
+
const generator = createHeadersGenerator({
|
|
335
|
+
csp: {
|
|
336
|
+
reportOnly: true,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const headers = generator.generate();
|
|
341
|
+
|
|
342
|
+
expect(headers).toHaveProperty('Content-Security-Policy-Report-Only');
|
|
343
|
+
expect(headers).not.toHaveProperty('Content-Security-Policy');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('supports both enforce and report-only simultaneously', () => {
|
|
347
|
+
const generator = createHeadersGenerator({
|
|
348
|
+
csp: {
|
|
349
|
+
scriptSrc: ["'self'"],
|
|
350
|
+
},
|
|
351
|
+
cspReportOnly: {
|
|
352
|
+
scriptSrc: ["'self'", "'strict-dynamic'"],
|
|
353
|
+
reportUri: 'https://example.com/csp-report',
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const headers = generator.generate();
|
|
358
|
+
|
|
359
|
+
expect(headers).toHaveProperty('Content-Security-Policy');
|
|
360
|
+
expect(headers).toHaveProperty('Content-Security-Policy-Report-Only');
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('edge cases', () => {
|
|
365
|
+
it('escapes special characters in CSP directives', () => {
|
|
366
|
+
const csp = generateCsp({
|
|
367
|
+
scriptSrc: ["https://example.com/path?param=value&other=test"],
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Should not break the CSP syntax
|
|
371
|
+
expect(csp).toContain('https://example.com/path');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('handles empty arrays gracefully', () => {
|
|
375
|
+
const csp = generateCsp({
|
|
376
|
+
scriptSrc: [],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(csp).toContain("script-src 'self'");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('validates CSP directive names', () => {
|
|
383
|
+
expect(() => {
|
|
384
|
+
generateCsp({
|
|
385
|
+
'invalid-directive': ['value'],
|
|
386
|
+
});
|
|
387
|
+
}).toThrow();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('validates Permissions-Policy feature names', () => {
|
|
391
|
+
expect(() => {
|
|
392
|
+
generatePermissionsPolicy({
|
|
393
|
+
'invalid-feature': ['self'],
|
|
394
|
+
});
|
|
395
|
+
}).toThrow();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Scanner Module (Trivy Integration)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SEVERITY = {
|
|
6
|
+
CRITICAL: 'CRITICAL',
|
|
7
|
+
HIGH: 'HIGH',
|
|
8
|
+
MEDIUM: 'MEDIUM',
|
|
9
|
+
LOW: 'LOW',
|
|
10
|
+
UNKNOWN: 'UNKNOWN',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const SEVERITY_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, UNKNOWN: 4 };
|
|
14
|
+
|
|
15
|
+
export function parseTrixyOutput(jsonOutput) {
|
|
16
|
+
const data = typeof jsonOutput === 'string' ? JSON.parse(jsonOutput) : jsonOutput;
|
|
17
|
+
const vulnerabilities = [];
|
|
18
|
+
const summary = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0, total: 0 };
|
|
19
|
+
|
|
20
|
+
for (const result of data.Results || []) {
|
|
21
|
+
for (const vuln of result.Vulnerabilities || []) {
|
|
22
|
+
vulnerabilities.push({
|
|
23
|
+
id: vuln.VulnerabilityID,
|
|
24
|
+
severity: vuln.Severity,
|
|
25
|
+
title: vuln.Title,
|
|
26
|
+
description: vuln.Description,
|
|
27
|
+
package: vuln.PkgName,
|
|
28
|
+
installedVersion: vuln.InstalledVersion,
|
|
29
|
+
fixedVersion: vuln.FixedVersion,
|
|
30
|
+
target: result.Target,
|
|
31
|
+
});
|
|
32
|
+
const key = vuln.Severity?.toLowerCase() || 'unknown';
|
|
33
|
+
if (summary[key] !== undefined) summary[key]++;
|
|
34
|
+
summary.total++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { vulnerabilities, summary, raw: data };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function filterBySeverity(vulnerabilities, minSeverity) {
|
|
42
|
+
const minOrder = SEVERITY_ORDER[minSeverity] ?? 4;
|
|
43
|
+
return vulnerabilities.filter(v => (SEVERITY_ORDER[v.severity] ?? 4) <= minOrder);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function shouldBlockBuild(parsed, options = {}) {
|
|
47
|
+
const blockOn = options.blockOn || 'CRITICAL';
|
|
48
|
+
const blockOrder = SEVERITY_ORDER[blockOn] ?? 0;
|
|
49
|
+
|
|
50
|
+
return parsed.vulnerabilities.some(v => (SEVERITY_ORDER[v.severity] ?? 4) <= blockOrder);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createImageScanner(config = {}) {
|
|
54
|
+
const { blockOn = 'CRITICAL', exec = null } = config;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
async scan(image) {
|
|
58
|
+
if (!exec) throw new Error('exec function required for scanning');
|
|
59
|
+
const output = await exec(`trivy image --format json ${image}`);
|
|
60
|
+
const parsed = parseTrixyOutput(output);
|
|
61
|
+
return {
|
|
62
|
+
image,
|
|
63
|
+
vulnerabilities: parsed.vulnerabilities,
|
|
64
|
+
summary: parsed.summary,
|
|
65
|
+
shouldBlock: shouldBlockBuild(parsed, { blockOn }),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
generateReport(parsed) {
|
|
70
|
+
const blocked = shouldBlockBuild(parsed, { blockOn });
|
|
71
|
+
return {
|
|
72
|
+
passed: !blocked,
|
|
73
|
+
summary: parsed.summary,
|
|
74
|
+
vulnerabilities: parsed.vulnerabilities,
|
|
75
|
+
blockingVulnerabilities: filterBySeverity(parsed.vulnerabilities, blockOn),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
parseOutput: parseTrixyOutput,
|
|
80
|
+
filterBySeverity,
|
|
81
|
+
shouldBlockBuild: (parsed) => shouldBlockBuild(parsed, { blockOn }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Scanner (Trivy) Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
parseTrixyOutput,
|
|
7
|
+
filterBySeverity,
|
|
8
|
+
shouldBlockBuild,
|
|
9
|
+
createImageScanner,
|
|
10
|
+
SEVERITY,
|
|
11
|
+
} from './image-scanner.js';
|
|
12
|
+
|
|
13
|
+
describe('image-scanner', () => {
|
|
14
|
+
const mockTrivyOutput = {
|
|
15
|
+
Results: [{
|
|
16
|
+
Target: 'node:20-alpine',
|
|
17
|
+
Vulnerabilities: [
|
|
18
|
+
{ VulnerabilityID: 'CVE-2024-0001', Severity: 'CRITICAL', Title: 'Critical vuln' },
|
|
19
|
+
{ VulnerabilityID: 'CVE-2024-0002', Severity: 'HIGH', Title: 'High vuln' },
|
|
20
|
+
{ VulnerabilityID: 'CVE-2024-0003', Severity: 'MEDIUM', Title: 'Medium vuln' },
|
|
21
|
+
{ VulnerabilityID: 'CVE-2024-0004', Severity: 'LOW', Title: 'Low vuln' },
|
|
22
|
+
],
|
|
23
|
+
}],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('parseTrixyOutput', () => {
|
|
27
|
+
it('parses Trivy JSON output', () => {
|
|
28
|
+
const result = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
29
|
+
expect(result.vulnerabilities).toHaveLength(4);
|
|
30
|
+
expect(result.summary.critical).toBe(1);
|
|
31
|
+
expect(result.summary.high).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('handles empty results', () => {
|
|
35
|
+
const result = parseTrixyOutput(JSON.stringify({ Results: [] }));
|
|
36
|
+
expect(result.vulnerabilities).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('handles null vulnerabilities', () => {
|
|
40
|
+
const result = parseTrixyOutput(JSON.stringify({ Results: [{ Target: 'test', Vulnerabilities: null }] }));
|
|
41
|
+
expect(result.vulnerabilities).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('filterBySeverity', () => {
|
|
46
|
+
it('filters by minimum severity', () => {
|
|
47
|
+
const parsed = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
48
|
+
const filtered = filterBySeverity(parsed.vulnerabilities, 'HIGH');
|
|
49
|
+
expect(filtered).toHaveLength(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns all for LOW severity', () => {
|
|
53
|
+
const parsed = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
54
|
+
const filtered = filterBySeverity(parsed.vulnerabilities, 'LOW');
|
|
55
|
+
expect(filtered).toHaveLength(4);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns only critical for CRITICAL', () => {
|
|
59
|
+
const parsed = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
60
|
+
const filtered = filterBySeverity(parsed.vulnerabilities, 'CRITICAL');
|
|
61
|
+
expect(filtered).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('shouldBlockBuild', () => {
|
|
66
|
+
it('blocks on critical vulnerabilities', () => {
|
|
67
|
+
const parsed = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
68
|
+
expect(shouldBlockBuild(parsed, { blockOn: 'CRITICAL' })).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('blocks on high when threshold is HIGH', () => {
|
|
72
|
+
const parsed = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
73
|
+
expect(shouldBlockBuild(parsed, { blockOn: 'HIGH' })).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('passes when no critical vulns', () => {
|
|
77
|
+
const noVulns = { Results: [{ Target: 'clean', Vulnerabilities: [] }] };
|
|
78
|
+
const parsed = parseTrixyOutput(JSON.stringify(noVulns));
|
|
79
|
+
expect(shouldBlockBuild(parsed, { blockOn: 'CRITICAL' })).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('createImageScanner', () => {
|
|
84
|
+
it('creates scanner with config', () => {
|
|
85
|
+
const scanner = createImageScanner({ blockOn: 'HIGH' });
|
|
86
|
+
expect(scanner).toBeDefined();
|
|
87
|
+
expect(scanner.scan).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('generates compliance report', () => {
|
|
91
|
+
const scanner = createImageScanner();
|
|
92
|
+
const parsed = parseTrixyOutput(JSON.stringify(mockTrivyOutput));
|
|
93
|
+
const report = scanner.generateReport(parsed);
|
|
94
|
+
expect(report.passed).toBe(false);
|
|
95
|
+
expect(report.summary).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('scanner returns structured results', async () => {
|
|
99
|
+
const mockExec = vi.fn().mockResolvedValue(JSON.stringify(mockTrivyOutput));
|
|
100
|
+
const scanner = createImageScanner({ exec: mockExec });
|
|
101
|
+
const result = await scanner.scan('node:20-alpine');
|
|
102
|
+
expect(result.image).toBe('node:20-alpine');
|
|
103
|
+
expect(result.vulnerabilities).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|