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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Auth Security Tests
3
+ *
4
+ * Tests for secure authentication primitives.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ hashPassword,
10
+ verifyPassword,
11
+ generateSessionToken,
12
+ validateSessionToken,
13
+ createRateLimiter,
14
+ createAccountLockout,
15
+ generateCookieOptions,
16
+ timingSafeCompare,
17
+ } from './auth-security.js';
18
+
19
+ describe('auth-security', () => {
20
+ describe('hashPassword', () => {
21
+ it('hashes password with Argon2id', async () => {
22
+ const hash = await hashPassword('mySecurePassword123');
23
+
24
+ expect(hash).toMatch(/^\$argon2id\$/);
25
+ });
26
+
27
+ it('produces different hashes for same password', async () => {
28
+ const hash1 = await hashPassword('samePassword');
29
+ const hash2 = await hashPassword('samePassword');
30
+
31
+ expect(hash1).not.toBe(hash2);
32
+ });
33
+
34
+ it('uses recommended Argon2id parameters', async () => {
35
+ const hash = await hashPassword('password');
36
+
37
+ // Argon2id with memory cost, time cost, parallelism
38
+ expect(hash).toContain('argon2id');
39
+ // Should have version, memory, time, parallelism encoded
40
+ expect(hash.split('$').length).toBeGreaterThan(3);
41
+ });
42
+
43
+ it('rejects empty password', async () => {
44
+ await expect(hashPassword('')).rejects.toThrow();
45
+ });
46
+
47
+ it('handles unicode passwords', async () => {
48
+ const hash = await hashPassword('密码123');
49
+ expect(hash).toMatch(/^\$argon2id\$/);
50
+ });
51
+
52
+ it('handles very long passwords', async () => {
53
+ const longPassword = 'a'.repeat(1000);
54
+ const hash = await hashPassword(longPassword);
55
+ expect(hash).toMatch(/^\$argon2id\$/);
56
+ });
57
+ });
58
+
59
+ describe('verifyPassword', () => {
60
+ it('returns true for correct password', async () => {
61
+ const hash = await hashPassword('correctPassword');
62
+ const result = await verifyPassword('correctPassword', hash);
63
+
64
+ expect(result).toBe(true);
65
+ });
66
+
67
+ it('returns false for incorrect password', async () => {
68
+ const hash = await hashPassword('correctPassword');
69
+ const result = await verifyPassword('wrongPassword', hash);
70
+
71
+ expect(result).toBe(false);
72
+ });
73
+
74
+ it('returns false for empty password', async () => {
75
+ const hash = await hashPassword('password');
76
+ const result = await verifyPassword('', hash);
77
+
78
+ expect(result).toBe(false);
79
+ });
80
+
81
+ it('returns false for malformed hash', async () => {
82
+ const result = await verifyPassword('password', 'not-a-valid-hash');
83
+
84
+ expect(result).toBe(false);
85
+ });
86
+
87
+ it('timing is constant regardless of password correctness', async () => {
88
+ const hash = await hashPassword('password');
89
+
90
+ // Measure time for correct password
91
+ const startCorrect = process.hrtime.bigint();
92
+ await verifyPassword('password', hash);
93
+ const endCorrect = process.hrtime.bigint();
94
+ const correctTime = Number(endCorrect - startCorrect);
95
+
96
+ // Measure time for incorrect password
97
+ const startIncorrect = process.hrtime.bigint();
98
+ await verifyPassword('wrongpassword', hash);
99
+ const endIncorrect = process.hrtime.bigint();
100
+ const incorrectTime = Number(endIncorrect - startIncorrect);
101
+
102
+ // Times should be within 20% of each other (timing-safe)
103
+ const ratio = Math.max(correctTime, incorrectTime) / Math.min(correctTime, incorrectTime);
104
+ expect(ratio).toBeLessThan(1.5);
105
+ });
106
+ });
107
+
108
+ describe('generateSessionToken', () => {
109
+ it('generates 256-bit token by default', () => {
110
+ const token = generateSessionToken();
111
+
112
+ // 256 bits = 32 bytes = 64 hex chars
113
+ expect(token).toMatch(/^[a-f0-9]{64}$/);
114
+ });
115
+
116
+ it('generates cryptographically random tokens', () => {
117
+ const tokens = new Set();
118
+ for (let i = 0; i < 100; i++) {
119
+ tokens.add(generateSessionToken());
120
+ }
121
+
122
+ // All tokens should be unique
123
+ expect(tokens.size).toBe(100);
124
+ });
125
+
126
+ it('generates token with custom length', () => {
127
+ const token = generateSessionToken({ bytes: 16 });
128
+
129
+ // 16 bytes = 32 hex chars
130
+ expect(token).toMatch(/^[a-f0-9]{32}$/);
131
+ });
132
+
133
+ it('generates base64url encoded token', () => {
134
+ const token = generateSessionToken({ encoding: 'base64url' });
135
+
136
+ expect(token).toMatch(/^[A-Za-z0-9_-]+$/);
137
+ });
138
+ });
139
+
140
+ describe('validateSessionToken', () => {
141
+ it('validates correctly formatted token', () => {
142
+ const token = generateSessionToken();
143
+ const result = validateSessionToken(token);
144
+
145
+ expect(result.valid).toBe(true);
146
+ });
147
+
148
+ it('rejects token with invalid characters', () => {
149
+ const result = validateSessionToken('invalid-token-with-dashes!');
150
+
151
+ expect(result.valid).toBe(false);
152
+ expect(result.reason).toContain('characters');
153
+ });
154
+
155
+ it('rejects token with wrong length', () => {
156
+ const result = validateSessionToken('abc123');
157
+
158
+ expect(result.valid).toBe(false);
159
+ expect(result.reason).toContain('length');
160
+ });
161
+
162
+ it('rejects empty token', () => {
163
+ const result = validateSessionToken('');
164
+
165
+ expect(result.valid).toBe(false);
166
+ });
167
+ });
168
+
169
+ describe('createRateLimiter', () => {
170
+ beforeEach(() => {
171
+ vi.useFakeTimers();
172
+ });
173
+
174
+ afterEach(() => {
175
+ vi.useRealTimers();
176
+ });
177
+
178
+ it('allows requests under threshold', () => {
179
+ const limiter = createRateLimiter({
180
+ maxAttempts: 5,
181
+ windowMs: 60000,
182
+ });
183
+
184
+ for (let i = 0; i < 5; i++) {
185
+ const result = limiter.check('user@example.com');
186
+ expect(result.allowed).toBe(true);
187
+ }
188
+ });
189
+
190
+ it('blocks requests over threshold', () => {
191
+ const limiter = createRateLimiter({
192
+ maxAttempts: 5,
193
+ windowMs: 60000,
194
+ });
195
+
196
+ for (let i = 0; i < 5; i++) {
197
+ limiter.check('user@example.com');
198
+ }
199
+
200
+ const result = limiter.check('user@example.com');
201
+ expect(result.allowed).toBe(false);
202
+ expect(result.retryAfter).toBeGreaterThan(0);
203
+ });
204
+
205
+ it('resets after window expires', () => {
206
+ const limiter = createRateLimiter({
207
+ maxAttempts: 5,
208
+ windowMs: 60000,
209
+ });
210
+
211
+ // Exhaust attempts
212
+ for (let i = 0; i < 5; i++) {
213
+ limiter.check('user@example.com');
214
+ }
215
+
216
+ expect(limiter.check('user@example.com').allowed).toBe(false);
217
+
218
+ // Advance time past window
219
+ vi.advanceTimersByTime(61000);
220
+
221
+ expect(limiter.check('user@example.com').allowed).toBe(true);
222
+ });
223
+
224
+ it('tracks different keys separately', () => {
225
+ const limiter = createRateLimiter({
226
+ maxAttempts: 5,
227
+ windowMs: 60000,
228
+ });
229
+
230
+ // Exhaust attempts for user1
231
+ for (let i = 0; i < 5; i++) {
232
+ limiter.check('user1@example.com');
233
+ }
234
+
235
+ // user2 should still be allowed
236
+ expect(limiter.check('user2@example.com').allowed).toBe(true);
237
+ });
238
+
239
+ it('provides remaining attempts count', () => {
240
+ const limiter = createRateLimiter({
241
+ maxAttempts: 5,
242
+ windowMs: 60000,
243
+ });
244
+
245
+ const result1 = limiter.check('user@example.com');
246
+ expect(result1.remaining).toBe(4);
247
+
248
+ const result2 = limiter.check('user@example.com');
249
+ expect(result2.remaining).toBe(3);
250
+ });
251
+
252
+ it('supports manual reset', () => {
253
+ const limiter = createRateLimiter({
254
+ maxAttempts: 5,
255
+ windowMs: 60000,
256
+ });
257
+
258
+ // Exhaust attempts
259
+ for (let i = 0; i < 5; i++) {
260
+ limiter.check('user@example.com');
261
+ }
262
+
263
+ limiter.reset('user@example.com');
264
+
265
+ expect(limiter.check('user@example.com').allowed).toBe(true);
266
+ });
267
+ });
268
+
269
+ describe('createAccountLockout', () => {
270
+ beforeEach(() => {
271
+ vi.useFakeTimers();
272
+ });
273
+
274
+ afterEach(() => {
275
+ vi.useRealTimers();
276
+ });
277
+
278
+ it('locks account after N failed attempts', () => {
279
+ const lockout = createAccountLockout({
280
+ maxFailures: 3,
281
+ lockoutDurationMs: 900000, // 15 minutes
282
+ });
283
+
284
+ lockout.recordFailure('user@example.com');
285
+ lockout.recordFailure('user@example.com');
286
+ lockout.recordFailure('user@example.com');
287
+
288
+ const result = lockout.isLocked('user@example.com');
289
+ expect(result.locked).toBe(true);
290
+ expect(result.unlockAt).toBeDefined();
291
+ });
292
+
293
+ it('unlocks after lockout duration', () => {
294
+ const lockout = createAccountLockout({
295
+ maxFailures: 3,
296
+ lockoutDurationMs: 900000,
297
+ });
298
+
299
+ // Lock the account
300
+ lockout.recordFailure('user@example.com');
301
+ lockout.recordFailure('user@example.com');
302
+ lockout.recordFailure('user@example.com');
303
+
304
+ expect(lockout.isLocked('user@example.com').locked).toBe(true);
305
+
306
+ // Advance time past lockout
307
+ vi.advanceTimersByTime(900001);
308
+
309
+ expect(lockout.isLocked('user@example.com').locked).toBe(false);
310
+ });
311
+
312
+ it('resets failures on successful login', () => {
313
+ const lockout = createAccountLockout({
314
+ maxFailures: 3,
315
+ lockoutDurationMs: 900000,
316
+ });
317
+
318
+ lockout.recordFailure('user@example.com');
319
+ lockout.recordFailure('user@example.com');
320
+
321
+ lockout.recordSuccess('user@example.com');
322
+
323
+ // Should be able to fail 3 more times before lockout
324
+ lockout.recordFailure('user@example.com');
325
+ lockout.recordFailure('user@example.com');
326
+
327
+ expect(lockout.isLocked('user@example.com').locked).toBe(false);
328
+ });
329
+
330
+ it('implements exponential backoff', () => {
331
+ const lockout = createAccountLockout({
332
+ maxFailures: 3,
333
+ lockoutDurationMs: 900000,
334
+ exponentialBackoff: true,
335
+ });
336
+
337
+ // First lockout
338
+ lockout.recordFailure('user@example.com');
339
+ lockout.recordFailure('user@example.com');
340
+ lockout.recordFailure('user@example.com');
341
+
342
+ const firstLockout = lockout.isLocked('user@example.com');
343
+
344
+ // Unlock and lock again
345
+ vi.advanceTimersByTime(900001);
346
+ lockout.recordFailure('user@example.com');
347
+ lockout.recordFailure('user@example.com');
348
+ lockout.recordFailure('user@example.com');
349
+
350
+ const secondLockout = lockout.isLocked('user@example.com');
351
+
352
+ // Second lockout should be longer
353
+ expect(secondLockout.unlockAt - Date.now()).toBeGreaterThan(
354
+ firstLockout.unlockAt - Date.now() + 900000
355
+ );
356
+ });
357
+ });
358
+
359
+ describe('generateCookieOptions', () => {
360
+ it('sets httpOnly by default', () => {
361
+ const options = generateCookieOptions();
362
+ expect(options.httpOnly).toBe(true);
363
+ });
364
+
365
+ it('sets secure in production', () => {
366
+ const options = generateCookieOptions({ production: true });
367
+ expect(options.secure).toBe(true);
368
+ });
369
+
370
+ it('sets sameSite to Strict by default', () => {
371
+ const options = generateCookieOptions();
372
+ expect(options.sameSite).toBe('Strict');
373
+ });
374
+
375
+ it('allows sameSite Lax for cross-site navigation', () => {
376
+ const options = generateCookieOptions({ sameSite: 'Lax' });
377
+ expect(options.sameSite).toBe('Lax');
378
+ });
379
+
380
+ it('sets path to / by default', () => {
381
+ const options = generateCookieOptions();
382
+ expect(options.path).toBe('/');
383
+ });
384
+
385
+ it('sets appropriate maxAge', () => {
386
+ const options = generateCookieOptions({ maxAge: 3600000 });
387
+ expect(options.maxAge).toBe(3600000);
388
+ });
389
+
390
+ it('includes domain when specified', () => {
391
+ const options = generateCookieOptions({ domain: 'example.com' });
392
+ expect(options.domain).toBe('example.com');
393
+ });
394
+ });
395
+
396
+ describe('timingSafeCompare', () => {
397
+ it('returns true for equal strings', () => {
398
+ const result = timingSafeCompare('password123', 'password123');
399
+ expect(result).toBe(true);
400
+ });
401
+
402
+ it('returns false for different strings', () => {
403
+ const result = timingSafeCompare('password123', 'password456');
404
+ expect(result).toBe(false);
405
+ });
406
+
407
+ it('returns false for different lengths', () => {
408
+ const result = timingSafeCompare('short', 'muchlongerstring');
409
+ expect(result).toBe(false);
410
+ });
411
+
412
+ it('returns false for empty vs non-empty', () => {
413
+ const result = timingSafeCompare('', 'password');
414
+ expect(result).toBe(false);
415
+ });
416
+
417
+ it('has constant timing regardless of match position', () => {
418
+ const target = 'abcdefghij';
419
+
420
+ // Mismatch at start
421
+ const startMismatch = 'xbcdefghij';
422
+ // Mismatch at end
423
+ const endMismatch = 'abcdefghix';
424
+
425
+ const times1 = [];
426
+ const times2 = [];
427
+
428
+ for (let i = 0; i < 100; i++) {
429
+ const start1 = process.hrtime.bigint();
430
+ timingSafeCompare(target, startMismatch);
431
+ const end1 = process.hrtime.bigint();
432
+ times1.push(Number(end1 - start1));
433
+
434
+ const start2 = process.hrtime.bigint();
435
+ timingSafeCompare(target, endMismatch);
436
+ const end2 = process.hrtime.bigint();
437
+ times2.push(Number(end2 - start2));
438
+ }
439
+
440
+ // Average times should be similar
441
+ const avg1 = times1.reduce((a, b) => a + b, 0) / times1.length;
442
+ const avg2 = times2.reduce((a, b) => a + b, 0) / times2.length;
443
+ const ratio = Math.max(avg1, avg2) / Math.min(avg1, avg2);
444
+
445
+ expect(ratio).toBeLessThan(2);
446
+ });
447
+ });
448
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * CIS Docker Benchmark Checker
3
+ */
4
+
5
+ export const CIS_CHECKS = {
6
+ '4.1': { title: 'Ensure a user for the container has been created', level: 1 },
7
+ '4.6': { title: 'Ensure HEALTHCHECK instructions have been added', level: 1 },
8
+ '4.8': { title: 'Ensure setuid and setgid permissions are removed', level: 2 },
9
+ '5.3': { title: 'Ensure Linux kernel capabilities are restricted', level: 1 },
10
+ '5.4': { title: 'Ensure privileged containers are not used', level: 1 },
11
+ '5.10': { title: 'Ensure memory usage for container is limited', level: 1 },
12
+ '5.11': { title: 'Ensure CPU priority is set appropriately', level: 1 },
13
+ '5.12': { title: 'Ensure the container root filesystem is mounted as read only', level: 1 },
14
+ '5.13': { title: 'Ensure incoming container traffic is bound to a specific host interface', level: 1 },
15
+ '5.14': { title: 'Ensure on-failure restart policy is set to 5', level: 1 },
16
+ '5.25': { title: 'Ensure the container is restricted from acquiring new privileges', level: 1 },
17
+ };
18
+
19
+ export function checkDockerfileCompliance(dockerfile) {
20
+ const findings = [];
21
+ const lines = dockerfile.split('\n').map(l => l.trim().toUpperCase());
22
+
23
+ // CIS 4.1 - USER directive
24
+ if (!lines.some(l => l.startsWith('USER ') && !l.includes('ROOT'))) {
25
+ findings.push({ cis: '4.1', severity: 'high', message: 'No non-root USER directive found.' });
26
+ }
27
+
28
+ // CIS 4.6 - HEALTHCHECK
29
+ if (!lines.some(l => l.startsWith('HEALTHCHECK '))) {
30
+ findings.push({ cis: '4.6', severity: 'medium', message: 'No HEALTHCHECK instruction.' });
31
+ }
32
+
33
+ // CIS 4.8 - Content trust / signing labels
34
+ if (!dockerfile.toLowerCase().includes('label') || !dockerfile.toLowerCase().includes('maintainer')) {
35
+ findings.push({ cis: '4.8', severity: 'low', message: 'Missing image labels for content trust.' });
36
+ }
37
+
38
+ return { findings, score: Math.max(0, 100 - findings.length * 20) };
39
+ }
40
+
41
+ export function checkComposeCompliance(compose) {
42
+ const findings = [];
43
+ const services = compose.services || {};
44
+
45
+ for (const [name, svc] of Object.entries(services)) {
46
+ // CIS 5.4 - No privileged
47
+ if (svc.privileged === true) {
48
+ findings.push({ cis: '5.4', severity: 'critical', service: name, message: `Service '${name}' uses privileged mode.` });
49
+ }
50
+
51
+ // CIS 5.3 - Capabilities
52
+ if (!svc.cap_drop?.includes('ALL')) {
53
+ findings.push({ cis: '5.3', severity: 'high', service: name, message: `Service '${name}' should drop ALL capabilities.` });
54
+ }
55
+
56
+ // CIS 5.10 - Memory limits
57
+ const hasMemLimit = svc.deploy?.resources?.limits?.memory || svc.mem_limit;
58
+ if (!hasMemLimit) {
59
+ findings.push({ cis: '5.10', severity: 'medium', service: name, message: `Service '${name}' has no memory limit.` });
60
+ }
61
+
62
+ // CIS 5.12 - Read-only root
63
+ if (svc.read_only !== true && !/db|postgres|mysql|mongo|redis/i.test(name)) {
64
+ findings.push({ cis: '5.12', severity: 'medium', service: name, message: `Service '${name}' should use read_only filesystem.` });
65
+ }
66
+
67
+ // CIS 5.25 - No new privileges
68
+ const hasNoNewPriv = svc.security_opt?.some(o => o.includes('no-new-privileges'));
69
+ if (!hasNoNewPriv) {
70
+ findings.push({ cis: '5.25', severity: 'medium', service: name, message: `Service '${name}' should set no-new-privileges.` });
71
+ }
72
+ }
73
+
74
+ return { findings, score: Math.max(0, 100 - findings.filter(f => f.severity === 'critical').length * 30 - findings.filter(f => f.severity === 'high').length * 15) };
75
+ }
76
+
77
+ export function checkRuntimeCompliance(compose) {
78
+ const findings = [];
79
+ const services = compose.services || {};
80
+
81
+ for (const [name, svc] of Object.entries(services)) {
82
+ // CIS 5.11 - PID limits
83
+ if (!svc.pids_limit) {
84
+ findings.push({ cis: '5.11', severity: 'low', service: name, message: `Service '${name}' has no PID limit.` });
85
+ }
86
+
87
+ // CIS 5.13 - Network mode
88
+ if (svc.network_mode === 'host') {
89
+ findings.push({ cis: '5.13', severity: 'high', service: name, message: `Service '${name}' uses host network.` });
90
+ }
91
+
92
+ // CIS 5.14 - Restart policy
93
+ if (svc.restart === 'always') {
94
+ findings.push({ cis: '5.14', severity: 'low', service: name, message: `Service '${name}' uses restart:always instead of on-failure.` });
95
+ }
96
+ }
97
+
98
+ return { findings, score: Math.max(0, 100 - findings.length * 10) };
99
+ }
100
+
101
+ export function generateComplianceReport(options = {}) {
102
+ const findings = [];
103
+
104
+ if (options.dockerfile) {
105
+ findings.push(...checkDockerfileCompliance(options.dockerfile).findings);
106
+ }
107
+ if (options.compose) {
108
+ findings.push(...checkComposeCompliance(options.compose).findings);
109
+ findings.push(...checkRuntimeCompliance(options.compose).findings);
110
+ }
111
+
112
+ const bySection = {};
113
+ for (const f of findings) {
114
+ const section = f.cis.split('.')[0];
115
+ bySection[section] = bySection[section] || [];
116
+ bySection[section].push(f);
117
+ }
118
+
119
+ const level1Checks = Object.entries(CIS_CHECKS).filter(([, v]) => v.level === 1).length;
120
+ const level1Fails = findings.filter(f => CIS_CHECKS[f.cis]?.level === 1).length;
121
+ const level1Score = Math.round(((level1Checks - level1Fails) / level1Checks) * 100);
122
+
123
+ return {
124
+ findings,
125
+ bySection,
126
+ level1Score,
127
+ summary: {
128
+ total: findings.length,
129
+ critical: findings.filter(f => f.severity === 'critical').length,
130
+ high: findings.filter(f => f.severity === 'high').length,
131
+ medium: findings.filter(f => f.severity === 'medium').length,
132
+ low: findings.filter(f => f.severity === 'low').length,
133
+ },
134
+ };
135
+ }
136
+
137
+ export function createCisBenchmark(config = {}) {
138
+ return {
139
+ checkDockerfile: checkDockerfileCompliance,
140
+ checkCompose: checkComposeCompliance,
141
+ checkRuntime: checkRuntimeCompliance,
142
+ generateReport: generateComplianceReport,
143
+ audit(options) {
144
+ const report = generateComplianceReport(options);
145
+ return {
146
+ ...report,
147
+ score: report.level1Score,
148
+ passed: report.level1Score >= (config.passThreshold || 70),
149
+ };
150
+ },
151
+ };
152
+ }