@gugananuvem/aws-local-simulator 1.0.31 → 1.0.34

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.
Files changed (79) hide show
  1. package/README.md +834 -834
  2. package/aws-config +153 -153
  3. package/bin/aws-local-simulator.js +63 -63
  4. package/package.json +3 -2
  5. package/src/config/config-loader.js +114 -114
  6. package/src/config/default-config.js +79 -79
  7. package/src/config/env-loader.js +68 -68
  8. package/src/index.js +146 -146
  9. package/src/index.mjs +123 -123
  10. package/src/server.js +463 -463
  11. package/src/services/apigateway/index.js +75 -75
  12. package/src/services/apigateway/server.js +607 -607
  13. package/src/services/apigateway/simulator.js +1405 -1405
  14. package/src/services/athena/index.js +75 -75
  15. package/src/services/athena/server.js +101 -101
  16. package/src/services/athena/simulador.js +998 -998
  17. package/src/services/athena/simulator.js +346 -346
  18. package/src/services/cloudformation/index.js +106 -106
  19. package/src/services/cloudformation/server.js +417 -417
  20. package/src/services/cloudformation/simulador.js +1020 -1020
  21. package/src/services/cloudtrail/index.js +84 -84
  22. package/src/services/cloudtrail/server.js +235 -235
  23. package/src/services/cloudtrail/simulador.js +719 -719
  24. package/src/services/cloudwatch/index.js +84 -84
  25. package/src/services/cloudwatch/server.js +366 -366
  26. package/src/services/cloudwatch/simulador.js +1173 -1173
  27. package/src/services/cognito/index.js +79 -79
  28. package/src/services/cognito/server.js +297 -297
  29. package/src/services/cognito/simulator.js +1992 -1761
  30. package/src/services/config/index.js +96 -96
  31. package/src/services/config/server.js +215 -215
  32. package/src/services/config/simulador.js +1260 -1260
  33. package/src/services/dynamodb/index.js +74 -74
  34. package/src/services/dynamodb/server.js +139 -139
  35. package/src/services/dynamodb/simulator.js +1005 -982
  36. package/src/services/dynamodb/sqlite-store.js +722 -0
  37. package/src/services/ecs/index.js +65 -65
  38. package/src/services/ecs/server.js +235 -235
  39. package/src/services/ecs/simulator.js +844 -844
  40. package/src/services/eventbridge/index.js +89 -89
  41. package/src/services/eventbridge/server.js +209 -209
  42. package/src/services/eventbridge/simulator.js +684 -684
  43. package/src/services/index.js +45 -45
  44. package/src/services/kms/index.js +75 -75
  45. package/src/services/kms/server.js +81 -81
  46. package/src/services/kms/simulator.js +344 -344
  47. package/src/services/lambda/handler-loader.js +183 -183
  48. package/src/services/lambda/index.js +81 -81
  49. package/src/services/lambda/route-registry.js +274 -274
  50. package/src/services/lambda/server.js +191 -191
  51. package/src/services/lambda/simulator.js +364 -364
  52. package/src/services/parameter-store/index.js +80 -80
  53. package/src/services/parameter-store/server.js +50 -50
  54. package/src/services/parameter-store/simulator.js +201 -201
  55. package/src/services/s3/index.js +73 -73
  56. package/src/services/s3/server.js +350 -350
  57. package/src/services/s3/simulator.js +568 -568
  58. package/src/services/secret-manager/index.js +80 -80
  59. package/src/services/secret-manager/server.js +51 -51
  60. package/src/services/secret-manager/simulator.js +182 -182
  61. package/src/services/sns/index.js +89 -89
  62. package/src/services/sns/server.js +607 -607
  63. package/src/services/sns/simulator.js +1482 -1482
  64. package/src/services/sqs/index.js +98 -98
  65. package/src/services/sqs/server.js +360 -360
  66. package/src/services/sqs/simulator.js +509 -509
  67. package/src/services/sts/index.js +37 -37
  68. package/src/services/sts/server.js +144 -144
  69. package/src/services/sts/simulator.js +69 -69
  70. package/src/services/xray/index.js +83 -83
  71. package/src/services/xray/server.js +308 -308
  72. package/src/services/xray/simulador.js +994 -994
  73. package/src/template/aws-config-template.js +87 -87
  74. package/src/template/aws-config-template.mjs +90 -90
  75. package/src/template/config-template.json +203 -203
  76. package/src/utils/aws-config.js +91 -91
  77. package/src/utils/cloudtrail-audit.js +129 -129
  78. package/src/utils/local-store.js +83 -83
  79. package/src/utils/logger.js +59 -59
@@ -1,719 +1,719 @@
1
- 'use strict';
2
-
3
- /**
4
- * @fileoverview CloudTrail Simulator
5
- *
6
- * Suporta:
7
- * Trails:
8
- * - CreateTrail / DeleteTrail / UpdateTrail
9
- * - DescribeTrails / GetTrail / GetTrailStatus
10
- * - StartLogging / StopLogging
11
- *
12
- * Events:
13
- * - LookupEvents (filtro por ResourceName, EventName, Username, etc.)
14
- * - GetEventSelectors / PutEventSelectors
15
- *
16
- * Integração:
17
- * - Registro automático de API calls dos outros serviços
18
- * - Entrega de logs para S3 (simulado)
19
- * - Integração com CloudWatch Logs
20
- *
21
- * Persistência via LocalStore
22
- */
23
-
24
- const { randomUUID } = require('crypto');
25
-
26
- // ─── Erros tipados ────────────────────────────────────────────────────────────
27
-
28
- class CloudTrailError extends Error {
29
- constructor(code, message, statusCode = 400) {
30
- super(message);
31
- this.code = code;
32
- this.statusCode = statusCode;
33
- }
34
- }
35
-
36
- const Errors = {
37
- TrailNotFound: (name) =>
38
- new CloudTrailError('TrailNotFoundException', `Unknown trail: ${name}`, 404),
39
- TrailAlreadyExists: (name) =>
40
- new CloudTrailError('TrailAlreadyExistsException', `Trail already exists: ${name}`, 400),
41
- InvalidTrailName: (name) =>
42
- new CloudTrailError('InvalidTrailNameException', `Invalid trail name: ${name}`, 400),
43
- InvalidParameter: (msg) =>
44
- new CloudTrailError('InvalidParameterCombinationException', msg, 400),
45
- MaximumNumberOfTrails: () =>
46
- new CloudTrailError('MaximumNumberOfTrailsExceededException', 'Maximum number of trails exceeded (max: 5)', 400),
47
- S3BucketNotFound: (bucket) =>
48
- new CloudTrailError('S3BucketDoesNotExistException', `S3 bucket does not exist: ${bucket}`, 400),
49
- InsuficientSnsTopicPolicy: () =>
50
- new CloudTrailError('InsuficientSnsTopicPolicyException', 'Insufficient SNS topic policy', 400),
51
- };
52
-
53
- // ─── Constantes ───────────────────────────────────────────────────────────────
54
-
55
- const REGION = 'us-east-1';
56
- const ACCOUNT_ID = '000000000000';
57
- const MAX_TRAILS = 5;
58
- const MAX_RESULTS_DEFAULT = 50;
59
- const MAX_RESULTS_MAX = 50;
60
-
61
- // ─── Utilitários ──────────────────────────────────────────────────────────────
62
-
63
- function trailArn(name) {
64
- return `arn:aws:cloudtrail:${REGION}:${ACCOUNT_ID}:trail/${name}`;
65
- }
66
-
67
- function validateTrailName(name) {
68
- if (!name || typeof name !== 'string') throw Errors.InvalidTrailName(name);
69
- if (name.length < 3 || name.length > 128) throw Errors.InvalidTrailName(name);
70
- if (!/^[a-zA-Z0-9._-]+$/.test(name)) throw Errors.InvalidTrailName(name);
71
- return true;
72
- }
73
-
74
- function matchesFilter(event, filter) {
75
- if (!filter) return true;
76
-
77
- if (filter.AttributeKey && filter.AttributeValue) {
78
- const key = filter.AttributeKey;
79
- const value = filter.AttributeValue;
80
-
81
- switch (key) {
82
- case 'EventId':
83
- if (event.EventId !== value) return false;
84
- break;
85
- case 'EventName':
86
- if (event.EventName !== value) return false;
87
- break;
88
- case 'ReadOnly':
89
- if (String(event.ReadOnly) !== value) return false;
90
- break;
91
- case 'Username':
92
- if (event.Username !== value) return false;
93
- break;
94
- case 'ResourceType':
95
- if (!event.Resources || !event.Resources.some(r => r.ResourceType === value)) return false;
96
- break;
97
- case 'ResourceName':
98
- if (!event.Resources || !event.Resources.some(r => r.ResourceName === value)) return false;
99
- break;
100
- case 'EventSource':
101
- if (event.EventSource !== value) return false;
102
- break;
103
- case 'AccessKeyId':
104
- if (event.AccessKeyId !== value) return false;
105
- break;
106
- default:
107
- break;
108
- }
109
- }
110
-
111
- return true;
112
- }
113
-
114
- // ─── Simulador Principal ──────────────────────────────────────────────────────
115
-
116
- class CloudTrailSimulator {
117
- /**
118
- * @param {Object} config - Global simulator config
119
- * @param {Object} store - LocalStore instance
120
- * @param {Object} logger - Logger instance
121
- */
122
- constructor(config, store, logger) {
123
- this.config = config;
124
- this.store = store;
125
- this.logger = logger;
126
-
127
- // Trails: Map<name, TrailConfig>
128
- this.trails = new Map();
129
-
130
- // Trail status: Map<name, { isLogging, latestDelivery, latestNotification }>
131
- this.trailStatus = new Map();
132
-
133
- // Event selectors: Map<name, EventSelector[]>
134
- this.eventSelectors = new Map();
135
-
136
- // Events log: Array de CloudTrail events
137
- this.events = [];
138
-
139
- // Injeções cross-service
140
- this.s3Simulator = null;
141
- this.cloudwatchSimulator = null;
142
- }
143
-
144
- // ─── Persistência ───────────────────────────────────────────────────────────
145
-
146
- async load() {
147
- try {
148
- const data = await this.store.load('cloudtrail');
149
- if (data) {
150
- if (data.trails) this.trails = new Map(Object.entries(data.trails));
151
- if (data.trailStatus) this.trailStatus = new Map(Object.entries(data.trailStatus));
152
- if (data.eventSelectors) this.eventSelectors = new Map(Object.entries(data.eventSelectors));
153
- if (data.events) this.events = data.events;
154
- this.logger.info(`[CloudTrail] Loaded ${this.trails.size} trails, ${this.events.length} events`);
155
- }
156
- } catch (err) {
157
- this.logger.warn('[CloudTrail] No persisted data found, starting fresh');
158
- }
159
-
160
- // Garante que existe um trail padrão com logging ativo
161
- if (this.trails.size === 0) {
162
- const defaultName = 'local-default-trail';
163
- this.trails.set(defaultName, { Name: defaultName, S3BucketName: null });
164
- this.trailStatus.set(defaultName, { isLogging: true });
165
- // Event selectors: captura management events + todos os data events
166
- this.eventSelectors.set(defaultName, [
167
- {
168
- ReadWriteType: 'All',
169
- IncludeManagementEvents: true,
170
- DataResources: [
171
- { Type: 'AWS::S3::Object', Values: ['arn:aws:s3:::*'] },
172
- { Type: 'AWS::DynamoDB::Table', Values: ['arn:aws:dynamodb:::*'] },
173
- { Type: 'AWS::Lambda::Function', Values: ['arn:aws:lambda:::*'] },
174
- { Type: 'AWS::APIGateway::Stage', Values: ['arn:aws:execute-api:::*'] },
175
- { Type: 'AWS::SecretsManager::Secret', Values: ['arn:aws:secretsmanager:::*'] },
176
- { Type: 'AWS::SSM::Parameter', Values: ['arn:aws:ssm:::*'] },
177
- { Type: 'AWS::Cognito::UserPool', Values: ['arn:aws:cognito-idp:::*'] },
178
- { Type: 'AWS::KMS::Key', Values: ['arn:aws:kms:::*'] },
179
- ],
180
- },
181
- ]);
182
- this.logger.debug('[CloudTrail] Trail padrão criado com logging ativo');
183
- }
184
- }
185
-
186
- async save() {
187
- try {
188
- const data = {
189
- trails: Object.fromEntries(this.trails),
190
- trailStatus: Object.fromEntries(this.trailStatus),
191
- eventSelectors: Object.fromEntries(this.eventSelectors),
192
- events: this.events.slice(-10000), // mantém os últimos 10.000 eventos
193
- };
194
- await this.store.save('cloudtrail', data);
195
- } catch (err) {
196
- this.logger.error('[CloudTrail] Failed to save data:', err.message);
197
- }
198
- }
199
-
200
- reset() {
201
- this.trails.clear();
202
- this.trailStatus.clear();
203
- this.eventSelectors.clear();
204
- this.events = [];
205
- }
206
-
207
- // ─── Registro de Evento (uso interno / cross-service) ───────────────────────
208
-
209
- /**
210
- * Registra um API call como evento CloudTrail.
211
- * Chamado pelos outros serviços via injeção.
212
- *
213
- * @param {Object} params
214
- * @param {string} params.eventName - Ex: "CreateBucket"
215
- * @param {string} params.eventSource - Ex: "s3.amazonaws.com"
216
- * @param {string} params.username - Ex: "test-user"
217
- * @param {boolean} params.readOnly - true se operação de leitura
218
- * @param {Array} params.resources - [{ ResourceType, ResourceName }]
219
- * @param {Object} params.requestParameters - Parâmetros da requisição
220
- * @param {Object} params.responseElements - Resposta da operação
221
- * @param {string} params.sourceIPAddress - IP da requisição
222
- * @param {string} params.userAgent - User-Agent da requisição
223
- */
224
- recordEvent(params = {}) {
225
- const event = {
226
- EventId: randomUUID(),
227
- EventName: params.eventName || 'UnknownEvent',
228
- EventSource: params.eventSource || 'local.amazonaws.com',
229
- EventTime: new Date().toISOString(),
230
- Username: params.username || 'local-user',
231
- ReadOnly: params.readOnly !== undefined ? params.readOnly : false,
232
- AccessKeyId: params.accessKeyId || 'AKIAIOSFODNN7EXAMPLE',
233
- Resources: params.resources || [],
234
- RequestParameters: params.requestParameters || null,
235
- ResponseElements: params.responseElements || null,
236
- SourceIPAddress: params.sourceIPAddress || '127.0.0.1',
237
- UserAgent: params.userAgent || 'aws-local-simulator',
238
- CloudTrailEvent: JSON.stringify({
239
- eventVersion: '1.08',
240
- userIdentity: {
241
- type: 'IAMUser',
242
- principalId: 'AIDIOSFODNN7EXAMPLE',
243
- arn: `arn:aws:iam::${ACCOUNT_ID}:user/${params.username || 'local-user'}`,
244
- accountId: ACCOUNT_ID,
245
- accessKeyId: params.accessKeyId || 'AKIAIOSFODNN7EXAMPLE',
246
- userName: params.username || 'local-user',
247
- },
248
- eventTime: new Date().toISOString(),
249
- eventSource: params.eventSource || 'local.amazonaws.com',
250
- eventName: params.eventName || 'UnknownEvent',
251
- awsRegion: REGION,
252
- sourceIPAddress: params.sourceIPAddress || '127.0.0.1',
253
- userAgent: params.userAgent || 'aws-local-simulator',
254
- requestParameters: params.requestParameters || null,
255
- responseElements: params.responseElements || null,
256
- requestID: randomUUID(),
257
- eventID: randomUUID(),
258
- readOnly: params.readOnly !== undefined ? params.readOnly : false,
259
- resources: params.resources || [],
260
- eventType: 'AwsApiCall',
261
- managementEvent: true,
262
- recipientAccountId: ACCOUNT_ID,
263
- }),
264
- };
265
-
266
- this.events.push(event);
267
-
268
- // Persiste no disco
269
- this.save();
270
-
271
- // Entrega para S3 (simula escrita de log file)
272
- this._deliverToS3(event);
273
-
274
- // Entrega para CloudWatch Logs se configurado
275
- this._deliverToCloudWatch(event);
276
-
277
- return event;
278
- }
279
-
280
- async _deliverToS3(event) {
281
- // Verifica se há trails ativos com S3 configurado
282
- for (const [name, trail] of this.trails) {
283
- const status = this.trailStatus.get(name) || {};
284
- if (!status.isLogging) continue;
285
- if (!trail.S3BucketName) continue;
286
-
287
- if (this.s3Simulator) {
288
- try {
289
- const date = new Date(event.EventTime);
290
- const key = `${trail.S3KeyPrefix || 'AWSLogs/'}${ACCOUNT_ID}/CloudTrail/${REGION}/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${randomUUID()}.json`;
291
-
292
- await this.s3Simulator.putObject({
293
- Bucket: trail.S3BucketName,
294
- Key: key,
295
- Body: JSON.stringify({ Records: [JSON.parse(event.CloudTrailEvent)] }),
296
- ContentType: 'application/json',
297
- });
298
-
299
- // Atualiza status
300
- const s = this.trailStatus.get(name) || {};
301
- s.LatestDeliveryTime = new Date().toISOString();
302
- s.LatestDeliveryAttemptTime = new Date().toISOString();
303
- this.trailStatus.set(name, s);
304
- } catch (err) {
305
- this.logger.warn(`[CloudTrail] Failed to deliver to S3 for trail ${name}: ${err.message}`);
306
- const s = this.trailStatus.get(name) || {};
307
- s.LatestDeliveryError = err.message;
308
- s.LatestDeliveryAttemptTime = new Date().toISOString();
309
- this.trailStatus.set(name, s);
310
- }
311
- }
312
- }
313
- }
314
-
315
- _deliverToCloudWatch(event) {
316
- for (const [name, trail] of this.trails) {
317
- const status = this.trailStatus.get(name) || {};
318
- if (!status.isLogging) continue;
319
- if (!trail.CloudWatchLogsLogGroupArn) continue;
320
-
321
- if (this.cloudwatchSimulator) {
322
- try {
323
- // Extrai o log group name do ARN
324
- const match = trail.CloudWatchLogsLogGroupArn.match(/log-group:([^:]+)/);
325
- if (!match) continue;
326
- const logGroupName = match[1];
327
-
328
- this.cloudwatchSimulator.putLambdaLogs(logGroupName, 'cloudtrail', [
329
- { timestamp: Date.now(), message: event.CloudTrailEvent },
330
- ]);
331
- } catch (err) {
332
- this.logger.warn(`[CloudTrail] Failed to deliver to CloudWatch for trail ${name}: ${err.message}`);
333
- }
334
- }
335
- }
336
- }
337
-
338
- // ─── Trails ─────────────────────────────────────────────────────────────────
339
-
340
- createTrail(params = {}) {
341
- const name = params.Name;
342
- validateTrailName(name);
343
-
344
- if (this.trails.has(name)) throw Errors.TrailAlreadyExists(name);
345
- if (this.trails.size >= MAX_TRAILS) throw Errors.MaximumNumberOfTrails();
346
-
347
- const trail = {
348
- Name: name,
349
- S3BucketName: params.S3BucketName || null,
350
- S3KeyPrefix: params.S3KeyPrefix || null,
351
- SnsTopicName: params.SnsTopicName || null,
352
- SnsTopicARN: params.SnsTopicName
353
- ? `arn:aws:sns:${REGION}:${ACCOUNT_ID}:${params.SnsTopicName}`
354
- : null,
355
- IncludeGlobalServiceEvents: params.IncludeGlobalServiceEvents !== false,
356
- IsMultiRegionTrail: params.IsMultiRegionTrail === true,
357
- HomeRegion: REGION,
358
- TrailARN: trailArn(name),
359
- LogFileValidationEnabled: params.EnableLogFileValidation === true,
360
- CloudWatchLogsLogGroupArn: params.CloudWatchLogsLogGroupArn || null,
361
- CloudWatchLogsRoleArn: params.CloudWatchLogsRoleArn || null,
362
- HasCustomEventSelectors: false,
363
- HasInsightSelectors: false,
364
- IsOrganizationTrail: false,
365
- CreatedAt: new Date().toISOString(),
366
- Tags: params.TagsList || [],
367
- };
368
-
369
- this.trails.set(name, trail);
370
- this.trailStatus.set(name, {
371
- isLogging: false,
372
- LatestDeliveryTime: null,
373
- LatestDeliveryAttemptTime: null,
374
- LatestDeliveryError: null,
375
- LatestNotificationTime: null,
376
- LatestNotificationAttemptTime: null,
377
- LatestNotificationError: null,
378
- LatestCloudWatchLogsDeliveryTime: null,
379
- LatestCloudWatchLogsDeliveryError: null,
380
- StartLoggingTime: null,
381
- StopLoggingTime: null,
382
- });
383
-
384
- // Seletores padrão
385
- this.eventSelectors.set(name, [{
386
- ReadWriteType: 'All',
387
- IncludeManagementEvents: true,
388
- DataResources: [],
389
- ExcludeManagementEventSources: [],
390
- }]);
391
-
392
- this.logger.info(`[CloudTrail] Trail created: ${name}`);
393
- this.save();
394
-
395
- return {
396
- Name: trail.Name,
397
- S3BucketName: trail.S3BucketName,
398
- S3KeyPrefix: trail.S3KeyPrefix,
399
- SnsTopicName: trail.SnsTopicName,
400
- SnsTopicARN: trail.SnsTopicARN,
401
- IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
402
- IsMultiRegionTrail: trail.IsMultiRegionTrail,
403
- TrailARN: trail.TrailARN,
404
- LogFileValidationEnabled: trail.LogFileValidationEnabled,
405
- CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
406
- CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
407
- };
408
- }
409
-
410
- updateTrail(params = {}) {
411
- const name = params.Name;
412
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
413
-
414
- const trail = this.trails.get(name);
415
-
416
- if (params.S3BucketName !== undefined) trail.S3BucketName = params.S3BucketName;
417
- if (params.S3KeyPrefix !== undefined) trail.S3KeyPrefix = params.S3KeyPrefix;
418
- if (params.SnsTopicName !== undefined) {
419
- trail.SnsTopicName = params.SnsTopicName;
420
- trail.SnsTopicARN = params.SnsTopicName
421
- ? `arn:aws:sns:${REGION}:${ACCOUNT_ID}:${params.SnsTopicName}`
422
- : null;
423
- }
424
- if (params.IncludeGlobalServiceEvents !== undefined) trail.IncludeGlobalServiceEvents = params.IncludeGlobalServiceEvents;
425
- if (params.IsMultiRegionTrail !== undefined) trail.IsMultiRegionTrail = params.IsMultiRegionTrail;
426
- if (params.EnableLogFileValidation !== undefined) trail.LogFileValidationEnabled = params.EnableLogFileValidation;
427
- if (params.CloudWatchLogsLogGroupArn !== undefined) trail.CloudWatchLogsLogGroupArn = params.CloudWatchLogsLogGroupArn;
428
- if (params.CloudWatchLogsRoleArn !== undefined) trail.CloudWatchLogsRoleArn = params.CloudWatchLogsRoleArn;
429
-
430
- this.trails.set(name, trail);
431
- this.logger.info(`[CloudTrail] Trail updated: ${name}`);
432
- this.save();
433
-
434
- return {
435
- Name: trail.Name,
436
- S3BucketName: trail.S3BucketName,
437
- S3KeyPrefix: trail.S3KeyPrefix,
438
- SnsTopicName: trail.SnsTopicName,
439
- SnsTopicARN: trail.SnsTopicARN,
440
- IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
441
- IsMultiRegionTrail: trail.IsMultiRegionTrail,
442
- TrailARN: trail.TrailARN,
443
- LogFileValidationEnabled: trail.LogFileValidationEnabled,
444
- CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
445
- CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
446
- };
447
- }
448
-
449
- deleteTrail(params = {}) {
450
- const name = params.Name;
451
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
452
-
453
- this.trails.delete(name);
454
- this.trailStatus.delete(name);
455
- this.eventSelectors.delete(name);
456
-
457
- this.logger.info(`[CloudTrail] Trail deleted: ${name}`);
458
- this.save();
459
-
460
- return {};
461
- }
462
-
463
- describeTrails(params = {}) {
464
- const includeShadow = params.includeShadowTrails !== false;
465
- let trailList = [...this.trails.values()];
466
-
467
- if (params.trailNameList && params.trailNameList.length > 0) {
468
- trailList = trailList.filter(t =>
469
- params.trailNameList.includes(t.Name) || params.trailNameList.includes(t.TrailARN)
470
- );
471
- }
472
-
473
- return {
474
- trailList: trailList.map(t => ({
475
- Name: t.Name,
476
- S3BucketName: t.S3BucketName,
477
- S3KeyPrefix: t.S3KeyPrefix,
478
- SnsTopicName: t.SnsTopicName,
479
- SnsTopicARN: t.SnsTopicARN,
480
- IncludeGlobalServiceEvents: t.IncludeGlobalServiceEvents,
481
- IsMultiRegionTrail: t.IsMultiRegionTrail,
482
- HomeRegion: t.HomeRegion,
483
- TrailARN: t.TrailARN,
484
- LogFileValidationEnabled: t.LogFileValidationEnabled,
485
- CloudWatchLogsLogGroupArn: t.CloudWatchLogsLogGroupArn,
486
- CloudWatchLogsRoleArn: t.CloudWatchLogsRoleArn,
487
- HasCustomEventSelectors: t.HasCustomEventSelectors,
488
- HasInsightSelectors: t.HasInsightSelectors,
489
- IsOrganizationTrail: t.IsOrganizationTrail,
490
- })),
491
- };
492
- }
493
-
494
- getTrail(params = {}) {
495
- const name = params.Name;
496
- const trail = this.trails.get(name);
497
- if (!trail) throw Errors.TrailNotFound(name);
498
-
499
- return {
500
- Trail: {
501
- Name: trail.Name,
502
- S3BucketName: trail.S3BucketName,
503
- S3KeyPrefix: trail.S3KeyPrefix,
504
- SnsTopicName: trail.SnsTopicName,
505
- SnsTopicARN: trail.SnsTopicARN,
506
- IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
507
- IsMultiRegionTrail: trail.IsMultiRegionTrail,
508
- HomeRegion: trail.HomeRegion,
509
- TrailARN: trail.TrailARN,
510
- LogFileValidationEnabled: trail.LogFileValidationEnabled,
511
- CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
512
- CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
513
- HasCustomEventSelectors: trail.HasCustomEventSelectors,
514
- HasInsightSelectors: trail.HasInsightSelectors,
515
- IsOrganizationTrail: trail.IsOrganizationTrail,
516
- },
517
- };
518
- }
519
-
520
- getTrailStatus(params = {}) {
521
- const name = params.Name;
522
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
523
-
524
- const status = this.trailStatus.get(name) || {};
525
-
526
- return {
527
- IsLogging: status.isLogging === true,
528
- LatestDeliveryError: status.LatestDeliveryError || null,
529
- LatestNotificationError: status.LatestNotificationError || null,
530
- LatestDeliveryTime: status.LatestDeliveryTime || null,
531
- LatestNotificationTime: status.LatestNotificationTime || null,
532
- StartLoggingTime: status.StartLoggingTime || null,
533
- StopLoggingTime: status.StopLoggingTime || null,
534
- LatestCloudWatchLogsDeliveryError: status.LatestCloudWatchLogsDeliveryError || null,
535
- LatestCloudWatchLogsDeliveryTime: status.LatestCloudWatchLogsDeliveryTime || null,
536
- LatestDeliveryAttemptTime: status.LatestDeliveryAttemptTime || '',
537
- LatestNotificationAttemptTime: status.LatestNotificationAttemptTime || '',
538
- LatestNotificationAttemptSucceeded: status.LatestNotificationAttemptSucceeded || '',
539
- LatestDeliveryAttemptSucceeded: status.LatestDeliveryAttemptSucceeded || '',
540
- TimeLoggingStarted: status.StartLoggingTime || '',
541
- TimeLoggingStopped: status.StopLoggingTime || '',
542
- };
543
- }
544
-
545
- startLogging(params = {}) {
546
- const name = params.Name;
547
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
548
-
549
- const status = this.trailStatus.get(name) || {};
550
- status.isLogging = true;
551
- status.StartLoggingTime = new Date().toISOString();
552
- this.trailStatus.set(name, status);
553
-
554
- this.logger.info(`[CloudTrail] Started logging for trail: ${name}`);
555
- this.save();
556
-
557
- return {};
558
- }
559
-
560
- stopLogging(params = {}) {
561
- const name = params.Name;
562
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
563
-
564
- const status = this.trailStatus.get(name) || {};
565
- status.isLogging = false;
566
- status.StopLoggingTime = new Date().toISOString();
567
- this.trailStatus.set(name, status);
568
-
569
- this.logger.info(`[CloudTrail] Stopped logging for trail: ${name}`);
570
- this.save();
571
-
572
- return {};
573
- }
574
-
575
- // ─── Event Selectors ────────────────────────────────────────────────────────
576
-
577
- getEventSelectors(params = {}) {
578
- const name = params.TrailName;
579
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
580
-
581
- const trail = this.trails.get(name);
582
- const selectors = this.eventSelectors.get(name) || [];
583
-
584
- return {
585
- TrailARN: trail.TrailARN,
586
- EventSelectors: selectors,
587
- };
588
- }
589
-
590
- putEventSelectors(params = {}) {
591
- const name = params.TrailName;
592
- if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
593
-
594
- const selectors = params.EventSelectors || [];
595
- this.eventSelectors.set(name, selectors);
596
-
597
- const trail = this.trails.get(name);
598
- trail.HasCustomEventSelectors = selectors.length > 0;
599
- this.trails.set(name, trail);
600
-
601
- this.logger.info(`[CloudTrail] Event selectors updated for trail: ${name}`);
602
- this.save();
603
-
604
- return {
605
- TrailARN: trail.TrailARN,
606
- EventSelectors: selectors,
607
- };
608
- }
609
-
610
- // ─── LookupEvents ───────────────────────────────────────────────────────────
611
-
612
- lookupEvents(params = {}) {
613
- let events = [...this.events];
614
-
615
- // Filtro por tempo
616
- if (params.StartTime) {
617
- const startTime = new Date(params.StartTime).getTime();
618
- events = events.filter(e => new Date(e.EventTime).getTime() >= startTime);
619
- }
620
- if (params.EndTime) {
621
- const endTime = new Date(params.EndTime).getTime();
622
- events = events.filter(e => new Date(e.EventTime).getTime() <= endTime);
623
- }
624
-
625
- // Filtro por atributo
626
- if (params.LookupAttributes && params.LookupAttributes.length > 0) {
627
- for (const attr of params.LookupAttributes) {
628
- events = events.filter(e => matchesFilter(e, attr));
629
- }
630
- }
631
-
632
- // Ordem: mais recente primeiro
633
- events = events.sort((a, b) => new Date(b.EventTime) - new Date(a.EventTime));
634
-
635
- // Paginação
636
- const maxResults = Math.min(params.MaxResults || MAX_RESULTS_DEFAULT, MAX_RESULTS_MAX);
637
- let startIdx = 0;
638
-
639
- if (params.NextToken) {
640
- try {
641
- startIdx = parseInt(Buffer.from(params.NextToken, 'base64').toString(), 10);
642
- } catch {
643
- startIdx = 0;
644
- }
645
- }
646
-
647
- const page = events.slice(startIdx, startIdx + maxResults);
648
- const nextIdx = startIdx + maxResults;
649
- const nextToken = nextIdx < events.length
650
- ? Buffer.from(String(nextIdx)).toString('base64')
651
- : null;
652
-
653
- return {
654
- Events: page,
655
- NextToken: nextToken,
656
- };
657
- }
658
-
659
- // ─── Tags ───────────────────────────────────────────────────────────────────
660
-
661
- addTags(params = {}) {
662
- const arn = params.ResourceId;
663
- // Encontra trail pelo ARN ou nome
664
- for (const [name, trail] of this.trails) {
665
- if (trail.TrailARN === arn || trail.Name === arn) {
666
- trail.Tags = trail.Tags || [];
667
- for (const tag of (params.TagsList || [])) {
668
- const existing = trail.Tags.findIndex(t => t.Key === tag.Key);
669
- if (existing >= 0) trail.Tags[existing] = tag;
670
- else trail.Tags.push(tag);
671
- }
672
- this.trails.set(name, trail);
673
- this.save();
674
- return {};
675
- }
676
- }
677
- throw Errors.TrailNotFound(arn);
678
- }
679
-
680
- removeTags(params = {}) {
681
- const arn = params.ResourceId;
682
- for (const [name, trail] of this.trails) {
683
- if (trail.TrailARN === arn || trail.Name === arn) {
684
- const keysToRemove = (params.TagsList || []).map(t => t.Key);
685
- trail.Tags = (trail.Tags || []).filter(t => !keysToRemove.includes(t.Key));
686
- this.trails.set(name, trail);
687
- this.save();
688
- return {};
689
- }
690
- }
691
- throw Errors.TrailNotFound(arn);
692
- }
693
-
694
- listTags(params = {}) {
695
- const result = [];
696
- for (const arn of (params.ResourceIdList || [])) {
697
- for (const [, trail] of this.trails) {
698
- if (trail.TrailARN === arn || trail.Name === arn) {
699
- result.push({ ResourceId: trail.TrailARN, TagsList: trail.Tags || [] });
700
- break;
701
- }
702
- }
703
- }
704
- return { ResourceTagList: result };
705
- }
706
-
707
- // ─── Admin ──────────────────────────────────────────────────────────────────
708
-
709
- getStatus() {
710
- return {
711
- service: 'cloudtrail',
712
- trails: this.trails.size,
713
- events: this.events.length,
714
- activeTrails: [...this.trailStatus.values()].filter(s => s.isLogging).length,
715
- };
716
- }
717
- }
718
-
719
- module.exports = { CloudTrailSimulator };
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview CloudTrail Simulator
5
+ *
6
+ * Suporta:
7
+ * Trails:
8
+ * - CreateTrail / DeleteTrail / UpdateTrail
9
+ * - DescribeTrails / GetTrail / GetTrailStatus
10
+ * - StartLogging / StopLogging
11
+ *
12
+ * Events:
13
+ * - LookupEvents (filtro por ResourceName, EventName, Username, etc.)
14
+ * - GetEventSelectors / PutEventSelectors
15
+ *
16
+ * Integração:
17
+ * - Registro automático de API calls dos outros serviços
18
+ * - Entrega de logs para S3 (simulado)
19
+ * - Integração com CloudWatch Logs
20
+ *
21
+ * Persistência via LocalStore
22
+ */
23
+
24
+ const { randomUUID } = require('crypto');
25
+
26
+ // ─── Erros tipados ────────────────────────────────────────────────────────────
27
+
28
+ class CloudTrailError extends Error {
29
+ constructor(code, message, statusCode = 400) {
30
+ super(message);
31
+ this.code = code;
32
+ this.statusCode = statusCode;
33
+ }
34
+ }
35
+
36
+ const Errors = {
37
+ TrailNotFound: (name) =>
38
+ new CloudTrailError('TrailNotFoundException', `Unknown trail: ${name}`, 404),
39
+ TrailAlreadyExists: (name) =>
40
+ new CloudTrailError('TrailAlreadyExistsException', `Trail already exists: ${name}`, 400),
41
+ InvalidTrailName: (name) =>
42
+ new CloudTrailError('InvalidTrailNameException', `Invalid trail name: ${name}`, 400),
43
+ InvalidParameter: (msg) =>
44
+ new CloudTrailError('InvalidParameterCombinationException', msg, 400),
45
+ MaximumNumberOfTrails: () =>
46
+ new CloudTrailError('MaximumNumberOfTrailsExceededException', 'Maximum number of trails exceeded (max: 5)', 400),
47
+ S3BucketNotFound: (bucket) =>
48
+ new CloudTrailError('S3BucketDoesNotExistException', `S3 bucket does not exist: ${bucket}`, 400),
49
+ InsuficientSnsTopicPolicy: () =>
50
+ new CloudTrailError('InsuficientSnsTopicPolicyException', 'Insufficient SNS topic policy', 400),
51
+ };
52
+
53
+ // ─── Constantes ───────────────────────────────────────────────────────────────
54
+
55
+ const REGION = 'us-east-1';
56
+ const ACCOUNT_ID = '000000000000';
57
+ const MAX_TRAILS = 5;
58
+ const MAX_RESULTS_DEFAULT = 50;
59
+ const MAX_RESULTS_MAX = 50;
60
+
61
+ // ─── Utilitários ──────────────────────────────────────────────────────────────
62
+
63
+ function trailArn(name) {
64
+ return `arn:aws:cloudtrail:${REGION}:${ACCOUNT_ID}:trail/${name}`;
65
+ }
66
+
67
+ function validateTrailName(name) {
68
+ if (!name || typeof name !== 'string') throw Errors.InvalidTrailName(name);
69
+ if (name.length < 3 || name.length > 128) throw Errors.InvalidTrailName(name);
70
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) throw Errors.InvalidTrailName(name);
71
+ return true;
72
+ }
73
+
74
+ function matchesFilter(event, filter) {
75
+ if (!filter) return true;
76
+
77
+ if (filter.AttributeKey && filter.AttributeValue) {
78
+ const key = filter.AttributeKey;
79
+ const value = filter.AttributeValue;
80
+
81
+ switch (key) {
82
+ case 'EventId':
83
+ if (event.EventId !== value) return false;
84
+ break;
85
+ case 'EventName':
86
+ if (event.EventName !== value) return false;
87
+ break;
88
+ case 'ReadOnly':
89
+ if (String(event.ReadOnly) !== value) return false;
90
+ break;
91
+ case 'Username':
92
+ if (event.Username !== value) return false;
93
+ break;
94
+ case 'ResourceType':
95
+ if (!event.Resources || !event.Resources.some(r => r.ResourceType === value)) return false;
96
+ break;
97
+ case 'ResourceName':
98
+ if (!event.Resources || !event.Resources.some(r => r.ResourceName === value)) return false;
99
+ break;
100
+ case 'EventSource':
101
+ if (event.EventSource !== value) return false;
102
+ break;
103
+ case 'AccessKeyId':
104
+ if (event.AccessKeyId !== value) return false;
105
+ break;
106
+ default:
107
+ break;
108
+ }
109
+ }
110
+
111
+ return true;
112
+ }
113
+
114
+ // ─── Simulador Principal ──────────────────────────────────────────────────────
115
+
116
+ class CloudTrailSimulator {
117
+ /**
118
+ * @param {Object} config - Global simulator config
119
+ * @param {Object} store - LocalStore instance
120
+ * @param {Object} logger - Logger instance
121
+ */
122
+ constructor(config, store, logger) {
123
+ this.config = config;
124
+ this.store = store;
125
+ this.logger = logger;
126
+
127
+ // Trails: Map<name, TrailConfig>
128
+ this.trails = new Map();
129
+
130
+ // Trail status: Map<name, { isLogging, latestDelivery, latestNotification }>
131
+ this.trailStatus = new Map();
132
+
133
+ // Event selectors: Map<name, EventSelector[]>
134
+ this.eventSelectors = new Map();
135
+
136
+ // Events log: Array de CloudTrail events
137
+ this.events = [];
138
+
139
+ // Injeções cross-service
140
+ this.s3Simulator = null;
141
+ this.cloudwatchSimulator = null;
142
+ }
143
+
144
+ // ─── Persistência ───────────────────────────────────────────────────────────
145
+
146
+ async load() {
147
+ try {
148
+ const data = await this.store.load('cloudtrail');
149
+ if (data) {
150
+ if (data.trails) this.trails = new Map(Object.entries(data.trails));
151
+ if (data.trailStatus) this.trailStatus = new Map(Object.entries(data.trailStatus));
152
+ if (data.eventSelectors) this.eventSelectors = new Map(Object.entries(data.eventSelectors));
153
+ if (data.events) this.events = data.events;
154
+ this.logger.info(`[CloudTrail] Loaded ${this.trails.size} trails, ${this.events.length} events`);
155
+ }
156
+ } catch (err) {
157
+ this.logger.warn('[CloudTrail] No persisted data found, starting fresh');
158
+ }
159
+
160
+ // Garante que existe um trail padrão com logging ativo
161
+ if (this.trails.size === 0) {
162
+ const defaultName = 'local-default-trail';
163
+ this.trails.set(defaultName, { Name: defaultName, S3BucketName: null });
164
+ this.trailStatus.set(defaultName, { isLogging: true });
165
+ // Event selectors: captura management events + todos os data events
166
+ this.eventSelectors.set(defaultName, [
167
+ {
168
+ ReadWriteType: 'All',
169
+ IncludeManagementEvents: true,
170
+ DataResources: [
171
+ { Type: 'AWS::S3::Object', Values: ['arn:aws:s3:::*'] },
172
+ { Type: 'AWS::DynamoDB::Table', Values: ['arn:aws:dynamodb:::*'] },
173
+ { Type: 'AWS::Lambda::Function', Values: ['arn:aws:lambda:::*'] },
174
+ { Type: 'AWS::APIGateway::Stage', Values: ['arn:aws:execute-api:::*'] },
175
+ { Type: 'AWS::SecretsManager::Secret', Values: ['arn:aws:secretsmanager:::*'] },
176
+ { Type: 'AWS::SSM::Parameter', Values: ['arn:aws:ssm:::*'] },
177
+ { Type: 'AWS::Cognito::UserPool', Values: ['arn:aws:cognito-idp:::*'] },
178
+ { Type: 'AWS::KMS::Key', Values: ['arn:aws:kms:::*'] },
179
+ ],
180
+ },
181
+ ]);
182
+ this.logger.debug('[CloudTrail] Trail padrão criado com logging ativo');
183
+ }
184
+ }
185
+
186
+ async save() {
187
+ try {
188
+ const data = {
189
+ trails: Object.fromEntries(this.trails),
190
+ trailStatus: Object.fromEntries(this.trailStatus),
191
+ eventSelectors: Object.fromEntries(this.eventSelectors),
192
+ events: this.events.slice(-10000), // mantém os últimos 10.000 eventos
193
+ };
194
+ await this.store.save('cloudtrail', data);
195
+ } catch (err) {
196
+ this.logger.error('[CloudTrail] Failed to save data:', err.message);
197
+ }
198
+ }
199
+
200
+ reset() {
201
+ this.trails.clear();
202
+ this.trailStatus.clear();
203
+ this.eventSelectors.clear();
204
+ this.events = [];
205
+ }
206
+
207
+ // ─── Registro de Evento (uso interno / cross-service) ───────────────────────
208
+
209
+ /**
210
+ * Registra um API call como evento CloudTrail.
211
+ * Chamado pelos outros serviços via injeção.
212
+ *
213
+ * @param {Object} params
214
+ * @param {string} params.eventName - Ex: "CreateBucket"
215
+ * @param {string} params.eventSource - Ex: "s3.amazonaws.com"
216
+ * @param {string} params.username - Ex: "test-user"
217
+ * @param {boolean} params.readOnly - true se operação de leitura
218
+ * @param {Array} params.resources - [{ ResourceType, ResourceName }]
219
+ * @param {Object} params.requestParameters - Parâmetros da requisição
220
+ * @param {Object} params.responseElements - Resposta da operação
221
+ * @param {string} params.sourceIPAddress - IP da requisição
222
+ * @param {string} params.userAgent - User-Agent da requisição
223
+ */
224
+ recordEvent(params = {}) {
225
+ const event = {
226
+ EventId: randomUUID(),
227
+ EventName: params.eventName || 'UnknownEvent',
228
+ EventSource: params.eventSource || 'local.amazonaws.com',
229
+ EventTime: new Date().toISOString(),
230
+ Username: params.username || 'local-user',
231
+ ReadOnly: params.readOnly !== undefined ? params.readOnly : false,
232
+ AccessKeyId: params.accessKeyId || 'AKIAIOSFODNN7EXAMPLE',
233
+ Resources: params.resources || [],
234
+ RequestParameters: params.requestParameters || null,
235
+ ResponseElements: params.responseElements || null,
236
+ SourceIPAddress: params.sourceIPAddress || '127.0.0.1',
237
+ UserAgent: params.userAgent || 'aws-local-simulator',
238
+ CloudTrailEvent: JSON.stringify({
239
+ eventVersion: '1.08',
240
+ userIdentity: {
241
+ type: 'IAMUser',
242
+ principalId: 'AIDIOSFODNN7EXAMPLE',
243
+ arn: `arn:aws:iam::${ACCOUNT_ID}:user/${params.username || 'local-user'}`,
244
+ accountId: ACCOUNT_ID,
245
+ accessKeyId: params.accessKeyId || 'AKIAIOSFODNN7EXAMPLE',
246
+ userName: params.username || 'local-user',
247
+ },
248
+ eventTime: new Date().toISOString(),
249
+ eventSource: params.eventSource || 'local.amazonaws.com',
250
+ eventName: params.eventName || 'UnknownEvent',
251
+ awsRegion: REGION,
252
+ sourceIPAddress: params.sourceIPAddress || '127.0.0.1',
253
+ userAgent: params.userAgent || 'aws-local-simulator',
254
+ requestParameters: params.requestParameters || null,
255
+ responseElements: params.responseElements || null,
256
+ requestID: randomUUID(),
257
+ eventID: randomUUID(),
258
+ readOnly: params.readOnly !== undefined ? params.readOnly : false,
259
+ resources: params.resources || [],
260
+ eventType: 'AwsApiCall',
261
+ managementEvent: true,
262
+ recipientAccountId: ACCOUNT_ID,
263
+ }),
264
+ };
265
+
266
+ this.events.push(event);
267
+
268
+ // Persiste no disco
269
+ this.save();
270
+
271
+ // Entrega para S3 (simula escrita de log file)
272
+ this._deliverToS3(event);
273
+
274
+ // Entrega para CloudWatch Logs se configurado
275
+ this._deliverToCloudWatch(event);
276
+
277
+ return event;
278
+ }
279
+
280
+ async _deliverToS3(event) {
281
+ // Verifica se há trails ativos com S3 configurado
282
+ for (const [name, trail] of this.trails) {
283
+ const status = this.trailStatus.get(name) || {};
284
+ if (!status.isLogging) continue;
285
+ if (!trail.S3BucketName) continue;
286
+
287
+ if (this.s3Simulator) {
288
+ try {
289
+ const date = new Date(event.EventTime);
290
+ const key = `${trail.S3KeyPrefix || 'AWSLogs/'}${ACCOUNT_ID}/CloudTrail/${REGION}/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${randomUUID()}.json`;
291
+
292
+ await this.s3Simulator.putObject({
293
+ Bucket: trail.S3BucketName,
294
+ Key: key,
295
+ Body: JSON.stringify({ Records: [JSON.parse(event.CloudTrailEvent)] }),
296
+ ContentType: 'application/json',
297
+ });
298
+
299
+ // Atualiza status
300
+ const s = this.trailStatus.get(name) || {};
301
+ s.LatestDeliveryTime = new Date().toISOString();
302
+ s.LatestDeliveryAttemptTime = new Date().toISOString();
303
+ this.trailStatus.set(name, s);
304
+ } catch (err) {
305
+ this.logger.warn(`[CloudTrail] Failed to deliver to S3 for trail ${name}: ${err.message}`);
306
+ const s = this.trailStatus.get(name) || {};
307
+ s.LatestDeliveryError = err.message;
308
+ s.LatestDeliveryAttemptTime = new Date().toISOString();
309
+ this.trailStatus.set(name, s);
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ _deliverToCloudWatch(event) {
316
+ for (const [name, trail] of this.trails) {
317
+ const status = this.trailStatus.get(name) || {};
318
+ if (!status.isLogging) continue;
319
+ if (!trail.CloudWatchLogsLogGroupArn) continue;
320
+
321
+ if (this.cloudwatchSimulator) {
322
+ try {
323
+ // Extrai o log group name do ARN
324
+ const match = trail.CloudWatchLogsLogGroupArn.match(/log-group:([^:]+)/);
325
+ if (!match) continue;
326
+ const logGroupName = match[1];
327
+
328
+ this.cloudwatchSimulator.putLambdaLogs(logGroupName, 'cloudtrail', [
329
+ { timestamp: Date.now(), message: event.CloudTrailEvent },
330
+ ]);
331
+ } catch (err) {
332
+ this.logger.warn(`[CloudTrail] Failed to deliver to CloudWatch for trail ${name}: ${err.message}`);
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // ─── Trails ─────────────────────────────────────────────────────────────────
339
+
340
+ createTrail(params = {}) {
341
+ const name = params.Name;
342
+ validateTrailName(name);
343
+
344
+ if (this.trails.has(name)) throw Errors.TrailAlreadyExists(name);
345
+ if (this.trails.size >= MAX_TRAILS) throw Errors.MaximumNumberOfTrails();
346
+
347
+ const trail = {
348
+ Name: name,
349
+ S3BucketName: params.S3BucketName || null,
350
+ S3KeyPrefix: params.S3KeyPrefix || null,
351
+ SnsTopicName: params.SnsTopicName || null,
352
+ SnsTopicARN: params.SnsTopicName
353
+ ? `arn:aws:sns:${REGION}:${ACCOUNT_ID}:${params.SnsTopicName}`
354
+ : null,
355
+ IncludeGlobalServiceEvents: params.IncludeGlobalServiceEvents !== false,
356
+ IsMultiRegionTrail: params.IsMultiRegionTrail === true,
357
+ HomeRegion: REGION,
358
+ TrailARN: trailArn(name),
359
+ LogFileValidationEnabled: params.EnableLogFileValidation === true,
360
+ CloudWatchLogsLogGroupArn: params.CloudWatchLogsLogGroupArn || null,
361
+ CloudWatchLogsRoleArn: params.CloudWatchLogsRoleArn || null,
362
+ HasCustomEventSelectors: false,
363
+ HasInsightSelectors: false,
364
+ IsOrganizationTrail: false,
365
+ CreatedAt: new Date().toISOString(),
366
+ Tags: params.TagsList || [],
367
+ };
368
+
369
+ this.trails.set(name, trail);
370
+ this.trailStatus.set(name, {
371
+ isLogging: false,
372
+ LatestDeliveryTime: null,
373
+ LatestDeliveryAttemptTime: null,
374
+ LatestDeliveryError: null,
375
+ LatestNotificationTime: null,
376
+ LatestNotificationAttemptTime: null,
377
+ LatestNotificationError: null,
378
+ LatestCloudWatchLogsDeliveryTime: null,
379
+ LatestCloudWatchLogsDeliveryError: null,
380
+ StartLoggingTime: null,
381
+ StopLoggingTime: null,
382
+ });
383
+
384
+ // Seletores padrão
385
+ this.eventSelectors.set(name, [{
386
+ ReadWriteType: 'All',
387
+ IncludeManagementEvents: true,
388
+ DataResources: [],
389
+ ExcludeManagementEventSources: [],
390
+ }]);
391
+
392
+ this.logger.info(`[CloudTrail] Trail created: ${name}`);
393
+ this.save();
394
+
395
+ return {
396
+ Name: trail.Name,
397
+ S3BucketName: trail.S3BucketName,
398
+ S3KeyPrefix: trail.S3KeyPrefix,
399
+ SnsTopicName: trail.SnsTopicName,
400
+ SnsTopicARN: trail.SnsTopicARN,
401
+ IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
402
+ IsMultiRegionTrail: trail.IsMultiRegionTrail,
403
+ TrailARN: trail.TrailARN,
404
+ LogFileValidationEnabled: trail.LogFileValidationEnabled,
405
+ CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
406
+ CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
407
+ };
408
+ }
409
+
410
+ updateTrail(params = {}) {
411
+ const name = params.Name;
412
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
413
+
414
+ const trail = this.trails.get(name);
415
+
416
+ if (params.S3BucketName !== undefined) trail.S3BucketName = params.S3BucketName;
417
+ if (params.S3KeyPrefix !== undefined) trail.S3KeyPrefix = params.S3KeyPrefix;
418
+ if (params.SnsTopicName !== undefined) {
419
+ trail.SnsTopicName = params.SnsTopicName;
420
+ trail.SnsTopicARN = params.SnsTopicName
421
+ ? `arn:aws:sns:${REGION}:${ACCOUNT_ID}:${params.SnsTopicName}`
422
+ : null;
423
+ }
424
+ if (params.IncludeGlobalServiceEvents !== undefined) trail.IncludeGlobalServiceEvents = params.IncludeGlobalServiceEvents;
425
+ if (params.IsMultiRegionTrail !== undefined) trail.IsMultiRegionTrail = params.IsMultiRegionTrail;
426
+ if (params.EnableLogFileValidation !== undefined) trail.LogFileValidationEnabled = params.EnableLogFileValidation;
427
+ if (params.CloudWatchLogsLogGroupArn !== undefined) trail.CloudWatchLogsLogGroupArn = params.CloudWatchLogsLogGroupArn;
428
+ if (params.CloudWatchLogsRoleArn !== undefined) trail.CloudWatchLogsRoleArn = params.CloudWatchLogsRoleArn;
429
+
430
+ this.trails.set(name, trail);
431
+ this.logger.info(`[CloudTrail] Trail updated: ${name}`);
432
+ this.save();
433
+
434
+ return {
435
+ Name: trail.Name,
436
+ S3BucketName: trail.S3BucketName,
437
+ S3KeyPrefix: trail.S3KeyPrefix,
438
+ SnsTopicName: trail.SnsTopicName,
439
+ SnsTopicARN: trail.SnsTopicARN,
440
+ IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
441
+ IsMultiRegionTrail: trail.IsMultiRegionTrail,
442
+ TrailARN: trail.TrailARN,
443
+ LogFileValidationEnabled: trail.LogFileValidationEnabled,
444
+ CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
445
+ CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
446
+ };
447
+ }
448
+
449
+ deleteTrail(params = {}) {
450
+ const name = params.Name;
451
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
452
+
453
+ this.trails.delete(name);
454
+ this.trailStatus.delete(name);
455
+ this.eventSelectors.delete(name);
456
+
457
+ this.logger.info(`[CloudTrail] Trail deleted: ${name}`);
458
+ this.save();
459
+
460
+ return {};
461
+ }
462
+
463
+ describeTrails(params = {}) {
464
+ const includeShadow = params.includeShadowTrails !== false;
465
+ let trailList = [...this.trails.values()];
466
+
467
+ if (params.trailNameList && params.trailNameList.length > 0) {
468
+ trailList = trailList.filter(t =>
469
+ params.trailNameList.includes(t.Name) || params.trailNameList.includes(t.TrailARN)
470
+ );
471
+ }
472
+
473
+ return {
474
+ trailList: trailList.map(t => ({
475
+ Name: t.Name,
476
+ S3BucketName: t.S3BucketName,
477
+ S3KeyPrefix: t.S3KeyPrefix,
478
+ SnsTopicName: t.SnsTopicName,
479
+ SnsTopicARN: t.SnsTopicARN,
480
+ IncludeGlobalServiceEvents: t.IncludeGlobalServiceEvents,
481
+ IsMultiRegionTrail: t.IsMultiRegionTrail,
482
+ HomeRegion: t.HomeRegion,
483
+ TrailARN: t.TrailARN,
484
+ LogFileValidationEnabled: t.LogFileValidationEnabled,
485
+ CloudWatchLogsLogGroupArn: t.CloudWatchLogsLogGroupArn,
486
+ CloudWatchLogsRoleArn: t.CloudWatchLogsRoleArn,
487
+ HasCustomEventSelectors: t.HasCustomEventSelectors,
488
+ HasInsightSelectors: t.HasInsightSelectors,
489
+ IsOrganizationTrail: t.IsOrganizationTrail,
490
+ })),
491
+ };
492
+ }
493
+
494
+ getTrail(params = {}) {
495
+ const name = params.Name;
496
+ const trail = this.trails.get(name);
497
+ if (!trail) throw Errors.TrailNotFound(name);
498
+
499
+ return {
500
+ Trail: {
501
+ Name: trail.Name,
502
+ S3BucketName: trail.S3BucketName,
503
+ S3KeyPrefix: trail.S3KeyPrefix,
504
+ SnsTopicName: trail.SnsTopicName,
505
+ SnsTopicARN: trail.SnsTopicARN,
506
+ IncludeGlobalServiceEvents: trail.IncludeGlobalServiceEvents,
507
+ IsMultiRegionTrail: trail.IsMultiRegionTrail,
508
+ HomeRegion: trail.HomeRegion,
509
+ TrailARN: trail.TrailARN,
510
+ LogFileValidationEnabled: trail.LogFileValidationEnabled,
511
+ CloudWatchLogsLogGroupArn: trail.CloudWatchLogsLogGroupArn,
512
+ CloudWatchLogsRoleArn: trail.CloudWatchLogsRoleArn,
513
+ HasCustomEventSelectors: trail.HasCustomEventSelectors,
514
+ HasInsightSelectors: trail.HasInsightSelectors,
515
+ IsOrganizationTrail: trail.IsOrganizationTrail,
516
+ },
517
+ };
518
+ }
519
+
520
+ getTrailStatus(params = {}) {
521
+ const name = params.Name;
522
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
523
+
524
+ const status = this.trailStatus.get(name) || {};
525
+
526
+ return {
527
+ IsLogging: status.isLogging === true,
528
+ LatestDeliveryError: status.LatestDeliveryError || null,
529
+ LatestNotificationError: status.LatestNotificationError || null,
530
+ LatestDeliveryTime: status.LatestDeliveryTime || null,
531
+ LatestNotificationTime: status.LatestNotificationTime || null,
532
+ StartLoggingTime: status.StartLoggingTime || null,
533
+ StopLoggingTime: status.StopLoggingTime || null,
534
+ LatestCloudWatchLogsDeliveryError: status.LatestCloudWatchLogsDeliveryError || null,
535
+ LatestCloudWatchLogsDeliveryTime: status.LatestCloudWatchLogsDeliveryTime || null,
536
+ LatestDeliveryAttemptTime: status.LatestDeliveryAttemptTime || '',
537
+ LatestNotificationAttemptTime: status.LatestNotificationAttemptTime || '',
538
+ LatestNotificationAttemptSucceeded: status.LatestNotificationAttemptSucceeded || '',
539
+ LatestDeliveryAttemptSucceeded: status.LatestDeliveryAttemptSucceeded || '',
540
+ TimeLoggingStarted: status.StartLoggingTime || '',
541
+ TimeLoggingStopped: status.StopLoggingTime || '',
542
+ };
543
+ }
544
+
545
+ startLogging(params = {}) {
546
+ const name = params.Name;
547
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
548
+
549
+ const status = this.trailStatus.get(name) || {};
550
+ status.isLogging = true;
551
+ status.StartLoggingTime = new Date().toISOString();
552
+ this.trailStatus.set(name, status);
553
+
554
+ this.logger.info(`[CloudTrail] Started logging for trail: ${name}`);
555
+ this.save();
556
+
557
+ return {};
558
+ }
559
+
560
+ stopLogging(params = {}) {
561
+ const name = params.Name;
562
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
563
+
564
+ const status = this.trailStatus.get(name) || {};
565
+ status.isLogging = false;
566
+ status.StopLoggingTime = new Date().toISOString();
567
+ this.trailStatus.set(name, status);
568
+
569
+ this.logger.info(`[CloudTrail] Stopped logging for trail: ${name}`);
570
+ this.save();
571
+
572
+ return {};
573
+ }
574
+
575
+ // ─── Event Selectors ────────────────────────────────────────────────────────
576
+
577
+ getEventSelectors(params = {}) {
578
+ const name = params.TrailName;
579
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
580
+
581
+ const trail = this.trails.get(name);
582
+ const selectors = this.eventSelectors.get(name) || [];
583
+
584
+ return {
585
+ TrailARN: trail.TrailARN,
586
+ EventSelectors: selectors,
587
+ };
588
+ }
589
+
590
+ putEventSelectors(params = {}) {
591
+ const name = params.TrailName;
592
+ if (!this.trails.has(name)) throw Errors.TrailNotFound(name);
593
+
594
+ const selectors = params.EventSelectors || [];
595
+ this.eventSelectors.set(name, selectors);
596
+
597
+ const trail = this.trails.get(name);
598
+ trail.HasCustomEventSelectors = selectors.length > 0;
599
+ this.trails.set(name, trail);
600
+
601
+ this.logger.info(`[CloudTrail] Event selectors updated for trail: ${name}`);
602
+ this.save();
603
+
604
+ return {
605
+ TrailARN: trail.TrailARN,
606
+ EventSelectors: selectors,
607
+ };
608
+ }
609
+
610
+ // ─── LookupEvents ───────────────────────────────────────────────────────────
611
+
612
+ lookupEvents(params = {}) {
613
+ let events = [...this.events];
614
+
615
+ // Filtro por tempo
616
+ if (params.StartTime) {
617
+ const startTime = new Date(params.StartTime).getTime();
618
+ events = events.filter(e => new Date(e.EventTime).getTime() >= startTime);
619
+ }
620
+ if (params.EndTime) {
621
+ const endTime = new Date(params.EndTime).getTime();
622
+ events = events.filter(e => new Date(e.EventTime).getTime() <= endTime);
623
+ }
624
+
625
+ // Filtro por atributo
626
+ if (params.LookupAttributes && params.LookupAttributes.length > 0) {
627
+ for (const attr of params.LookupAttributes) {
628
+ events = events.filter(e => matchesFilter(e, attr));
629
+ }
630
+ }
631
+
632
+ // Ordem: mais recente primeiro
633
+ events = events.sort((a, b) => new Date(b.EventTime) - new Date(a.EventTime));
634
+
635
+ // Paginação
636
+ const maxResults = Math.min(params.MaxResults || MAX_RESULTS_DEFAULT, MAX_RESULTS_MAX);
637
+ let startIdx = 0;
638
+
639
+ if (params.NextToken) {
640
+ try {
641
+ startIdx = parseInt(Buffer.from(params.NextToken, 'base64').toString(), 10);
642
+ } catch {
643
+ startIdx = 0;
644
+ }
645
+ }
646
+
647
+ const page = events.slice(startIdx, startIdx + maxResults);
648
+ const nextIdx = startIdx + maxResults;
649
+ const nextToken = nextIdx < events.length
650
+ ? Buffer.from(String(nextIdx)).toString('base64')
651
+ : null;
652
+
653
+ return {
654
+ Events: page,
655
+ NextToken: nextToken,
656
+ };
657
+ }
658
+
659
+ // ─── Tags ───────────────────────────────────────────────────────────────────
660
+
661
+ addTags(params = {}) {
662
+ const arn = params.ResourceId;
663
+ // Encontra trail pelo ARN ou nome
664
+ for (const [name, trail] of this.trails) {
665
+ if (trail.TrailARN === arn || trail.Name === arn) {
666
+ trail.Tags = trail.Tags || [];
667
+ for (const tag of (params.TagsList || [])) {
668
+ const existing = trail.Tags.findIndex(t => t.Key === tag.Key);
669
+ if (existing >= 0) trail.Tags[existing] = tag;
670
+ else trail.Tags.push(tag);
671
+ }
672
+ this.trails.set(name, trail);
673
+ this.save();
674
+ return {};
675
+ }
676
+ }
677
+ throw Errors.TrailNotFound(arn);
678
+ }
679
+
680
+ removeTags(params = {}) {
681
+ const arn = params.ResourceId;
682
+ for (const [name, trail] of this.trails) {
683
+ if (trail.TrailARN === arn || trail.Name === arn) {
684
+ const keysToRemove = (params.TagsList || []).map(t => t.Key);
685
+ trail.Tags = (trail.Tags || []).filter(t => !keysToRemove.includes(t.Key));
686
+ this.trails.set(name, trail);
687
+ this.save();
688
+ return {};
689
+ }
690
+ }
691
+ throw Errors.TrailNotFound(arn);
692
+ }
693
+
694
+ listTags(params = {}) {
695
+ const result = [];
696
+ for (const arn of (params.ResourceIdList || [])) {
697
+ for (const [, trail] of this.trails) {
698
+ if (trail.TrailARN === arn || trail.Name === arn) {
699
+ result.push({ ResourceId: trail.TrailARN, TagsList: trail.Tags || [] });
700
+ break;
701
+ }
702
+ }
703
+ }
704
+ return { ResourceTagList: result };
705
+ }
706
+
707
+ // ─── Admin ──────────────────────────────────────────────────────────────────
708
+
709
+ getStatus() {
710
+ return {
711
+ service: 'cloudtrail',
712
+ trails: this.trails.size,
713
+ events: this.events.length,
714
+ activeTrails: [...this.trailStatus.values()].filter(s => s.isLogging).length,
715
+ };
716
+ }
717
+ }
718
+
719
+ module.exports = { CloudTrailSimulator };