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,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Audit Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
logDeploymentEvent,
|
|
7
|
+
queryAuditLog,
|
|
8
|
+
exportAuditLog,
|
|
9
|
+
AUDIT_EVENTS,
|
|
10
|
+
createDeploymentAudit,
|
|
11
|
+
} from './deployment-audit.js';
|
|
12
|
+
|
|
13
|
+
describe('deployment-audit', () => {
|
|
14
|
+
describe('AUDIT_EVENTS', () => {
|
|
15
|
+
it('defines all event types', () => {
|
|
16
|
+
expect(AUDIT_EVENTS.DEPLOYMENT_STARTED).toBe('deployment_started');
|
|
17
|
+
expect(AUDIT_EVENTS.DEPLOYMENT_COMPLETED).toBe('deployment_completed');
|
|
18
|
+
expect(AUDIT_EVENTS.DEPLOYMENT_FAILED).toBe('deployment_failed');
|
|
19
|
+
expect(AUDIT_EVENTS.APPROVAL_REQUESTED).toBe('approval_requested');
|
|
20
|
+
expect(AUDIT_EVENTS.APPROVAL_GRANTED).toBe('approval_granted');
|
|
21
|
+
expect(AUDIT_EVENTS.APPROVAL_DENIED).toBe('approval_denied');
|
|
22
|
+
expect(AUDIT_EVENTS.ROLLBACK_TRIGGERED).toBe('rollback_triggered');
|
|
23
|
+
expect(AUDIT_EVENTS.ROLLBACK_COMPLETED).toBe('rollback_completed');
|
|
24
|
+
expect(AUDIT_EVENTS.SECURITY_GATE_PASSED).toBe('security_gate_passed');
|
|
25
|
+
expect(AUDIT_EVENTS.SECURITY_GATE_FAILED).toBe('security_gate_failed');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('logDeploymentEvent', () => {
|
|
30
|
+
it('logs event with required fields', async () => {
|
|
31
|
+
const mockWrite = vi.fn().mockResolvedValue(true);
|
|
32
|
+
|
|
33
|
+
const entry = await logDeploymentEvent({
|
|
34
|
+
event: 'deployment_started',
|
|
35
|
+
deploymentId: 'deploy-123',
|
|
36
|
+
branch: 'main',
|
|
37
|
+
user: 'alice',
|
|
38
|
+
writeFn: mockWrite,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(entry.id).toBeDefined();
|
|
42
|
+
expect(entry.event).toBe('deployment_started');
|
|
43
|
+
expect(entry.deploymentId).toBe('deploy-123');
|
|
44
|
+
expect(entry.branch).toBe('main');
|
|
45
|
+
expect(entry.user).toBe('alice');
|
|
46
|
+
expect(entry.timestamp).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('includes metadata when provided', async () => {
|
|
50
|
+
const mockWrite = vi.fn().mockResolvedValue(true);
|
|
51
|
+
|
|
52
|
+
const entry = await logDeploymentEvent({
|
|
53
|
+
event: 'deployment_completed',
|
|
54
|
+
deploymentId: 'deploy-123',
|
|
55
|
+
branch: 'main',
|
|
56
|
+
user: 'alice',
|
|
57
|
+
metadata: {
|
|
58
|
+
duration: 120000,
|
|
59
|
+
commitSha: 'abc123',
|
|
60
|
+
},
|
|
61
|
+
writeFn: mockWrite,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(entry.metadata.duration).toBe(120000);
|
|
65
|
+
expect(entry.metadata.commitSha).toBe('abc123');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('generates checksum for integrity', async () => {
|
|
69
|
+
const mockWrite = vi.fn().mockResolvedValue(true);
|
|
70
|
+
|
|
71
|
+
const entry = await logDeploymentEvent({
|
|
72
|
+
event: 'deployment_started',
|
|
73
|
+
deploymentId: 'deploy-123',
|
|
74
|
+
branch: 'main',
|
|
75
|
+
user: 'alice',
|
|
76
|
+
writeFn: mockWrite,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(entry.checksum).toBeDefined();
|
|
80
|
+
expect(entry.checksum).toMatch(/^[a-f0-9]{64}$/); // SHA-256
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('links to previous entry', async () => {
|
|
84
|
+
const mockWrite = vi.fn().mockResolvedValue(true);
|
|
85
|
+
const previousChecksum = 'abc123def456';
|
|
86
|
+
|
|
87
|
+
const entry = await logDeploymentEvent({
|
|
88
|
+
event: 'deployment_completed',
|
|
89
|
+
deploymentId: 'deploy-123',
|
|
90
|
+
branch: 'main',
|
|
91
|
+
user: 'alice',
|
|
92
|
+
previousChecksum,
|
|
93
|
+
writeFn: mockWrite,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(entry.previousChecksum).toBe(previousChecksum);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('queryAuditLog', () => {
|
|
101
|
+
const mockEntries = [
|
|
102
|
+
{ id: '1', event: 'deployment_started', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:00:00Z' },
|
|
103
|
+
{ id: '2', event: 'deployment_completed', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:05:00Z' },
|
|
104
|
+
{ id: '3', event: 'deployment_started', branch: 'dev', user: 'bob', timestamp: '2024-01-01T11:00:00Z' },
|
|
105
|
+
{ id: '4', event: 'deployment_failed', branch: 'dev', user: 'bob', timestamp: '2024-01-01T11:02:00Z' },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
it('queries by date range', async () => {
|
|
109
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
110
|
+
|
|
111
|
+
const results = await queryAuditLog({
|
|
112
|
+
startDate: '2024-01-01T10:00:00Z',
|
|
113
|
+
endDate: '2024-01-01T10:30:00Z',
|
|
114
|
+
queryFn: mockQuery,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(results).toHaveLength(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('queries by user', async () => {
|
|
121
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
122
|
+
|
|
123
|
+
const results = await queryAuditLog({
|
|
124
|
+
user: 'bob',
|
|
125
|
+
queryFn: mockQuery,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(results).toHaveLength(2);
|
|
129
|
+
expect(results.every(e => e.user === 'bob')).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('queries by branch', async () => {
|
|
133
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
134
|
+
|
|
135
|
+
const results = await queryAuditLog({
|
|
136
|
+
branch: 'main',
|
|
137
|
+
queryFn: mockQuery,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(results).toHaveLength(2);
|
|
141
|
+
expect(results.every(e => e.branch === 'main')).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('queries by event type', async () => {
|
|
145
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
146
|
+
|
|
147
|
+
const results = await queryAuditLog({
|
|
148
|
+
event: 'deployment_failed',
|
|
149
|
+
queryFn: mockQuery,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(results).toHaveLength(1);
|
|
153
|
+
expect(results[0].event).toBe('deployment_failed');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('combines filters', async () => {
|
|
157
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
158
|
+
|
|
159
|
+
const results = await queryAuditLog({
|
|
160
|
+
branch: 'dev',
|
|
161
|
+
user: 'bob',
|
|
162
|
+
queryFn: mockQuery,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(results).toHaveLength(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('paginates results', async () => {
|
|
169
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
170
|
+
|
|
171
|
+
const results = await queryAuditLog({
|
|
172
|
+
limit: 2,
|
|
173
|
+
offset: 1,
|
|
174
|
+
queryFn: mockQuery,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(results).toHaveLength(2);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('exportAuditLog', () => {
|
|
182
|
+
const mockEntries = [
|
|
183
|
+
{ id: '1', event: 'deployment_started', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:00:00Z' },
|
|
184
|
+
{ id: '2', event: 'deployment_completed', branch: 'main', user: 'alice', timestamp: '2024-01-01T10:05:00Z' },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
it('exports as JSON', async () => {
|
|
188
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
189
|
+
|
|
190
|
+
const result = await exportAuditLog({
|
|
191
|
+
format: 'json',
|
|
192
|
+
queryFn: mockQuery,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const parsed = JSON.parse(result);
|
|
196
|
+
expect(parsed.entries).toHaveLength(2);
|
|
197
|
+
expect(parsed.exportedAt).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('exports as CSV', async () => {
|
|
201
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
202
|
+
|
|
203
|
+
const result = await exportAuditLog({
|
|
204
|
+
format: 'csv',
|
|
205
|
+
queryFn: mockQuery,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(result).toContain('id,event,branch,user,timestamp');
|
|
209
|
+
expect(result).toContain('deployment_started');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('exports as SIEM format (CEF)', async () => {
|
|
213
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
214
|
+
|
|
215
|
+
const result = await exportAuditLog({
|
|
216
|
+
format: 'cef',
|
|
217
|
+
queryFn: mockQuery,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(result).toContain('CEF:0');
|
|
221
|
+
expect(result).toContain('deployment_started');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('includes metadata in export', async () => {
|
|
225
|
+
const mockQuery = vi.fn().mockResolvedValue(mockEntries);
|
|
226
|
+
|
|
227
|
+
const result = await exportAuditLog({
|
|
228
|
+
format: 'json',
|
|
229
|
+
includeMetadata: true,
|
|
230
|
+
queryFn: mockQuery,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const parsed = JSON.parse(result);
|
|
234
|
+
expect(parsed.totalEntries).toBe(2);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('createDeploymentAudit', () => {
|
|
239
|
+
it('creates audit logger', () => {
|
|
240
|
+
const audit = createDeploymentAudit();
|
|
241
|
+
expect(audit.log).toBeDefined();
|
|
242
|
+
expect(audit.query).toBeDefined();
|
|
243
|
+
expect(audit.export).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('logs events to storage', async () => {
|
|
247
|
+
const mockStorage = {
|
|
248
|
+
write: vi.fn().mockResolvedValue(true),
|
|
249
|
+
read: vi.fn().mockResolvedValue([]),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const audit = createDeploymentAudit({ storage: mockStorage });
|
|
253
|
+
|
|
254
|
+
await audit.log({
|
|
255
|
+
event: 'deployment_started',
|
|
256
|
+
deploymentId: 'deploy-123',
|
|
257
|
+
branch: 'main',
|
|
258
|
+
user: 'alice',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(mockStorage.write).toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('maintains checksum chain', async () => {
|
|
265
|
+
const audit = createDeploymentAudit();
|
|
266
|
+
|
|
267
|
+
const entry1 = await audit.log({
|
|
268
|
+
event: 'deployment_started',
|
|
269
|
+
deploymentId: 'deploy-123',
|
|
270
|
+
branch: 'main',
|
|
271
|
+
user: 'alice',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const entry2 = await audit.log({
|
|
275
|
+
event: 'deployment_completed',
|
|
276
|
+
deploymentId: 'deploy-123',
|
|
277
|
+
branch: 'main',
|
|
278
|
+
user: 'alice',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(entry2.previousChecksum).toBe(entry1.checksum);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('verifies log integrity', async () => {
|
|
285
|
+
const audit = createDeploymentAudit();
|
|
286
|
+
|
|
287
|
+
await audit.log({ event: 'deployment_started', deploymentId: 'd1', branch: 'main', user: 'alice' });
|
|
288
|
+
await audit.log({ event: 'deployment_completed', deploymentId: 'd1', branch: 'main', user: 'alice' });
|
|
289
|
+
|
|
290
|
+
const integrity = await audit.verifyIntegrity();
|
|
291
|
+
expect(integrity.valid).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('detects tampered entries', async () => {
|
|
295
|
+
const audit = createDeploymentAudit();
|
|
296
|
+
|
|
297
|
+
await audit.log({ event: 'deployment_started', deploymentId: 'd1', branch: 'main', user: 'alice' });
|
|
298
|
+
|
|
299
|
+
// Simulate tampering
|
|
300
|
+
audit._tamperEntry(0, { user: 'mallory' });
|
|
301
|
+
|
|
302
|
+
const integrity = await audit.verifyIntegrity();
|
|
303
|
+
expect(integrity.valid).toBe(false);
|
|
304
|
+
expect(integrity.tamperedEntries).toHaveLength(1);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Executor
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment orchestration with support for multiple strategies:
|
|
5
|
+
* - Rolling: Gradual replacement of instances
|
|
6
|
+
* - Blue-Green: Deploy to inactive slot, then switch traffic
|
|
7
|
+
* - Canary: Gradual traffic shift to new version
|
|
8
|
+
* - Recreate: Stop old version, start new version
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deployment state constants
|
|
15
|
+
*/
|
|
16
|
+
export const DEPLOYMENT_STATES = {
|
|
17
|
+
PENDING: 'pending',
|
|
18
|
+
BUILDING: 'building',
|
|
19
|
+
DEPLOYING: 'deploying',
|
|
20
|
+
HEALTH_CHECK: 'health_check',
|
|
21
|
+
SWITCHING: 'switching',
|
|
22
|
+
COMPLETED: 'completed',
|
|
23
|
+
FAILED: 'failed',
|
|
24
|
+
ROLLED_BACK: 'rolled_back',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Deployment strategy constants
|
|
29
|
+
*/
|
|
30
|
+
export const DEPLOYMENT_STRATEGIES = {
|
|
31
|
+
ROLLING: 'rolling',
|
|
32
|
+
BLUE_GREEN: 'blue-green',
|
|
33
|
+
CANARY: 'canary',
|
|
34
|
+
RECREATE: 'recreate',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a new deployment object
|
|
39
|
+
* @param {Object} options - Deployment options
|
|
40
|
+
* @param {string} options.branch - Git branch name
|
|
41
|
+
* @param {string} options.commitSha - Git commit SHA
|
|
42
|
+
* @param {string} [options.strategy='rolling'] - Deployment strategy
|
|
43
|
+
* @returns {Object} Deployment object
|
|
44
|
+
*/
|
|
45
|
+
export function createDeployment(options) {
|
|
46
|
+
const { branch, commitSha, strategy = DEPLOYMENT_STRATEGIES.ROLLING } = options;
|
|
47
|
+
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: randomUUID(),
|
|
52
|
+
branch,
|
|
53
|
+
commitSha,
|
|
54
|
+
strategy,
|
|
55
|
+
state: DEPLOYMENT_STATES.PENDING,
|
|
56
|
+
createdAt: now,
|
|
57
|
+
stateHistory: [{ state: DEPLOYMENT_STATES.PENDING, timestamp: now }],
|
|
58
|
+
error: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Transition deployment to a new state
|
|
64
|
+
* @param {Object} deployment - Deployment object
|
|
65
|
+
* @param {string} newState - New state
|
|
66
|
+
* @param {Function} [onStateChange] - State change callback
|
|
67
|
+
* @returns {Object} Updated deployment
|
|
68
|
+
*/
|
|
69
|
+
function transitionState(deployment, newState, onStateChange) {
|
|
70
|
+
deployment.state = newState;
|
|
71
|
+
deployment.stateHistory.push({
|
|
72
|
+
state: newState,
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (onStateChange) {
|
|
77
|
+
// Pass a snapshot to preserve state at time of callback
|
|
78
|
+
onStateChange({ ...deployment, stateHistory: [...deployment.stateHistory] });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return deployment;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute a deployment
|
|
86
|
+
* @param {Object} deployment - Deployment object
|
|
87
|
+
* @param {Object} options - Execution options
|
|
88
|
+
* @param {Function} options.build - Build function
|
|
89
|
+
* @param {Function} [options.deploy] - Deploy function
|
|
90
|
+
* @param {Function} [options.healthCheck] - Health check function
|
|
91
|
+
* @param {Function} [options.switchTraffic] - Traffic switch function (for blue-green)
|
|
92
|
+
* @param {Function} [options.onStateChange] - State change callback
|
|
93
|
+
* @returns {Promise<Object>} Updated deployment
|
|
94
|
+
*/
|
|
95
|
+
export async function executeDeployment(deployment, options) {
|
|
96
|
+
const { build, deploy, healthCheck, switchTraffic: switchTrafficFn, onStateChange } = options;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Building phase
|
|
100
|
+
transitionState(deployment, DEPLOYMENT_STATES.BUILDING, onStateChange);
|
|
101
|
+
const buildResult = await build(deployment);
|
|
102
|
+
|
|
103
|
+
if (!buildResult.success) {
|
|
104
|
+
deployment.error = buildResult.error || 'Build failed';
|
|
105
|
+
return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Deploying phase
|
|
109
|
+
if (deploy) {
|
|
110
|
+
transitionState(deployment, DEPLOYMENT_STATES.DEPLOYING, onStateChange);
|
|
111
|
+
const deployResult = await deploy(deployment);
|
|
112
|
+
|
|
113
|
+
if (!deployResult.success) {
|
|
114
|
+
deployment.error = deployResult.error || 'Deploy failed';
|
|
115
|
+
return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
deployment.slot = deployResult.slot;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Health check phase
|
|
122
|
+
if (healthCheck) {
|
|
123
|
+
transitionState(deployment, DEPLOYMENT_STATES.HEALTH_CHECK, onStateChange);
|
|
124
|
+
const healthResult = await healthCheck(deployment);
|
|
125
|
+
|
|
126
|
+
if (!healthResult.healthy) {
|
|
127
|
+
deployment.error = healthResult.reason || 'Health check failed';
|
|
128
|
+
return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Traffic switching phase (for blue-green deployments)
|
|
133
|
+
if (deployment.strategy === DEPLOYMENT_STRATEGIES.BLUE_GREEN && switchTrafficFn) {
|
|
134
|
+
transitionState(deployment, DEPLOYMENT_STATES.SWITCHING, onStateChange);
|
|
135
|
+
const switchResult = await switchTrafficFn(deployment);
|
|
136
|
+
|
|
137
|
+
if (!switchResult.success) {
|
|
138
|
+
deployment.error = switchResult.error || 'Traffic switch failed';
|
|
139
|
+
return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Completed
|
|
144
|
+
return transitionState(deployment, DEPLOYMENT_STATES.COMPLETED, onStateChange);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
deployment.error = error.message;
|
|
147
|
+
return transitionState(deployment, DEPLOYMENT_STATES.FAILED, onStateChange);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Run health checks against multiple endpoints
|
|
153
|
+
* @param {Object} options - Health check options
|
|
154
|
+
* @param {string[]} options.endpoints - URLs to check
|
|
155
|
+
* @param {Function} options.fetch - Fetch function
|
|
156
|
+
* @param {number} [options.retries=1] - Number of retry attempts
|
|
157
|
+
* @param {number} [options.retryDelay=1000] - Delay between retries in ms
|
|
158
|
+
* @param {number} [options.timeout=30000] - Timeout in ms
|
|
159
|
+
* @returns {Promise<Object>} Health check result
|
|
160
|
+
*/
|
|
161
|
+
export async function runHealthChecks(options) {
|
|
162
|
+
const { endpoints, fetch: fetchFn, retries = 1, retryDelay = 1000, timeout = 30000 } = options;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check a single endpoint with timeout
|
|
166
|
+
*/
|
|
167
|
+
async function checkEndpoint(url) {
|
|
168
|
+
return Promise.race([
|
|
169
|
+
fetchFn(url),
|
|
170
|
+
new Promise((_, reject) =>
|
|
171
|
+
setTimeout(() => reject(new Error('timeout')), timeout)
|
|
172
|
+
),
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check endpoint with retries
|
|
178
|
+
*/
|
|
179
|
+
async function checkWithRetries(url, attemptsLeft) {
|
|
180
|
+
try {
|
|
181
|
+
const response = await checkEndpoint(url);
|
|
182
|
+
if (response.ok) {
|
|
183
|
+
return { healthy: true, url };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (attemptsLeft > 1) {
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
188
|
+
return checkWithRetries(url, attemptsLeft - 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { healthy: false, url, reason: `Status ${response.status}` };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (attemptsLeft > 1) {
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
195
|
+
return checkWithRetries(url, attemptsLeft - 1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { healthy: false, url, reason: error.message };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const results = await Promise.all(
|
|
204
|
+
endpoints.map((endpoint) => checkWithRetries(endpoint, retries))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const unhealthy = results.filter((r) => !r.healthy);
|
|
208
|
+
|
|
209
|
+
if (unhealthy.length > 0) {
|
|
210
|
+
return {
|
|
211
|
+
healthy: false,
|
|
212
|
+
reason: unhealthy.map((u) => `${u.url}: ${u.reason}`).join(', '),
|
|
213
|
+
results,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { healthy: true, results };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return { healthy: false, reason: error.message };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Switch traffic between deployment slots
|
|
225
|
+
* @param {Object} options - Switch options
|
|
226
|
+
* @param {string} options.fromSlot - Source slot
|
|
227
|
+
* @param {string} options.toSlot - Target slot
|
|
228
|
+
* @param {Function} options.switchFn - Switch function
|
|
229
|
+
* @returns {Promise<Object>} Switch result
|
|
230
|
+
*/
|
|
231
|
+
export async function switchTraffic(options) {
|
|
232
|
+
const { fromSlot, toSlot, switchFn } = options;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const result = await switchFn(fromSlot, toSlot);
|
|
236
|
+
return { success: true, ...result };
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return { success: false, error: error.message };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Cleanup old deployment resources
|
|
244
|
+
* @param {Object} options - Cleanup options
|
|
245
|
+
* @param {string} options.slot - Slot to cleanup
|
|
246
|
+
* @param {Function} options.cleanupFn - Cleanup function
|
|
247
|
+
* @returns {Promise<Object>} Cleanup result
|
|
248
|
+
*/
|
|
249
|
+
export async function cleanupOldDeployment(options) {
|
|
250
|
+
const { slot, cleanupFn } = options;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await cleanupFn(slot);
|
|
254
|
+
return { success: true };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// Log warning but don't throw - cleanup failures are not critical
|
|
257
|
+
console.warn(`Cleanup failed for slot ${slot}:`, error.message);
|
|
258
|
+
return { success: false, error: error.message };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create a deployment executor instance
|
|
264
|
+
* @param {Object} [config={}] - Executor configuration
|
|
265
|
+
* @param {Function} [config.build] - Build function
|
|
266
|
+
* @param {Function} [config.deploy] - Deploy function
|
|
267
|
+
* @param {Function} [config.healthCheck] - Health check function
|
|
268
|
+
* @param {Function} [config.switchTraffic] - Traffic switch function
|
|
269
|
+
* @returns {Object} Deployment executor
|
|
270
|
+
*/
|
|
271
|
+
export function createDeploymentExecutor(config = {}) {
|
|
272
|
+
const deployments = new Map();
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
/**
|
|
276
|
+
* Execute an existing deployment
|
|
277
|
+
* @param {Object} deployment - Deployment to execute
|
|
278
|
+
* @param {Object} [options] - Additional options
|
|
279
|
+
* @returns {Promise<Object>} Execution result
|
|
280
|
+
*/
|
|
281
|
+
async execute(deployment, options = {}) {
|
|
282
|
+
return executeDeployment(deployment, { ...config, ...options });
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Start a new deployment
|
|
287
|
+
* @param {Object} options - Deployment options
|
|
288
|
+
* @returns {Promise<Object>} Created and executed deployment
|
|
289
|
+
*/
|
|
290
|
+
async start(options) {
|
|
291
|
+
const deployment = createDeployment(options);
|
|
292
|
+
deployments.set(deployment.id, deployment);
|
|
293
|
+
|
|
294
|
+
// Execute in background, don't await
|
|
295
|
+
executeDeployment(deployment, config).catch((error) => {
|
|
296
|
+
deployment.error = error.message;
|
|
297
|
+
deployment.state = DEPLOYMENT_STATES.FAILED;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return deployment;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get a deployment by ID
|
|
305
|
+
* @param {string} id - Deployment ID
|
|
306
|
+
* @returns {Object|undefined} Deployment or undefined
|
|
307
|
+
*/
|
|
308
|
+
getDeployment(id) {
|
|
309
|
+
return deployments.get(id);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* List all deployments
|
|
314
|
+
* @returns {Object[]} Array of deployments
|
|
315
|
+
*/
|
|
316
|
+
list() {
|
|
317
|
+
return Array.from(deployments.values());
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Cancel a deployment
|
|
322
|
+
* @param {string} id - Deployment ID
|
|
323
|
+
* @returns {boolean} True if cancelled
|
|
324
|
+
*/
|
|
325
|
+
cancel(id) {
|
|
326
|
+
const deployment = deployments.get(id);
|
|
327
|
+
if (deployment && deployment.state !== DEPLOYMENT_STATES.COMPLETED) {
|
|
328
|
+
deployment.state = DEPLOYMENT_STATES.FAILED;
|
|
329
|
+
deployment.error = 'Cancelled';
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|