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,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
|
+
}
|