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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Encryption at Rest
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a sealed secret
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
* @param {string} options.name - Secret name
|
|
9
|
+
* @param {Object} options.data - Secret data (key-value pairs)
|
|
10
|
+
* @param {string} options.namespace - Target namespace (default: 'default')
|
|
11
|
+
* @param {Function} options.mockSeal - Mock seal function for testing
|
|
12
|
+
* @returns {Object} SealedSecret resource
|
|
13
|
+
*/
|
|
14
|
+
export function generateSealedSecret({ name, data = {}, namespace = 'default', mockSeal } = {}) {
|
|
15
|
+
const encryptedData = {};
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of Object.entries(data)) {
|
|
18
|
+
if (mockSeal) {
|
|
19
|
+
encryptedData[key] = mockSeal(value);
|
|
20
|
+
} else {
|
|
21
|
+
// In production, this would use kubeseal
|
|
22
|
+
encryptedData[key] = Buffer.from(value).toString('base64');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
apiVersion: 'bitnami.com/v1alpha1',
|
|
28
|
+
kind: 'SealedSecret',
|
|
29
|
+
metadata: {
|
|
30
|
+
name,
|
|
31
|
+
namespace
|
|
32
|
+
},
|
|
33
|
+
spec: {
|
|
34
|
+
encryptedData
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Configures encryption at rest
|
|
41
|
+
* @param {Object} options - Configuration options
|
|
42
|
+
* @param {string} options.provider - Encryption provider ('kms', 'aescbc', 'secretbox')
|
|
43
|
+
* @param {string} options.keyId - Key ID (for KMS)
|
|
44
|
+
* @returns {Object} Encryption configuration
|
|
45
|
+
*/
|
|
46
|
+
export function configureEncryptionAtRest({ provider = 'kms', keyId } = {}) {
|
|
47
|
+
const config = {
|
|
48
|
+
kind: 'EncryptionConfiguration',
|
|
49
|
+
apiVersion: 'apiserver.config.k8s.io/v1',
|
|
50
|
+
resources: [{
|
|
51
|
+
resources: ['secrets'],
|
|
52
|
+
providers: []
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (provider === 'kms') {
|
|
57
|
+
config.resources[0].providers.push({
|
|
58
|
+
kms: {
|
|
59
|
+
name: 'kms-provider',
|
|
60
|
+
endpoint: 'unix:///var/run/kms-provider.sock',
|
|
61
|
+
cachesize: 1000,
|
|
62
|
+
timeout: '3s'
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (keyId) {
|
|
66
|
+
config.resources[0].providers[0].kms.keyId = keyId;
|
|
67
|
+
}
|
|
68
|
+
} else if (provider === 'aescbc') {
|
|
69
|
+
config.resources[0].providers.push({
|
|
70
|
+
aescbc: {
|
|
71
|
+
keys: [{
|
|
72
|
+
name: 'key1',
|
|
73
|
+
secret: '<base64-encoded-secret>'
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Always add identity provider as fallback for reading unencrypted secrets
|
|
80
|
+
config.resources[0].providers.push({ identity: {} });
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
providers: config.resources[0].providers,
|
|
84
|
+
config
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generates an ExternalSecret custom resource
|
|
90
|
+
* @param {Object} options - Configuration options
|
|
91
|
+
* @param {string} options.name - Secret name
|
|
92
|
+
* @param {string} options.store - Secret store name
|
|
93
|
+
* @param {Array} options.keys - Keys to fetch from store
|
|
94
|
+
* @param {string} options.namespace - Target namespace (default: 'default')
|
|
95
|
+
* @returns {Object} ExternalSecret resource
|
|
96
|
+
*/
|
|
97
|
+
export function generateExternalSecret({ name, store, keys = [], namespace = 'default' } = {}) {
|
|
98
|
+
return {
|
|
99
|
+
apiVersion: 'external-secrets.io/v1beta1',
|
|
100
|
+
kind: 'ExternalSecret',
|
|
101
|
+
metadata: {
|
|
102
|
+
name,
|
|
103
|
+
namespace
|
|
104
|
+
},
|
|
105
|
+
spec: {
|
|
106
|
+
refreshInterval: '1h',
|
|
107
|
+
secretStoreRef: {
|
|
108
|
+
name: store,
|
|
109
|
+
kind: 'SecretStore'
|
|
110
|
+
},
|
|
111
|
+
target: {
|
|
112
|
+
name,
|
|
113
|
+
creationPolicy: 'Owner'
|
|
114
|
+
},
|
|
115
|
+
data: keys.map(key => ({
|
|
116
|
+
secretKey: key,
|
|
117
|
+
remoteRef: {
|
|
118
|
+
key: name,
|
|
119
|
+
property: key
|
|
120
|
+
}
|
|
121
|
+
}))
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generates Vault integration configuration
|
|
128
|
+
* @param {Object} options - Configuration options
|
|
129
|
+
* @param {string} options.address - Vault server address
|
|
130
|
+
* @param {string} options.role - Vault role
|
|
131
|
+
* @param {string} options.authPath - Auth path (default: 'kubernetes')
|
|
132
|
+
* @returns {string} Vault configuration as string
|
|
133
|
+
*/
|
|
134
|
+
export function generateVaultConfig({ address, role, authPath = 'kubernetes' } = {}) {
|
|
135
|
+
return `
|
|
136
|
+
vault:
|
|
137
|
+
address: "${address}"
|
|
138
|
+
auth:
|
|
139
|
+
method: kubernetes
|
|
140
|
+
path: ${authPath}
|
|
141
|
+
role: ${role}
|
|
142
|
+
secrets:
|
|
143
|
+
path: secret/data
|
|
144
|
+
`.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates a secrets encryption manager
|
|
149
|
+
* @returns {Object} Manager with seal, rotate, and getLog methods
|
|
150
|
+
*/
|
|
151
|
+
export function createSecretsEncryption() {
|
|
152
|
+
const log = [];
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
seal: (options) => {
|
|
156
|
+
log.push(`Sealing secret: ${options.name}`);
|
|
157
|
+
return generateSealedSecret(options);
|
|
158
|
+
},
|
|
159
|
+
rotate: (options) => {
|
|
160
|
+
log.push(`Rotating encryption key`);
|
|
161
|
+
return { rotated: true, timestamp: new Date().toISOString() };
|
|
162
|
+
},
|
|
163
|
+
getLog: () => {
|
|
164
|
+
// Never include sensitive data in logs
|
|
165
|
+
return log.join('\n');
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Encryption Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { generateSealedSecret, configureEncryptionAtRest, generateExternalSecret, generateVaultConfig, createSecretsEncryption } from './secrets-encryption.js';
|
|
6
|
+
|
|
7
|
+
describe('secrets-encryption', () => {
|
|
8
|
+
describe('generateSealedSecret', () => {
|
|
9
|
+
it('generates sealed secret', () => {
|
|
10
|
+
const secret = generateSealedSecret({ name: 'db-creds', data: { password: 'secret' }, mockSeal: vi.fn().mockReturnValue('encrypted') });
|
|
11
|
+
expect(secret.kind).toBe('SealedSecret');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('configureEncryptionAtRest', () => {
|
|
16
|
+
it('configures KMS encryption', () => {
|
|
17
|
+
const config = configureEncryptionAtRest({ provider: 'kms', keyId: 'arn:aws:kms:...' });
|
|
18
|
+
expect(config.providers.some(p => p.kms !== undefined)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('generateExternalSecret', () => {
|
|
23
|
+
it('generates ExternalSecret CR', () => {
|
|
24
|
+
const secret = generateExternalSecret({ name: 'db-creds', store: 'vault', keys: ['password'] });
|
|
25
|
+
expect(secret.kind).toBe('ExternalSecret');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('generateVaultConfig', () => {
|
|
30
|
+
it('generates Vault integration config', () => {
|
|
31
|
+
const config = generateVaultConfig({ address: 'https://vault.example.com', role: 'app' });
|
|
32
|
+
expect(config).toContain('vault.example.com');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('createSecretsEncryption', () => {
|
|
37
|
+
it('creates manager', () => {
|
|
38
|
+
const manager = createSecretsEncryption();
|
|
39
|
+
expect(manager.seal).toBeDefined();
|
|
40
|
+
expect(manager.rotate).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('never logs secret values', () => {
|
|
44
|
+
const manager = createSecretsEncryption();
|
|
45
|
+
const log = manager.getLog();
|
|
46
|
+
expect(log).not.toContain('password');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert Manager
|
|
3
|
+
* Alert routing and notifications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ALERT_SEVERITY = {
|
|
7
|
+
CRITICAL: 'critical',
|
|
8
|
+
WARNING: 'warning',
|
|
9
|
+
INFO: 'info',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a unique alert ID
|
|
14
|
+
* @returns {string} Unique ID
|
|
15
|
+
*/
|
|
16
|
+
function generateId() {
|
|
17
|
+
return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a fingerprint for deduplication
|
|
22
|
+
* @param {Object} alert - Alert object
|
|
23
|
+
* @returns {string} Fingerprint
|
|
24
|
+
*/
|
|
25
|
+
function generateFingerprint(alert) {
|
|
26
|
+
return `${alert.title}_${alert.severity}_${alert.source || 'unknown'}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create an alert
|
|
31
|
+
* @param {Object} options - Alert options
|
|
32
|
+
* @param {string} options.title - Alert title
|
|
33
|
+
* @param {string} options.severity - Alert severity
|
|
34
|
+
* @param {string} options.description - Alert description
|
|
35
|
+
* @param {string} options.source - Alert source
|
|
36
|
+
* @returns {Object} Alert object
|
|
37
|
+
*/
|
|
38
|
+
export function createAlert(options = {}) {
|
|
39
|
+
const { title, severity = ALERT_SEVERITY.INFO, description, source } = options;
|
|
40
|
+
|
|
41
|
+
const alert = {
|
|
42
|
+
id: generateId(),
|
|
43
|
+
title,
|
|
44
|
+
severity,
|
|
45
|
+
description,
|
|
46
|
+
source,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
acknowledged: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
alert.fingerprint = generateFingerprint(alert);
|
|
52
|
+
|
|
53
|
+
return alert;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Route an alert to appropriate channels
|
|
58
|
+
* @param {Object} alert - Alert object
|
|
59
|
+
* @param {Object} rules - Routing rules by severity
|
|
60
|
+
* @returns {Array} List of channels to send to
|
|
61
|
+
*/
|
|
62
|
+
export function routeAlert(alert, rules = {}) {
|
|
63
|
+
const { severity } = alert;
|
|
64
|
+
return rules[severity] || [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Send alert to PagerDuty
|
|
69
|
+
* @param {Object} alert - Alert object
|
|
70
|
+
* @param {Object} options - Configuration options
|
|
71
|
+
* @param {Function} options.post - Post function
|
|
72
|
+
* @param {string} options.routingKey - PagerDuty routing key
|
|
73
|
+
* @returns {Object} Response
|
|
74
|
+
*/
|
|
75
|
+
export async function sendToPagerDuty(alert, options = {}) {
|
|
76
|
+
const { post, routingKey } = options;
|
|
77
|
+
|
|
78
|
+
if (!post) {
|
|
79
|
+
throw new Error('post function required');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payload = {
|
|
83
|
+
routing_key: routingKey,
|
|
84
|
+
event_action: 'trigger',
|
|
85
|
+
dedup_key: alert.fingerprint || alert.id,
|
|
86
|
+
payload: {
|
|
87
|
+
summary: alert.title,
|
|
88
|
+
severity: alert.severity === ALERT_SEVERITY.CRITICAL ? 'critical' : 'warning',
|
|
89
|
+
source: alert.source || 'monitoring',
|
|
90
|
+
timestamp: alert.timestamp,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return post('https://events.pagerduty.com/v2/enqueue', payload);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Send alert to Slack
|
|
99
|
+
* @param {Object} alert - Alert object
|
|
100
|
+
* @param {Object} options - Configuration options
|
|
101
|
+
* @param {Function} options.post - Post function
|
|
102
|
+
* @param {string} options.webhookUrl - Slack webhook URL
|
|
103
|
+
* @returns {Object} Response
|
|
104
|
+
*/
|
|
105
|
+
export async function sendToSlack(alert, options = {}) {
|
|
106
|
+
const { post, webhookUrl } = options;
|
|
107
|
+
|
|
108
|
+
if (!post) {
|
|
109
|
+
throw new Error('post function required');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const severityEmoji = {
|
|
113
|
+
[ALERT_SEVERITY.CRITICAL]: ':rotating_light:',
|
|
114
|
+
[ALERT_SEVERITY.WARNING]: ':warning:',
|
|
115
|
+
[ALERT_SEVERITY.INFO]: ':information_source:',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const payload = {
|
|
119
|
+
text: `${severityEmoji[alert.severity] || ''} ${alert.title}`,
|
|
120
|
+
attachments: [
|
|
121
|
+
{
|
|
122
|
+
color: alert.severity === ALERT_SEVERITY.CRITICAL ? 'danger' : 'warning',
|
|
123
|
+
fields: [
|
|
124
|
+
{ title: 'Severity', value: alert.severity, short: true },
|
|
125
|
+
{ title: 'Time', value: alert.timestamp, short: true },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (alert.description) {
|
|
132
|
+
payload.attachments[0].text = alert.description;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return post(webhookUrl, payload);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deduplicate alerts by fingerprint
|
|
140
|
+
* @param {Array} alerts - Array of alerts
|
|
141
|
+
* @returns {Array} Deduplicated alerts
|
|
142
|
+
*/
|
|
143
|
+
export function deduplicateAlerts(alerts) {
|
|
144
|
+
const seen = new Map();
|
|
145
|
+
|
|
146
|
+
for (const alert of alerts) {
|
|
147
|
+
const key = alert.fingerprint;
|
|
148
|
+
if (!seen.has(key)) {
|
|
149
|
+
seen.set(key, alert);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return Array.from(seen.values());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Acknowledge an alert
|
|
158
|
+
* @param {Object} alert - Alert object
|
|
159
|
+
* @param {Object} options - Acknowledgment options
|
|
160
|
+
* @param {string} options.user - User acknowledging
|
|
161
|
+
* @param {string} options.note - Acknowledgment note
|
|
162
|
+
* @returns {Object} Acknowledged alert
|
|
163
|
+
*/
|
|
164
|
+
export function acknowledgeAlert(alert, options = {}) {
|
|
165
|
+
const { user, note } = options;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...alert,
|
|
169
|
+
acknowledged: true,
|
|
170
|
+
acknowledgedBy: user,
|
|
171
|
+
acknowledgedAt: new Date().toISOString(),
|
|
172
|
+
acknowledgeNote: note,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create an alert manager
|
|
178
|
+
* @param {Object} options - Configuration options
|
|
179
|
+
* @param {Object} options.escalation - Escalation rules
|
|
180
|
+
* @param {Object} options.routing - Routing rules
|
|
181
|
+
* @returns {Object} Alert manager
|
|
182
|
+
*/
|
|
183
|
+
export function createAlertManager(options = {}) {
|
|
184
|
+
const { escalation, routing = {} } = options;
|
|
185
|
+
|
|
186
|
+
const alerts = new Map();
|
|
187
|
+
const config = {
|
|
188
|
+
escalation,
|
|
189
|
+
routing,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
async send(alertOptions) {
|
|
194
|
+
const alert = createAlert(alertOptions);
|
|
195
|
+
alerts.set(alert.id, alert);
|
|
196
|
+
|
|
197
|
+
const routes = routeAlert(alert, config.routing);
|
|
198
|
+
return { alert, routes };
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
acknowledge(alertId, ackOptions) {
|
|
202
|
+
const alert = alerts.get(alertId);
|
|
203
|
+
if (!alert) {
|
|
204
|
+
throw new Error(`Alert ${alertId} not found`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const acked = acknowledgeAlert(alert, ackOptions);
|
|
208
|
+
alerts.set(alertId, acked);
|
|
209
|
+
return acked;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
list(filter = {}) {
|
|
213
|
+
let result = Array.from(alerts.values());
|
|
214
|
+
|
|
215
|
+
if (filter.severity) {
|
|
216
|
+
result = result.filter((a) => a.severity === filter.severity);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (filter.acknowledged !== undefined) {
|
|
220
|
+
result = result.filter((a) => a.acknowledged === filter.acknowledged);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
get(alertId) {
|
|
227
|
+
return alerts.get(alertId);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
configure(newConfig) {
|
|
231
|
+
Object.assign(config, newConfig);
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
getConfig() {
|
|
235
|
+
return { ...config };
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert Manager Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createAlert,
|
|
7
|
+
routeAlert,
|
|
8
|
+
sendToPagerDuty,
|
|
9
|
+
sendToSlack,
|
|
10
|
+
deduplicateAlerts,
|
|
11
|
+
acknowledgeAlert,
|
|
12
|
+
ALERT_SEVERITY,
|
|
13
|
+
createAlertManager,
|
|
14
|
+
} from './alert-manager.js';
|
|
15
|
+
|
|
16
|
+
describe('alert-manager', () => {
|
|
17
|
+
describe('ALERT_SEVERITY', () => {
|
|
18
|
+
it('defines severity constants', () => {
|
|
19
|
+
expect(ALERT_SEVERITY.CRITICAL).toBe('critical');
|
|
20
|
+
expect(ALERT_SEVERITY.WARNING).toBe('warning');
|
|
21
|
+
expect(ALERT_SEVERITY.INFO).toBe('info');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createAlert', () => {
|
|
26
|
+
it('creates alert with required fields', () => {
|
|
27
|
+
const alert = createAlert({ title: 'Test Alert', severity: 'critical' });
|
|
28
|
+
expect(alert.id).toBeDefined();
|
|
29
|
+
expect(alert.title).toBe('Test Alert');
|
|
30
|
+
expect(alert.severity).toBe('critical');
|
|
31
|
+
expect(alert.timestamp).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('routeAlert', () => {
|
|
36
|
+
it('routes critical alerts to PagerDuty', () => {
|
|
37
|
+
const routes = routeAlert({ severity: 'critical' }, {
|
|
38
|
+
critical: ['pagerduty'],
|
|
39
|
+
warning: ['slack'],
|
|
40
|
+
});
|
|
41
|
+
expect(routes).toContain('pagerduty');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('routes warning alerts to Slack', () => {
|
|
45
|
+
const routes = routeAlert({ severity: 'warning' }, {
|
|
46
|
+
critical: ['pagerduty'],
|
|
47
|
+
warning: ['slack'],
|
|
48
|
+
});
|
|
49
|
+
expect(routes).toContain('slack');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('sendToPagerDuty', () => {
|
|
54
|
+
it('sends alert to PagerDuty', async () => {
|
|
55
|
+
const mockPost = vi.fn().mockResolvedValue({ ok: true });
|
|
56
|
+
await sendToPagerDuty({ title: 'Test' }, { post: mockPost, routingKey: 'key' });
|
|
57
|
+
expect(mockPost).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('sendToSlack', () => {
|
|
62
|
+
it('sends alert to Slack', async () => {
|
|
63
|
+
const mockPost = vi.fn().mockResolvedValue({ ok: true });
|
|
64
|
+
await sendToSlack({ title: 'Test' }, { post: mockPost, webhookUrl: 'url' });
|
|
65
|
+
expect(mockPost).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('deduplicateAlerts', () => {
|
|
70
|
+
it('removes duplicate alerts', () => {
|
|
71
|
+
const alerts = [
|
|
72
|
+
{ id: '1', fingerprint: 'abc', title: 'Alert 1' },
|
|
73
|
+
{ id: '2', fingerprint: 'abc', title: 'Alert 1' },
|
|
74
|
+
{ id: '3', fingerprint: 'def', title: 'Alert 2' },
|
|
75
|
+
];
|
|
76
|
+
const deduped = deduplicateAlerts(alerts);
|
|
77
|
+
expect(deduped.length).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('acknowledgeAlert', () => {
|
|
82
|
+
it('marks alert as acknowledged', () => {
|
|
83
|
+
const alert = createAlert({ title: 'Test' });
|
|
84
|
+
const acked = acknowledgeAlert(alert, { user: 'admin' });
|
|
85
|
+
expect(acked.acknowledged).toBe(true);
|
|
86
|
+
expect(acked.acknowledgedBy).toBe('admin');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('createAlertManager', () => {
|
|
91
|
+
it('creates manager with methods', () => {
|
|
92
|
+
const manager = createAlertManager();
|
|
93
|
+
expect(manager.send).toBeDefined();
|
|
94
|
+
expect(manager.acknowledge).toBeDefined();
|
|
95
|
+
expect(manager.list).toBeDefined();
|
|
96
|
+
expect(manager.configure).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('configures escalation rules', () => {
|
|
100
|
+
const manager = createAlertManager({
|
|
101
|
+
escalation: { afterMinutes: 5, to: 'pagerduty' },
|
|
102
|
+
});
|
|
103
|
+
expect(manager.getConfig().escalation).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|