@stacksjs/ts-cloud-core 0.1.1
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/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
SecretsRotationManager,
|
|
4
|
+
secretsRotationManager,
|
|
5
|
+
SecretsManager,
|
|
6
|
+
secretsManager,
|
|
7
|
+
CertificateManager,
|
|
8
|
+
certificateManager,
|
|
9
|
+
SecurityScanningManager,
|
|
10
|
+
securityScanningManager,
|
|
11
|
+
} from '.'
|
|
12
|
+
|
|
13
|
+
describe('Secrets Rotation Manager', () => {
|
|
14
|
+
let manager: SecretsRotationManager
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
manager = new SecretsRotationManager()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('Rotation Configuration', () => {
|
|
21
|
+
it('should enable RDS rotation', () => {
|
|
22
|
+
const rotation = manager.enableRDSRotation({
|
|
23
|
+
secretId: 'db-credentials',
|
|
24
|
+
databaseIdentifier: 'production-db',
|
|
25
|
+
engine: 'postgres',
|
|
26
|
+
rotationDays: 30,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(rotation.id).toContain('rotation')
|
|
30
|
+
expect(rotation.secretType).toBe('rds_credentials')
|
|
31
|
+
expect(rotation.rotationEnabled).toBe(true)
|
|
32
|
+
expect(rotation.rotationDays).toBe(30)
|
|
33
|
+
expect(rotation.nextRotation).toBeDefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should enable API key rotation', () => {
|
|
37
|
+
const rotation = manager.enableAPIKeyRotation({
|
|
38
|
+
secretId: 'api-key-secret',
|
|
39
|
+
rotationDays: 90,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(rotation.secretType).toBe('api_key')
|
|
43
|
+
expect(rotation.rotationDays).toBe(90)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should enable OAuth token rotation', () => {
|
|
47
|
+
const rotation = manager.enableOAuthRotation({
|
|
48
|
+
secretId: 'oauth-token',
|
|
49
|
+
rotationLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:oauth-rotation',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(rotation.secretType).toBe('oauth_token')
|
|
53
|
+
expect(rotation.rotationLambdaArn).toBeDefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should enable SSH key rotation', () => {
|
|
57
|
+
const rotation = manager.enableSSHKeyRotation({
|
|
58
|
+
secretId: 'ssh-key',
|
|
59
|
+
rotationLambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:ssh-rotation',
|
|
60
|
+
rotationDays: 180,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(rotation.secretType).toBe('ssh_key')
|
|
64
|
+
expect(rotation.rotationDays).toBe(180)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('Rotation Execution', () => {
|
|
69
|
+
it('should execute rotation', async () => {
|
|
70
|
+
const rotation = manager.enableAPIKeyRotation({
|
|
71
|
+
secretId: 'test-secret',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const result = await manager.executeRotation(rotation.id)
|
|
75
|
+
|
|
76
|
+
expect(result.success).toBe(true)
|
|
77
|
+
expect(result.secretId).toBe('test-secret')
|
|
78
|
+
expect(result.newVersion).toBeDefined()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should check if rotation needed', () => {
|
|
82
|
+
const rotation = manager.enableAPIKeyRotation({
|
|
83
|
+
secretId: 'test-secret',
|
|
84
|
+
rotationDays: 30,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// New rotation should need rotation (never rotated)
|
|
88
|
+
const needsRotation = manager.needsRotation(rotation.id)
|
|
89
|
+
expect(needsRotation).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should get secrets needing rotation', async () => {
|
|
93
|
+
manager.enableAPIKeyRotation({ secretId: 'secret1' })
|
|
94
|
+
manager.enableAPIKeyRotation({ secretId: 'secret2' })
|
|
95
|
+
|
|
96
|
+
const secrets = manager.getSecretsNeedingRotation()
|
|
97
|
+
|
|
98
|
+
expect(secrets.length).toBeGreaterThan(0)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('Rotation Schedules', () => {
|
|
103
|
+
it('should create rotation schedule', () => {
|
|
104
|
+
const schedule = manager.createSchedule({
|
|
105
|
+
name: 'daily-rotation',
|
|
106
|
+
secrets: ['secret1', 'secret2'],
|
|
107
|
+
schedule: 'rate(1 day)',
|
|
108
|
+
enabled: true,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(schedule.id).toContain('schedule')
|
|
112
|
+
expect(schedule.name).toBe('daily-rotation')
|
|
113
|
+
expect(schedule.secrets).toHaveLength(2)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('CloudFormation Generation', () => {
|
|
118
|
+
it('should generate rotation CloudFormation', () => {
|
|
119
|
+
const rotation = manager.enableRDSRotation({
|
|
120
|
+
secretId: 'db-creds',
|
|
121
|
+
databaseIdentifier: 'prod-db',
|
|
122
|
+
engine: 'postgres',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const cf = manager.generateRotationCF(rotation)
|
|
126
|
+
|
|
127
|
+
expect(cf.RotationEnabled).toBe(true)
|
|
128
|
+
expect(cf.RotationRules.AutomaticallyAfterDays).toBe(30)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should generate rotation Lambda CloudFormation', () => {
|
|
132
|
+
const cf = manager.generateRotationLambdaCF({
|
|
133
|
+
functionName: 'test-rotation',
|
|
134
|
+
secretType: 'rds_credentials',
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(cf.Type).toBe('AWS::Lambda::Function')
|
|
138
|
+
expect(cf.Properties.Runtime).toBe('python3.11')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should use global instance', () => {
|
|
143
|
+
expect(secretsRotationManager).toBeInstanceOf(SecretsRotationManager)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Secrets Manager', () => {
|
|
148
|
+
let manager: SecretsManager
|
|
149
|
+
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
manager = new SecretsManager()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('Version Management', () => {
|
|
155
|
+
it('should create secret version', () => {
|
|
156
|
+
const version = manager.createVersion({
|
|
157
|
+
secretId: 'my-secret',
|
|
158
|
+
versionId: 'v1',
|
|
159
|
+
versionStages: ['AWSCURRENT'],
|
|
160
|
+
createdAt: new Date(),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(version.id).toContain('version')
|
|
164
|
+
expect(version.secretId).toBe('my-secret')
|
|
165
|
+
expect(version.versionStages).toContain('AWSCURRENT')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should get version by stage', () => {
|
|
169
|
+
manager.createVersion({
|
|
170
|
+
secretId: 'my-secret',
|
|
171
|
+
versionId: 'v1',
|
|
172
|
+
versionStages: ['AWSCURRENT'],
|
|
173
|
+
createdAt: new Date(),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const version = manager.getVersionByStage('my-secret', 'AWSCURRENT')
|
|
177
|
+
|
|
178
|
+
expect(version).toBeDefined()
|
|
179
|
+
expect(version?.versionId).toBe('v1')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should list versions for secret', () => {
|
|
183
|
+
manager.createVersion({
|
|
184
|
+
secretId: 'my-secret',
|
|
185
|
+
versionId: 'v1',
|
|
186
|
+
versionStages: ['AWSPREVIOUS'],
|
|
187
|
+
createdAt: new Date(Date.now() - 1000),
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
manager.createVersion({
|
|
191
|
+
secretId: 'my-secret',
|
|
192
|
+
versionId: 'v2',
|
|
193
|
+
versionStages: ['AWSCURRENT'],
|
|
194
|
+
createdAt: new Date(),
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const versions = manager.listVersions('my-secret')
|
|
198
|
+
|
|
199
|
+
expect(versions).toHaveLength(2)
|
|
200
|
+
expect(versions[0].versionId).toBe('v2') // Most recent first
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should deprecate version', () => {
|
|
204
|
+
const version = manager.createVersion({
|
|
205
|
+
secretId: 'my-secret',
|
|
206
|
+
versionId: 'v1',
|
|
207
|
+
versionStages: ['AWSCURRENT'],
|
|
208
|
+
createdAt: new Date(),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
manager.deprecateVersion('v1')
|
|
212
|
+
|
|
213
|
+
expect(version.deprecatedAt).toBeDefined()
|
|
214
|
+
expect(version.versionStages).not.toContain('AWSCURRENT')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should restore version', () => {
|
|
218
|
+
const oldVersion = manager.createVersion({
|
|
219
|
+
secretId: 'my-secret',
|
|
220
|
+
versionId: 'v1',
|
|
221
|
+
versionStages: ['AWSPREVIOUS'],
|
|
222
|
+
createdAt: new Date(Date.now() - 1000),
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
manager.createVersion({
|
|
226
|
+
secretId: 'my-secret',
|
|
227
|
+
versionId: 'v2',
|
|
228
|
+
versionStages: ['AWSCURRENT'],
|
|
229
|
+
createdAt: new Date(),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
manager.restoreVersion('v1')
|
|
233
|
+
|
|
234
|
+
expect(oldVersion.versionStages).toContain('AWSCURRENT')
|
|
235
|
+
expect(oldVersion.deprecatedAt).toBeUndefined()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('Audit Trail', () => {
|
|
240
|
+
it('should audit secret action', () => {
|
|
241
|
+
const audit = manager.auditAction({
|
|
242
|
+
secretId: 'my-secret',
|
|
243
|
+
action: 'READ',
|
|
244
|
+
actor: 'user@example.com',
|
|
245
|
+
success: true,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
expect(audit.id).toContain('audit')
|
|
249
|
+
expect(audit.action).toBe('READ')
|
|
250
|
+
expect(audit.timestamp).toBeDefined()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should get audit trail', () => {
|
|
254
|
+
manager.auditAction({
|
|
255
|
+
secretId: 'my-secret',
|
|
256
|
+
action: 'READ',
|
|
257
|
+
actor: 'user1',
|
|
258
|
+
success: true,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
manager.auditAction({
|
|
262
|
+
secretId: 'my-secret',
|
|
263
|
+
action: 'UPDATE',
|
|
264
|
+
actor: 'user2',
|
|
265
|
+
success: true,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const trail = manager.getAuditTrail('my-secret')
|
|
269
|
+
|
|
270
|
+
expect(trail).toHaveLength(2)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should get failed accesses', () => {
|
|
274
|
+
manager.auditAction({
|
|
275
|
+
secretId: 'my-secret',
|
|
276
|
+
action: 'READ',
|
|
277
|
+
actor: 'attacker',
|
|
278
|
+
success: false,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const failures = manager.getFailedAccesses('my-secret')
|
|
282
|
+
|
|
283
|
+
expect(failures).toHaveLength(1)
|
|
284
|
+
expect(failures[0].success).toBe(false)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('External Managers', () => {
|
|
289
|
+
it('should register HashiCorp Vault', () => {
|
|
290
|
+
const vault = manager.registerVault({
|
|
291
|
+
name: 'production-vault',
|
|
292
|
+
endpoint: 'https://vault.example.com',
|
|
293
|
+
token: 'vault-token',
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
expect(vault.id).toContain('ext-manager')
|
|
297
|
+
expect(vault.type).toBe('vault')
|
|
298
|
+
expect(vault.endpoint).toBe('https://vault.example.com')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should register 1Password', () => {
|
|
302
|
+
const onepassword = manager.registerOnePassword({
|
|
303
|
+
name: 'team-1password',
|
|
304
|
+
apiKey: '1password-api-key',
|
|
305
|
+
syncEnabled: true,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
expect(onepassword.type).toBe('onepassword')
|
|
309
|
+
expect(onepassword.syncEnabled).toBe(true)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('Secret Replication', () => {
|
|
314
|
+
it('should enable replication', () => {
|
|
315
|
+
const replication = manager.enableReplication({
|
|
316
|
+
secretId: 'my-secret',
|
|
317
|
+
sourceRegion: 'us-east-1',
|
|
318
|
+
replicaRegions: ['us-west-2', 'eu-west-1'],
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
expect(replication.id).toContain('replication')
|
|
322
|
+
expect(replication.replicaRegions).toHaveLength(2)
|
|
323
|
+
expect(replication.status).toBe('replicating')
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('Secret Policies', () => {
|
|
328
|
+
it('should create secret policy', () => {
|
|
329
|
+
const policy = manager.createPolicy({
|
|
330
|
+
secretId: 'my-secret',
|
|
331
|
+
allowedPrincipals: ['arn:aws:iam::123456789012:role/MyRole'],
|
|
332
|
+
allowedActions: ['secretsmanager:GetSecretValue'],
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
expect(policy.id).toContain('policy')
|
|
336
|
+
expect(policy.policy.Statement).toHaveLength(1)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should create cross-account policy', () => {
|
|
340
|
+
const policy = manager.createCrossAccountPolicy({
|
|
341
|
+
secretId: 'my-secret',
|
|
342
|
+
accountId: '987654321098',
|
|
343
|
+
roleNames: ['AppRole', 'AdminRole'],
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
expect(policy.policy.Statement[0].Principal.AWS).toHaveLength(2)
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should use global instance', () => {
|
|
351
|
+
expect(secretsManager).toBeInstanceOf(SecretsManager)
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
describe('Certificate Manager', () => {
|
|
356
|
+
let manager: CertificateManager
|
|
357
|
+
|
|
358
|
+
beforeEach(() => {
|
|
359
|
+
manager = new CertificateManager()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('Certificate Requests', () => {
|
|
363
|
+
it('should request certificate', () => {
|
|
364
|
+
const cert = manager.requestCertificate({
|
|
365
|
+
domainName: 'example.com',
|
|
366
|
+
validationMethod: 'DNS',
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
expect(cert.id).toContain('cert')
|
|
370
|
+
expect(cert.domainName).toBe('example.com')
|
|
371
|
+
expect(cert.status).toBe('PENDING_VALIDATION')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should request wildcard certificate', () => {
|
|
375
|
+
const cert = manager.requestWildcardCertificate({
|
|
376
|
+
domainName: '*.example.com',
|
|
377
|
+
includeApex: true,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
expect(cert.domainName).toBe('*.example.com')
|
|
381
|
+
expect(cert.subjectAlternativeNames).toContain('example.com')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('should request multi-domain certificate', () => {
|
|
385
|
+
const cert = manager.requestMultiDomainCertificate({
|
|
386
|
+
primaryDomain: 'example.com',
|
|
387
|
+
additionalDomains: ['www.example.com', 'api.example.com'],
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
expect(cert.subjectAlternativeNames).toHaveLength(2)
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe('Certificate Validation', () => {
|
|
395
|
+
it('should validate certificate', () => {
|
|
396
|
+
const cert = manager.requestCertificate({
|
|
397
|
+
domainName: 'example.com',
|
|
398
|
+
validationMethod: 'DNS',
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
const result = manager.validateCertificate(cert.id)
|
|
402
|
+
|
|
403
|
+
expect(result.success).toBe(true)
|
|
404
|
+
expect(cert.status).toBe('ISSUED')
|
|
405
|
+
expect(cert.expiresAt).toBeDefined()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should create DNS validation records', () => {
|
|
409
|
+
const cert = manager.requestCertificate({
|
|
410
|
+
domainName: 'example.com',
|
|
411
|
+
validationMethod: 'DNS',
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
const validation = manager.getValidation(cert.id)
|
|
415
|
+
|
|
416
|
+
expect(validation).toBeDefined()
|
|
417
|
+
expect(validation?.validationMethod).toBe('DNS')
|
|
418
|
+
expect(validation?.resourceRecords).toBeDefined()
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
describe('Certificate Renewal', () => {
|
|
423
|
+
it('should enable auto-renewal', () => {
|
|
424
|
+
const cert = manager.requestCertificate({
|
|
425
|
+
domainName: 'example.com',
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
const renewal = manager.enableAutoRenewal({
|
|
429
|
+
certificateArn: cert.arn,
|
|
430
|
+
renewBeforeDays: 30,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
expect(renewal.id).toContain('renewal')
|
|
434
|
+
expect(renewal.autoRenew).toBe(true)
|
|
435
|
+
expect(renewal.renewBeforeDays).toBe(30)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('should renew certificate', async () => {
|
|
439
|
+
const cert = manager.requestCertificate({
|
|
440
|
+
domainName: 'example.com',
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
manager.validateCertificate(cert.id)
|
|
444
|
+
|
|
445
|
+
const renewal = manager.enableAutoRenewal({
|
|
446
|
+
certificateArn: cert.arn,
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
const result = await manager.renewCertificate(renewal.id)
|
|
450
|
+
|
|
451
|
+
expect(result.success).toBe(true)
|
|
452
|
+
expect(renewal.lastRenewal).toBeDefined()
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
describe('Certificate Monitoring', () => {
|
|
457
|
+
it('should create certificate monitor', () => {
|
|
458
|
+
const monitor = manager.createMonitor({
|
|
459
|
+
name: 'production-certs',
|
|
460
|
+
certificates: ['cert-arn-1', 'cert-arn-2'],
|
|
461
|
+
expirationThreshold: 30,
|
|
462
|
+
alertEnabled: true,
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
expect(monitor.id).toContain('monitor')
|
|
466
|
+
expect(monitor.certificates).toHaveLength(2)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('should check expiration', () => {
|
|
470
|
+
const cert = manager.requestCertificate({
|
|
471
|
+
domainName: 'example.com',
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
manager.validateCertificate(cert.id)
|
|
475
|
+
|
|
476
|
+
// Set expiration to 15 days from now
|
|
477
|
+
cert.expiresAt = new Date(Date.now() + 15 * 24 * 60 * 60 * 1000)
|
|
478
|
+
|
|
479
|
+
const alerts = manager.checkExpiration()
|
|
480
|
+
|
|
481
|
+
expect(alerts.length).toBeGreaterThan(0)
|
|
482
|
+
expect(alerts[0].alertType).toBe('expiring_soon')
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('should get expiring certificates', () => {
|
|
486
|
+
const cert1 = manager.requestCertificate({ domainName: 'example.com' })
|
|
487
|
+
manager.validateCertificate(cert1.id)
|
|
488
|
+
cert1.expiresAt = new Date(Date.now() + 15 * 24 * 60 * 60 * 1000)
|
|
489
|
+
|
|
490
|
+
const cert2 = manager.requestCertificate({ domainName: 'test.com' })
|
|
491
|
+
manager.validateCertificate(cert2.id)
|
|
492
|
+
cert2.expiresAt = new Date(Date.now() + 400 * 24 * 60 * 60 * 1000)
|
|
493
|
+
|
|
494
|
+
const expiring = manager.getExpiringCertificates(30)
|
|
495
|
+
|
|
496
|
+
expect(expiring).toHaveLength(1)
|
|
497
|
+
expect(expiring[0].domainName).toBe('example.com')
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
describe('CloudFormation Generation', () => {
|
|
502
|
+
it('should generate certificate CloudFormation', () => {
|
|
503
|
+
const cert = manager.requestCertificate({
|
|
504
|
+
domainName: 'example.com',
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const cf = manager.generateCertificateCF(cert)
|
|
508
|
+
|
|
509
|
+
expect(cf.Type).toBe('AWS::CertificateManager::Certificate')
|
|
510
|
+
expect(cf.Properties.DomainName).toBe('example.com')
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('should generate expiration alarm', () => {
|
|
514
|
+
const cert = manager.requestCertificate({
|
|
515
|
+
domainName: 'example.com',
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
const cf = manager.generateExpirationAlarmCF({
|
|
519
|
+
alarmName: 'cert-expiring',
|
|
520
|
+
certificateArn: cert.arn,
|
|
521
|
+
daysBeforeExpiration: 30,
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
expect(cf.Type).toBe('AWS::CloudWatch::Alarm')
|
|
525
|
+
expect(cf.Properties.Threshold).toBe(30)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('should use global instance', () => {
|
|
530
|
+
expect(certificateManager).toBeInstanceOf(CertificateManager)
|
|
531
|
+
})
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
describe('Security Scanning Manager', () => {
|
|
535
|
+
let manager: SecurityScanningManager
|
|
536
|
+
|
|
537
|
+
beforeEach(() => {
|
|
538
|
+
manager = new SecurityScanningManager()
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
describe('Scan Creation', () => {
|
|
542
|
+
it('should create container scan', () => {
|
|
543
|
+
const scan = manager.createContainerScan({
|
|
544
|
+
name: 'app-image-scan',
|
|
545
|
+
imageUri: 'my-repo/app:latest',
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
expect(scan.id).toContain('scan')
|
|
549
|
+
expect(scan.scanType).toBe('container_image')
|
|
550
|
+
expect(scan.target.type).toBe('ecr_image')
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('should create Lambda scan', () => {
|
|
554
|
+
const scan = manager.createLambdaScan({
|
|
555
|
+
name: 'lambda-scan',
|
|
556
|
+
functionName: 'my-function',
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
expect(scan.scanType).toBe('vulnerability')
|
|
560
|
+
expect(scan.target.type).toBe('lambda')
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('should create secrets detection scan', () => {
|
|
564
|
+
const scan = manager.createSecretsDetectionScan({
|
|
565
|
+
name: 'repo-secrets-scan',
|
|
566
|
+
repositoryUrl: 'https://github.com/org/repo',
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
expect(scan.scanType).toBe('secrets_detection')
|
|
570
|
+
expect(scan.target.type).toBe('repository')
|
|
571
|
+
})
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
describe('Scan Execution', () => {
|
|
575
|
+
it('should execute scan', async () => {
|
|
576
|
+
const scan = manager.createContainerScan({
|
|
577
|
+
name: 'test-scan',
|
|
578
|
+
imageUri: 'test:latest',
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
const result = await manager.executeScan(scan.id)
|
|
582
|
+
|
|
583
|
+
expect(result.status).toBe('completed')
|
|
584
|
+
expect(result.summary).toBeDefined()
|
|
585
|
+
expect(result.findings).toBeDefined()
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('should generate scan summary', async () => {
|
|
589
|
+
const scan = manager.createContainerScan({
|
|
590
|
+
name: 'test-scan',
|
|
591
|
+
imageUri: 'test:latest',
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
await manager.executeScan(scan.id)
|
|
595
|
+
|
|
596
|
+
expect(scan.summary?.totalFindings).toBeGreaterThanOrEqual(0)
|
|
597
|
+
expect(scan.summary?.executionTime).toBeGreaterThanOrEqual(0)
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
describe('Findings Management', () => {
|
|
602
|
+
it('should create finding', () => {
|
|
603
|
+
const finding = manager.createFinding({
|
|
604
|
+
severity: 'HIGH',
|
|
605
|
+
title: 'SQL Injection vulnerability',
|
|
606
|
+
description: 'User input not sanitized',
|
|
607
|
+
affectedResource: 'app.js:42',
|
|
608
|
+
remediation: 'Use parameterized queries',
|
|
609
|
+
status: 'OPEN',
|
|
610
|
+
firstDetected: new Date(),
|
|
611
|
+
lastSeen: new Date(),
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
expect(finding.id).toContain('finding')
|
|
615
|
+
expect(finding.severity).toBe('HIGH')
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('should suppress finding', () => {
|
|
619
|
+
const finding = manager.createFinding({
|
|
620
|
+
severity: 'MEDIUM',
|
|
621
|
+
title: 'Test finding',
|
|
622
|
+
description: 'Test',
|
|
623
|
+
affectedResource: 'test',
|
|
624
|
+
status: 'OPEN',
|
|
625
|
+
firstDetected: new Date(),
|
|
626
|
+
lastSeen: new Date(),
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
manager.suppressFinding(finding.id, 'False positive')
|
|
630
|
+
|
|
631
|
+
expect(finding.status).toBe('SUPPRESSED')
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
it('should resolve finding', () => {
|
|
635
|
+
const finding = manager.createFinding({
|
|
636
|
+
severity: 'LOW',
|
|
637
|
+
title: 'Test finding',
|
|
638
|
+
description: 'Test',
|
|
639
|
+
affectedResource: 'test',
|
|
640
|
+
status: 'OPEN',
|
|
641
|
+
firstDetected: new Date(),
|
|
642
|
+
lastSeen: new Date(),
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
manager.resolveFinding(finding.id)
|
|
646
|
+
|
|
647
|
+
expect(finding.status).toBe('RESOLVED')
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('should get open findings by severity', async () => {
|
|
651
|
+
const scan = manager.createContainerScan({
|
|
652
|
+
name: 'test',
|
|
653
|
+
imageUri: 'test:latest',
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
await manager.executeScan(scan.id)
|
|
657
|
+
|
|
658
|
+
const criticalFindings = manager.getOpenFindings('CRITICAL')
|
|
659
|
+
const allOpenFindings = manager.getOpenFindings()
|
|
660
|
+
|
|
661
|
+
expect(Array.isArray(criticalFindings)).toBe(true)
|
|
662
|
+
expect(Array.isArray(allOpenFindings)).toBe(true)
|
|
663
|
+
})
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
describe('Compliance Checks', () => {
|
|
667
|
+
it('should run compliance check', () => {
|
|
668
|
+
const checks = manager.runComplianceCheck({
|
|
669
|
+
framework: 'CIS_AWS_FOUNDATIONS_1_4',
|
|
670
|
+
resourceType: 'AWS::IAM::User',
|
|
671
|
+
resourceId: 'user-123',
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
expect(checks.length).toBeGreaterThan(0)
|
|
675
|
+
expect(checks[0].framework).toBe('CIS_AWS_FOUNDATIONS_1_4')
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('should get checks by status', () => {
|
|
679
|
+
manager.runComplianceCheck({
|
|
680
|
+
framework: 'CIS_AWS_FOUNDATIONS_1_4',
|
|
681
|
+
resourceType: 'AWS::IAM::User',
|
|
682
|
+
resourceId: 'user-123',
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
const passed = manager.getComplianceChecksByStatus('PASS')
|
|
686
|
+
const failed = manager.getComplianceChecksByStatus('FAIL')
|
|
687
|
+
|
|
688
|
+
expect(Array.isArray(passed)).toBe(true)
|
|
689
|
+
expect(Array.isArray(failed)).toBe(true)
|
|
690
|
+
})
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
describe('Security Posture', () => {
|
|
694
|
+
it('should assess security posture', () => {
|
|
695
|
+
manager.runComplianceCheck({
|
|
696
|
+
framework: 'CIS_AWS_FOUNDATIONS_1_4',
|
|
697
|
+
resourceType: 'AWS::IAM::User',
|
|
698
|
+
resourceId: 'user-123',
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
const posture = manager.assessSecurityPosture({
|
|
702
|
+
accountId: '123456789012',
|
|
703
|
+
region: 'us-east-1',
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
expect(posture.id).toContain('posture')
|
|
707
|
+
expect(posture.score).toBeGreaterThanOrEqual(0)
|
|
708
|
+
expect(posture.score).toBeLessThanOrEqual(100)
|
|
709
|
+
expect(posture.grade).toMatch(/^[ABCDF]$/)
|
|
710
|
+
expect(posture.recommendations).toBeDefined()
|
|
711
|
+
})
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
describe('Reports', () => {
|
|
715
|
+
it('should generate vulnerability report', async () => {
|
|
716
|
+
const scan = manager.createContainerScan({
|
|
717
|
+
name: 'test',
|
|
718
|
+
imageUri: 'test:latest',
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
await manager.executeScan(scan.id)
|
|
722
|
+
|
|
723
|
+
const report = manager.generateReport({
|
|
724
|
+
scanId: scan.id,
|
|
725
|
+
reportType: 'detailed',
|
|
726
|
+
format: 'pdf',
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
expect(report.id).toContain('report')
|
|
730
|
+
expect(report.format).toBe('pdf')
|
|
731
|
+
expect(report.s3Location).toBeDefined()
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
it('should use global instance', () => {
|
|
736
|
+
expect(securityScanningManager).toBeInstanceOf(SecurityScanningManager)
|
|
737
|
+
})
|
|
738
|
+
})
|