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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Approval Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createApprovalRequest,
|
|
7
|
+
verifyApproval,
|
|
8
|
+
verify2FA,
|
|
9
|
+
getApprovers,
|
|
10
|
+
checkApprovalStatus,
|
|
11
|
+
APPROVAL_STATUS,
|
|
12
|
+
createDeploymentApproval,
|
|
13
|
+
} from './deployment-approval.js';
|
|
14
|
+
|
|
15
|
+
describe('deployment-approval', () => {
|
|
16
|
+
describe('APPROVAL_STATUS', () => {
|
|
17
|
+
it('defines all status constants', () => {
|
|
18
|
+
expect(APPROVAL_STATUS.PENDING).toBe('pending');
|
|
19
|
+
expect(APPROVAL_STATUS.APPROVED).toBe('approved');
|
|
20
|
+
expect(APPROVAL_STATUS.REJECTED).toBe('rejected');
|
|
21
|
+
expect(APPROVAL_STATUS.EXPIRED).toBe('expired');
|
|
22
|
+
expect(APPROVAL_STATUS.CANCELLED).toBe('cancelled');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('createApprovalRequest', () => {
|
|
27
|
+
it('creates approval request with required fields', () => {
|
|
28
|
+
const request = createApprovalRequest({
|
|
29
|
+
branch: 'main',
|
|
30
|
+
requestedBy: 'alice',
|
|
31
|
+
tier: 'stable',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(request.id).toBeDefined();
|
|
35
|
+
expect(request.branch).toBe('main');
|
|
36
|
+
expect(request.requestedBy).toBe('alice');
|
|
37
|
+
expect(request.tier).toBe('stable');
|
|
38
|
+
expect(request.status).toBe('pending');
|
|
39
|
+
expect(request.createdAt).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('sets expiry time', () => {
|
|
43
|
+
const request = createApprovalRequest({
|
|
44
|
+
branch: 'main',
|
|
45
|
+
requestedBy: 'alice',
|
|
46
|
+
tier: 'stable',
|
|
47
|
+
expiresInMinutes: 30,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const expectedExpiry = new Date(request.createdAt);
|
|
51
|
+
expectedExpiry.setMinutes(expectedExpiry.getMinutes() + 30);
|
|
52
|
+
expect(new Date(request.expiresAt).getTime()).toBeCloseTo(expectedExpiry.getTime(), -3);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('defaults to 60 minute expiry', () => {
|
|
56
|
+
const request = createApprovalRequest({
|
|
57
|
+
branch: 'main',
|
|
58
|
+
requestedBy: 'alice',
|
|
59
|
+
tier: 'stable',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const created = new Date(request.createdAt);
|
|
63
|
+
const expires = new Date(request.expiresAt);
|
|
64
|
+
const diffMinutes = (expires - created) / 60000;
|
|
65
|
+
expect(diffMinutes).toBe(60);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('includes commit info when provided', () => {
|
|
69
|
+
const request = createApprovalRequest({
|
|
70
|
+
branch: 'main',
|
|
71
|
+
requestedBy: 'alice',
|
|
72
|
+
tier: 'stable',
|
|
73
|
+
commitSha: 'abc123',
|
|
74
|
+
commitMessage: 'Fix bug',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(request.commitSha).toBe('abc123');
|
|
78
|
+
expect(request.commitMessage).toBe('Fix bug');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('verifyApproval', () => {
|
|
83
|
+
it('approves with valid approver', async () => {
|
|
84
|
+
const request = createApprovalRequest({
|
|
85
|
+
branch: 'main',
|
|
86
|
+
requestedBy: 'alice',
|
|
87
|
+
tier: 'stable',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await verifyApproval(request, {
|
|
91
|
+
approver: 'bob',
|
|
92
|
+
approvers: ['bob', 'carol'],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(result.status).toBe('approved');
|
|
96
|
+
expect(result.approvedBy).toBe('bob');
|
|
97
|
+
expect(result.approvedAt).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejects if not in approvers list', async () => {
|
|
101
|
+
const request = createApprovalRequest({
|
|
102
|
+
branch: 'main',
|
|
103
|
+
requestedBy: 'alice',
|
|
104
|
+
tier: 'stable',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const result = await verifyApproval(request, {
|
|
108
|
+
approver: 'dave',
|
|
109
|
+
approvers: ['bob', 'carol'],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.status).toBe('rejected');
|
|
113
|
+
expect(result.reason).toContain('not authorized');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects expired requests', async () => {
|
|
117
|
+
const request = createApprovalRequest({
|
|
118
|
+
branch: 'main',
|
|
119
|
+
requestedBy: 'alice',
|
|
120
|
+
tier: 'stable',
|
|
121
|
+
expiresInMinutes: -1, // Already expired
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await verifyApproval(request, {
|
|
125
|
+
approver: 'bob',
|
|
126
|
+
approvers: ['bob'],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.status).toBe('expired');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('prevents self-approval', async () => {
|
|
133
|
+
const request = createApprovalRequest({
|
|
134
|
+
branch: 'main',
|
|
135
|
+
requestedBy: 'alice',
|
|
136
|
+
tier: 'stable',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = await verifyApproval(request, {
|
|
140
|
+
approver: 'alice',
|
|
141
|
+
approvers: ['alice', 'bob'],
|
|
142
|
+
allowSelfApproval: false,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.status).toBe('rejected');
|
|
146
|
+
expect(result.reason).toContain('self-approval');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('allows self-approval when configured', async () => {
|
|
150
|
+
const request = createApprovalRequest({
|
|
151
|
+
branch: 'main',
|
|
152
|
+
requestedBy: 'alice',
|
|
153
|
+
tier: 'stable',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = await verifyApproval(request, {
|
|
157
|
+
approver: 'alice',
|
|
158
|
+
approvers: ['alice'],
|
|
159
|
+
allowSelfApproval: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.status).toBe('approved');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('verify2FA', () => {
|
|
167
|
+
it('verifies valid TOTP code', async () => {
|
|
168
|
+
const mockVerifier = vi.fn().mockResolvedValue(true);
|
|
169
|
+
const result = await verify2FA('alice', '123456', { verifier: mockVerifier });
|
|
170
|
+
|
|
171
|
+
expect(result.verified).toBe(true);
|
|
172
|
+
expect(mockVerifier).toHaveBeenCalledWith('alice', '123456');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('rejects invalid TOTP code', async () => {
|
|
176
|
+
const mockVerifier = vi.fn().mockResolvedValue(false);
|
|
177
|
+
const result = await verify2FA('alice', '000000', { verifier: mockVerifier });
|
|
178
|
+
|
|
179
|
+
expect(result.verified).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('handles verification errors', async () => {
|
|
183
|
+
const mockVerifier = vi.fn().mockRejectedValue(new Error('Service unavailable'));
|
|
184
|
+
const result = await verify2FA('alice', '123456', { verifier: mockVerifier });
|
|
185
|
+
|
|
186
|
+
expect(result.verified).toBe(false);
|
|
187
|
+
expect(result.error).toContain('Service unavailable');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('validates code format', async () => {
|
|
191
|
+
const result = await verify2FA('alice', 'abc', {});
|
|
192
|
+
expect(result.verified).toBe(false);
|
|
193
|
+
expect(result.error).toContain('Invalid code format');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('getApprovers', () => {
|
|
198
|
+
it('returns approvers from config', () => {
|
|
199
|
+
const config = {
|
|
200
|
+
deployment: {
|
|
201
|
+
approvers: ['alice', 'bob'],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const approvers = getApprovers(config);
|
|
205
|
+
expect(approvers).toEqual(['alice', 'bob']);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('returns empty array when not configured', () => {
|
|
209
|
+
const approvers = getApprovers({});
|
|
210
|
+
expect(approvers).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('filters by tier when specified', () => {
|
|
214
|
+
const config = {
|
|
215
|
+
deployment: {
|
|
216
|
+
approvers: {
|
|
217
|
+
stable: ['alice', 'bob'],
|
|
218
|
+
dev: ['carol'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
expect(getApprovers(config, 'stable')).toEqual(['alice', 'bob']);
|
|
223
|
+
expect(getApprovers(config, 'dev')).toEqual(['carol']);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('checkApprovalStatus', () => {
|
|
228
|
+
it('returns pending for fresh request', () => {
|
|
229
|
+
const request = createApprovalRequest({
|
|
230
|
+
branch: 'main',
|
|
231
|
+
requestedBy: 'alice',
|
|
232
|
+
tier: 'stable',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const status = checkApprovalStatus(request);
|
|
236
|
+
expect(status).toBe('pending');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns expired for old request', () => {
|
|
240
|
+
const request = createApprovalRequest({
|
|
241
|
+
branch: 'main',
|
|
242
|
+
requestedBy: 'alice',
|
|
243
|
+
tier: 'stable',
|
|
244
|
+
expiresInMinutes: -1,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const status = checkApprovalStatus(request);
|
|
248
|
+
expect(status).toBe('expired');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('returns approved when approved', () => {
|
|
252
|
+
const request = createApprovalRequest({
|
|
253
|
+
branch: 'main',
|
|
254
|
+
requestedBy: 'alice',
|
|
255
|
+
tier: 'stable',
|
|
256
|
+
});
|
|
257
|
+
request.status = 'approved';
|
|
258
|
+
request.approvedBy = 'bob';
|
|
259
|
+
|
|
260
|
+
const status = checkApprovalStatus(request);
|
|
261
|
+
expect(status).toBe('approved');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('createDeploymentApproval', () => {
|
|
266
|
+
it('creates approval manager', () => {
|
|
267
|
+
const approval = createDeploymentApproval();
|
|
268
|
+
expect(approval.createRequest).toBeDefined();
|
|
269
|
+
expect(approval.approve).toBeDefined();
|
|
270
|
+
expect(approval.reject).toBeDefined();
|
|
271
|
+
expect(approval.checkStatus).toBeDefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('stores pending requests', async () => {
|
|
275
|
+
const approval = createDeploymentApproval();
|
|
276
|
+
const request = await approval.createRequest({
|
|
277
|
+
branch: 'main',
|
|
278
|
+
requestedBy: 'alice',
|
|
279
|
+
tier: 'stable',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const retrieved = approval.getRequest(request.id);
|
|
283
|
+
expect(retrieved).toBeDefined();
|
|
284
|
+
expect(retrieved.branch).toBe('main');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('lists pending requests', async () => {
|
|
288
|
+
const approval = createDeploymentApproval();
|
|
289
|
+
await approval.createRequest({ branch: 'main', requestedBy: 'alice', tier: 'stable' });
|
|
290
|
+
await approval.createRequest({ branch: 'release/1.0', requestedBy: 'bob', tier: 'stable' });
|
|
291
|
+
|
|
292
|
+
const pending = approval.listPending();
|
|
293
|
+
expect(pending).toHaveLength(2);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Audit Module
|
|
3
|
+
*
|
|
4
|
+
* Provides audit logging for deployment events with:
|
|
5
|
+
* - Immutable event logging
|
|
6
|
+
* - Checksum chain for integrity verification
|
|
7
|
+
* - Query and export capabilities (JSON, CSV, CEF)
|
|
8
|
+
*/
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Audit event type constants
|
|
13
|
+
*/
|
|
14
|
+
export const AUDIT_EVENTS = {
|
|
15
|
+
DEPLOYMENT_STARTED: 'deployment_started',
|
|
16
|
+
DEPLOYMENT_COMPLETED: 'deployment_completed',
|
|
17
|
+
DEPLOYMENT_FAILED: 'deployment_failed',
|
|
18
|
+
APPROVAL_REQUESTED: 'approval_requested',
|
|
19
|
+
APPROVAL_GRANTED: 'approval_granted',
|
|
20
|
+
APPROVAL_DENIED: 'approval_denied',
|
|
21
|
+
ROLLBACK_TRIGGERED: 'rollback_triggered',
|
|
22
|
+
ROLLBACK_COMPLETED: 'rollback_completed',
|
|
23
|
+
SECURITY_GATE_PASSED: 'security_gate_passed',
|
|
24
|
+
SECURITY_GATE_FAILED: 'security_gate_failed',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a unique ID
|
|
29
|
+
* @returns {string} Unique identifier
|
|
30
|
+
*/
|
|
31
|
+
function generateId() {
|
|
32
|
+
return `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate SHA-256 checksum of an entry
|
|
37
|
+
* @param {object} entry - The entry to checksum
|
|
38
|
+
* @param {string} [previousChecksum] - Previous entry's checksum
|
|
39
|
+
* @returns {string} SHA-256 hex digest
|
|
40
|
+
*/
|
|
41
|
+
function calculateChecksum(entry, previousChecksum = '') {
|
|
42
|
+
const data = JSON.stringify({
|
|
43
|
+
id: entry.id,
|
|
44
|
+
event: entry.event,
|
|
45
|
+
deploymentId: entry.deploymentId,
|
|
46
|
+
branch: entry.branch,
|
|
47
|
+
user: entry.user,
|
|
48
|
+
timestamp: entry.timestamp,
|
|
49
|
+
metadata: entry.metadata,
|
|
50
|
+
previousChecksum,
|
|
51
|
+
});
|
|
52
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Log a deployment event
|
|
57
|
+
* @param {object} options - Event options
|
|
58
|
+
* @param {string} options.event - Event type
|
|
59
|
+
* @param {string} options.deploymentId - Deployment identifier
|
|
60
|
+
* @param {string} options.branch - Branch name
|
|
61
|
+
* @param {string} options.user - User who triggered the event
|
|
62
|
+
* @param {object} [options.metadata] - Additional metadata
|
|
63
|
+
* @param {string} [options.previousChecksum] - Previous entry's checksum
|
|
64
|
+
* @param {function} [options.writeFn] - Function to write the entry
|
|
65
|
+
* @returns {Promise<object>} The logged entry
|
|
66
|
+
*/
|
|
67
|
+
export async function logDeploymentEvent(options) {
|
|
68
|
+
const {
|
|
69
|
+
event,
|
|
70
|
+
deploymentId,
|
|
71
|
+
branch,
|
|
72
|
+
user,
|
|
73
|
+
metadata = {},
|
|
74
|
+
previousChecksum = null,
|
|
75
|
+
writeFn,
|
|
76
|
+
} = options;
|
|
77
|
+
|
|
78
|
+
const entry = {
|
|
79
|
+
id: generateId(),
|
|
80
|
+
event,
|
|
81
|
+
deploymentId,
|
|
82
|
+
branch,
|
|
83
|
+
user,
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
metadata,
|
|
86
|
+
previousChecksum,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
entry.checksum = calculateChecksum(entry, previousChecksum);
|
|
90
|
+
|
|
91
|
+
if (writeFn) {
|
|
92
|
+
await writeFn(entry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Query the audit log
|
|
100
|
+
* @param {object} options - Query options
|
|
101
|
+
* @param {string} [options.startDate] - Start date for range filter
|
|
102
|
+
* @param {string} [options.endDate] - End date for range filter
|
|
103
|
+
* @param {string} [options.user] - Filter by user
|
|
104
|
+
* @param {string} [options.branch] - Filter by branch
|
|
105
|
+
* @param {string} [options.event] - Filter by event type
|
|
106
|
+
* @param {number} [options.limit] - Maximum entries to return
|
|
107
|
+
* @param {number} [options.offset] - Offset for pagination
|
|
108
|
+
* @param {function} options.queryFn - Function to fetch entries
|
|
109
|
+
* @returns {Promise<object[]>} Matching entries
|
|
110
|
+
*/
|
|
111
|
+
export async function queryAuditLog(options) {
|
|
112
|
+
const {
|
|
113
|
+
startDate,
|
|
114
|
+
endDate,
|
|
115
|
+
user,
|
|
116
|
+
branch,
|
|
117
|
+
event,
|
|
118
|
+
limit,
|
|
119
|
+
offset = 0,
|
|
120
|
+
queryFn,
|
|
121
|
+
} = options;
|
|
122
|
+
|
|
123
|
+
let entries = await queryFn();
|
|
124
|
+
|
|
125
|
+
// Apply filters
|
|
126
|
+
if (startDate || endDate) {
|
|
127
|
+
entries = entries.filter((entry) => {
|
|
128
|
+
const ts = new Date(entry.timestamp);
|
|
129
|
+
if (startDate && ts < new Date(startDate)) return false;
|
|
130
|
+
if (endDate && ts > new Date(endDate)) return false;
|
|
131
|
+
return true;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (user) {
|
|
136
|
+
entries = entries.filter((entry) => entry.user === user);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (branch) {
|
|
140
|
+
entries = entries.filter((entry) => entry.branch === branch);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (event) {
|
|
144
|
+
entries = entries.filter((entry) => entry.event === event);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply pagination
|
|
148
|
+
if (offset > 0) {
|
|
149
|
+
entries = entries.slice(offset);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (limit !== undefined) {
|
|
153
|
+
entries = entries.slice(0, limit);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Export audit log in various formats
|
|
161
|
+
* @param {object} options - Export options
|
|
162
|
+
* @param {string} options.format - Export format (json, csv, cef)
|
|
163
|
+
* @param {boolean} [options.includeMetadata] - Include export metadata
|
|
164
|
+
* @param {function} options.queryFn - Function to fetch entries
|
|
165
|
+
* @returns {Promise<string>} Exported data
|
|
166
|
+
*/
|
|
167
|
+
export async function exportAuditLog(options) {
|
|
168
|
+
const { format, includeMetadata = false, queryFn, ...queryOptions } = options;
|
|
169
|
+
|
|
170
|
+
const entries = await queryFn(queryOptions);
|
|
171
|
+
|
|
172
|
+
switch (format) {
|
|
173
|
+
case 'json':
|
|
174
|
+
return exportAsJson(entries, includeMetadata);
|
|
175
|
+
case 'csv':
|
|
176
|
+
return exportAsCsv(entries);
|
|
177
|
+
case 'cef':
|
|
178
|
+
return exportAsCef(entries);
|
|
179
|
+
default:
|
|
180
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Export entries as JSON
|
|
186
|
+
* @param {object[]} entries - Entries to export
|
|
187
|
+
* @param {boolean} includeMetadata - Include export metadata
|
|
188
|
+
* @returns {string} JSON string
|
|
189
|
+
*/
|
|
190
|
+
function exportAsJson(entries, includeMetadata) {
|
|
191
|
+
const result = {
|
|
192
|
+
entries,
|
|
193
|
+
exportedAt: new Date().toISOString(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (includeMetadata) {
|
|
197
|
+
result.totalEntries = entries.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return JSON.stringify(result, null, 2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Export entries as CSV
|
|
205
|
+
* @param {object[]} entries - Entries to export
|
|
206
|
+
* @returns {string} CSV string
|
|
207
|
+
*/
|
|
208
|
+
function exportAsCsv(entries) {
|
|
209
|
+
const headers = ['id', 'event', 'branch', 'user', 'timestamp'];
|
|
210
|
+
const lines = [headers.join(',')];
|
|
211
|
+
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const row = headers.map((h) => {
|
|
214
|
+
const value = entry[h] || '';
|
|
215
|
+
// Escape quotes and wrap in quotes if contains comma
|
|
216
|
+
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
|
|
217
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
});
|
|
221
|
+
lines.push(row.join(','));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Export entries as CEF (Common Event Format) for SIEM integration
|
|
229
|
+
* @param {object[]} entries - Entries to export
|
|
230
|
+
* @returns {string} CEF formatted string
|
|
231
|
+
*/
|
|
232
|
+
function exportAsCef(entries) {
|
|
233
|
+
const lines = [];
|
|
234
|
+
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
// CEF format: CEF:Version|Device Vendor|Device Product|Device Version|Signature ID|Name|Severity|Extension
|
|
237
|
+
const cefLine = [
|
|
238
|
+
'CEF:0',
|
|
239
|
+
'DeploymentAudit',
|
|
240
|
+
'DeploymentAudit',
|
|
241
|
+
'1.0',
|
|
242
|
+
entry.event,
|
|
243
|
+
entry.event,
|
|
244
|
+
getSeverity(entry.event),
|
|
245
|
+
`branch=${entry.branch} user=${entry.user} deploymentId=${entry.deploymentId || ''} timestamp=${entry.timestamp}`,
|
|
246
|
+
].join('|');
|
|
247
|
+
lines.push(cefLine);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get severity level for CEF format based on event type
|
|
255
|
+
* @param {string} event - Event type
|
|
256
|
+
* @returns {number} Severity (0-10)
|
|
257
|
+
*/
|
|
258
|
+
function getSeverity(event) {
|
|
259
|
+
const severityMap = {
|
|
260
|
+
[AUDIT_EVENTS.DEPLOYMENT_STARTED]: 1,
|
|
261
|
+
[AUDIT_EVENTS.DEPLOYMENT_COMPLETED]: 1,
|
|
262
|
+
[AUDIT_EVENTS.DEPLOYMENT_FAILED]: 7,
|
|
263
|
+
[AUDIT_EVENTS.APPROVAL_REQUESTED]: 3,
|
|
264
|
+
[AUDIT_EVENTS.APPROVAL_GRANTED]: 1,
|
|
265
|
+
[AUDIT_EVENTS.APPROVAL_DENIED]: 5,
|
|
266
|
+
[AUDIT_EVENTS.ROLLBACK_TRIGGERED]: 6,
|
|
267
|
+
[AUDIT_EVENTS.ROLLBACK_COMPLETED]: 3,
|
|
268
|
+
[AUDIT_EVENTS.SECURITY_GATE_PASSED]: 1,
|
|
269
|
+
[AUDIT_EVENTS.SECURITY_GATE_FAILED]: 8,
|
|
270
|
+
};
|
|
271
|
+
return severityMap[event] || 5;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Create a deployment audit instance with storage
|
|
276
|
+
* @param {object} [options] - Configuration options
|
|
277
|
+
* @param {object} [options.storage] - Storage backend with read/write methods
|
|
278
|
+
* @returns {object} Audit instance with log, query, export, verifyIntegrity methods
|
|
279
|
+
*/
|
|
280
|
+
export function createDeploymentAudit(options = {}) {
|
|
281
|
+
const { storage } = options;
|
|
282
|
+
|
|
283
|
+
// In-memory storage if none provided
|
|
284
|
+
let entries = [];
|
|
285
|
+
let lastChecksum = null;
|
|
286
|
+
|
|
287
|
+
const internalStorage = {
|
|
288
|
+
write: async (entry) => {
|
|
289
|
+
entries.push(entry);
|
|
290
|
+
return true;
|
|
291
|
+
},
|
|
292
|
+
read: async () => [...entries],
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const activeStorage = storage || internalStorage;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
/**
|
|
299
|
+
* Log a deployment event
|
|
300
|
+
* @param {object} eventOptions - Event options
|
|
301
|
+
* @returns {Promise<object>} The logged entry
|
|
302
|
+
*/
|
|
303
|
+
async log(eventOptions) {
|
|
304
|
+
const entry = await logDeploymentEvent({
|
|
305
|
+
...eventOptions,
|
|
306
|
+
previousChecksum: lastChecksum,
|
|
307
|
+
writeFn: activeStorage.write,
|
|
308
|
+
});
|
|
309
|
+
lastChecksum = entry.checksum;
|
|
310
|
+
return entry;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Query the audit log
|
|
315
|
+
* @param {object} queryOptions - Query options
|
|
316
|
+
* @returns {Promise<object[]>} Matching entries
|
|
317
|
+
*/
|
|
318
|
+
async query(queryOptions = {}) {
|
|
319
|
+
return queryAuditLog({
|
|
320
|
+
...queryOptions,
|
|
321
|
+
queryFn: activeStorage.read,
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Export the audit log
|
|
327
|
+
* @param {object} exportOptions - Export options
|
|
328
|
+
* @returns {Promise<string>} Exported data
|
|
329
|
+
*/
|
|
330
|
+
async export(exportOptions) {
|
|
331
|
+
return exportAuditLog({
|
|
332
|
+
...exportOptions,
|
|
333
|
+
queryFn: activeStorage.read,
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Verify the integrity of the audit log
|
|
339
|
+
* @returns {Promise<object>} Integrity result with valid flag and tampered entries
|
|
340
|
+
*/
|
|
341
|
+
async verifyIntegrity() {
|
|
342
|
+
const allEntries = await activeStorage.read();
|
|
343
|
+
const tamperedEntries = [];
|
|
344
|
+
let prevChecksum = null;
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < allEntries.length; i++) {
|
|
347
|
+
const entry = allEntries[i];
|
|
348
|
+
const expectedChecksum = calculateChecksum(entry, prevChecksum);
|
|
349
|
+
|
|
350
|
+
if (entry.checksum !== expectedChecksum) {
|
|
351
|
+
tamperedEntries.push({ index: i, entry });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
prevChecksum = entry.checksum;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
valid: tamperedEntries.length === 0,
|
|
359
|
+
tamperedEntries,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Internal method for testing - tamper with an entry
|
|
365
|
+
* @param {number} index - Entry index
|
|
366
|
+
* @param {object} changes - Changes to apply
|
|
367
|
+
*/
|
|
368
|
+
_tamperEntry(index, changes) {
|
|
369
|
+
if (entries[index]) {
|
|
370
|
+
Object.assign(entries[index], changes);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|