@gugananuvem/aws-local-simulator 1.0.12 → 1.0.14

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 (57) hide show
  1. package/README.md +235 -11
  2. package/package.json +12 -2
  3. package/src/config/default-config.js +1 -0
  4. package/src/index.js +18 -2
  5. package/src/server.js +36 -32
  6. package/src/services/apigateway/index.js +5 -0
  7. package/src/services/apigateway/server.js +20 -0
  8. package/src/services/apigateway/simulator.js +13 -3
  9. package/src/services/athena/index.js +75 -0
  10. package/src/services/athena/server.js +101 -0
  11. package/src/services/athena/simulador.js +998 -0
  12. package/src/services/athena/simulator.js +346 -0
  13. package/src/services/cloudformation/index.js +106 -0
  14. package/src/services/cloudformation/server.js +417 -0
  15. package/src/services/cloudformation/simulador.js +1045 -0
  16. package/src/services/cloudtrail/index.js +84 -0
  17. package/src/services/cloudtrail/server.js +235 -0
  18. package/src/services/cloudtrail/simulador.js +719 -0
  19. package/src/services/cloudwatch/index.js +84 -0
  20. package/src/services/cloudwatch/server.js +366 -0
  21. package/src/services/cloudwatch/simulador.js +1173 -0
  22. package/src/services/cognito/index.js +5 -0
  23. package/src/services/cognito/simulator.js +4 -0
  24. package/src/services/config/index.js +96 -0
  25. package/src/services/config/server.js +215 -0
  26. package/src/services/config/simulador.js +1260 -0
  27. package/src/services/dynamodb/index.js +7 -3
  28. package/src/services/dynamodb/server.js +4 -2
  29. package/src/services/dynamodb/simulator.js +39 -29
  30. package/src/services/eventbridge/index.js +55 -51
  31. package/src/services/eventbridge/server.js +209 -0
  32. package/src/services/eventbridge/simulator.js +684 -0
  33. package/src/services/index.js +30 -4
  34. package/src/services/kms/index.js +75 -0
  35. package/src/services/kms/server.js +67 -0
  36. package/src/services/kms/simulator.js +324 -0
  37. package/src/services/lambda/index.js +5 -0
  38. package/src/services/lambda/simulator.js +48 -38
  39. package/src/services/parameter-store/index.js +80 -0
  40. package/src/services/parameter-store/server.js +50 -0
  41. package/src/services/parameter-store/simulator.js +201 -0
  42. package/src/services/s3/index.js +7 -3
  43. package/src/services/s3/server.js +20 -13
  44. package/src/services/s3/simulator.js +163 -407
  45. package/src/services/secret-manager/index.js +80 -0
  46. package/src/services/secret-manager/server.js +50 -0
  47. package/src/services/secret-manager/simulator.js +171 -0
  48. package/src/services/sns/index.js +55 -42
  49. package/src/services/sns/server.js +580 -0
  50. package/src/services/sns/simulator.js +1482 -0
  51. package/src/services/sqs/index.js +2 -4
  52. package/src/services/sqs/server.js +4 -2
  53. package/src/services/xray/index.js +83 -0
  54. package/src/services/xray/server.js +308 -0
  55. package/src/services/xray/simulador.js +994 -0
  56. package/src/utils/cloudtrail-audit.js +129 -0
  57. package/src/utils/local-store.js +18 -2
@@ -0,0 +1,1260 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview AWS Config Simulator
5
+ *
6
+ * Suporta:
7
+ * Configuration Recorders:
8
+ * - PutConfigurationRecorder / DeleteConfigurationRecorder
9
+ * - DescribeConfigurationRecorders / DescribeConfigurationRecorderStatus
10
+ * - StartConfigurationRecorder / StopConfigurationRecorder
11
+ *
12
+ * Delivery Channels:
13
+ * - PutDeliveryChannel / DeleteDeliveryChannel
14
+ * - DescribeDeliveryChannels / DescribeDeliveryChannelStatus
15
+ * - DeliverConfigSnapshot
16
+ *
17
+ * Config Rules:
18
+ * - PutConfigRule / DeleteConfigRule
19
+ * - DescribeConfigRules / DescribeConfigRuleEvaluationStatus
20
+ * - StartConfigRulesEvaluation
21
+ * - GetComplianceDetailsByConfigRule
22
+ * - GetComplianceDetailsByResource
23
+ * - GetComplianceSummaryByConfigRule
24
+ * - GetComplianceSummaryByResourceType
25
+ *
26
+ * Resource Configuration:
27
+ * - GetResourceConfigHistory
28
+ * - ListDiscoveredResources
29
+ * - GetDiscoveredResourceCounts
30
+ * - BatchGetResourceConfig
31
+ * - BatchGetAggregateResourceConfig
32
+ *
33
+ * Conformance Packs:
34
+ * - PutConformancePack / DeleteConformancePack
35
+ * - DescribeConformancePacks / DescribeConformancePackStatus
36
+ * - GetConformancePackComplianceSummary
37
+ *
38
+ * Aggregators:
39
+ * - PutConfigurationAggregator / DeleteConfigurationAggregator
40
+ * - DescribeConfigurationAggregators
41
+ *
42
+ * Remediation:
43
+ * - PutRemediationConfigurations / DeleteRemediationConfigurations
44
+ * - DescribeRemediationConfigurations
45
+ * - StartRemediationExecution
46
+ *
47
+ * Tags:
48
+ * - TagResource / UntagResource / ListTagsForResource
49
+ *
50
+ * Persistência via LocalStore
51
+ */
52
+
53
+ const { randomUUID } = require('crypto');
54
+
55
+ // ─── Erros tipados ────────────────────────────────────────────────────────────
56
+
57
+ class ConfigError extends Error {
58
+ constructor(code, message, statusCode = 400) {
59
+ super(message);
60
+ this.code = code;
61
+ this.statusCode = statusCode;
62
+ }
63
+ }
64
+
65
+ const Errors = {
66
+ NoSuchConfigRule: (name) =>
67
+ new ConfigError('NoSuchConfigRuleException', `The ConfigRule '${name}' provided in the request is invalid`, 400),
68
+ NoSuchConfigurationRecorder: (name) =>
69
+ new ConfigError('NoSuchConfigurationRecorderException', `Cannot find configuration recorder '${name}'`, 400),
70
+ NoSuchDeliveryChannel: (name) =>
71
+ new ConfigError('NoSuchDeliveryChannelException', `Cannot find delivery channel '${name}'`, 400),
72
+ NoSuchConformancePack: (name) =>
73
+ new ConfigError('NoSuchConformancePackException', `Conformance pack '${name}' not found`, 400),
74
+ NoSuchConfigurationAggregator: (name) =>
75
+ new ConfigError('NoSuchConfigurationAggregatorException', `Aggregator '${name}' not found`, 400),
76
+ NoSuchRemediationConfiguration: (name) =>
77
+ new ConfigError('NoSuchRemediationConfigurationException', `Remediation configuration not found for rule '${name}'`, 400),
78
+ MaxActiveRulesExceeded: () =>
79
+ new ConfigError('MaxActiveRulesExceededException', 'Maximum number of active rules exceeded (max: 150)', 400),
80
+ MaxNumberOfConfigRules: () =>
81
+ new ConfigError('MaxNumberOfConfigRulesExceededException', 'Maximum number of config rules exceeded', 400),
82
+ InvalidParameterValue: (msg) =>
83
+ new ConfigError('InvalidParameterValueException', msg, 400),
84
+ ValidationError: (msg) =>
85
+ new ConfigError('ValidationException', msg, 400),
86
+ ResourceNotFoundException: (msg) =>
87
+ new ConfigError('ResourceNotFoundException', msg, 404),
88
+ LimitExceeded: (msg) =>
89
+ new ConfigError('LimitExceededException', msg, 400),
90
+ };
91
+
92
+ // ─── Constantes ───────────────────────────────────────────────────────────────
93
+
94
+ const REGION = 'us-east-1';
95
+ const ACCOUNT_ID = '000000000000';
96
+ const MAX_RULES = 150;
97
+ const MAX_RESULTS_DEFAULT = 100;
98
+
99
+ // Tipos de recursos suportados
100
+ const SUPPORTED_RESOURCE_TYPES = [
101
+ 'AWS::EC2::Instance',
102
+ 'AWS::EC2::VPC',
103
+ 'AWS::EC2::Subnet',
104
+ 'AWS::EC2::SecurityGroup',
105
+ 'AWS::EC2::InternetGateway',
106
+ 'AWS::EC2::RouteTable',
107
+ 'AWS::EC2::NetworkInterface',
108
+ 'AWS::S3::Bucket',
109
+ 'AWS::IAM::Role',
110
+ 'AWS::IAM::Policy',
111
+ 'AWS::IAM::User',
112
+ 'AWS::IAM::Group',
113
+ 'AWS::Lambda::Function',
114
+ 'AWS::DynamoDB::Table',
115
+ 'AWS::SNS::Topic',
116
+ 'AWS::SQS::Queue',
117
+ 'AWS::CloudFormation::Stack',
118
+ 'AWS::CloudWatch::Alarm',
119
+ 'AWS::KMS::Key',
120
+ 'AWS::SecretsManager::Secret',
121
+ 'AWS::ECS::Cluster',
122
+ 'AWS::ECS::TaskDefinition',
123
+ 'AWS::ECS::Service',
124
+ 'AWS::StepFunctions::StateMachine',
125
+ 'AWS::ApiGateway::RestApi',
126
+ 'AWS::Cognito::UserPool',
127
+ ];
128
+
129
+ // Modos de gravação do recorder
130
+ const RECORDER_MODES = ['ALL', 'INCLUDE', 'EXCLUDE'];
131
+
132
+ // ─── Utilitários ──────────────────────────────────────────────────────────────
133
+
134
+ function now() {
135
+ return new Date().toISOString();
136
+ }
137
+
138
+ function resourceArn(resourceType, resourceId) {
139
+ const service = resourceType.split('::')[1].toLowerCase();
140
+ return `arn:aws:${service}:${REGION}:${ACCOUNT_ID}:${resourceId}`;
141
+ }
142
+
143
+ function configRuleArn(ruleName) {
144
+ return `arn:aws:config:${REGION}:${ACCOUNT_ID}:config-rule/config-rule-${ruleName}`;
145
+ }
146
+
147
+ function recorderArn(recorderName) {
148
+ return `arn:aws:config:${REGION}:${ACCOUNT_ID}:configuration-recorder/${recorderName}`;
149
+ }
150
+
151
+ function conformancePackArn(packName) {
152
+ return `arn:aws:config:${REGION}:${ACCOUNT_ID}:conformance-pack/${packName}`;
153
+ }
154
+
155
+ function aggregatorArn(aggregatorName) {
156
+ return `arn:aws:config:${REGION}:${ACCOUNT_ID}:config-aggregator/${aggregatorName}`;
157
+ }
158
+
159
+ function paginate(items, nextToken, limit = MAX_RESULTS_DEFAULT) {
160
+ let start = 0;
161
+ if (nextToken) {
162
+ try {
163
+ start = parseInt(Buffer.from(nextToken, 'base64').toString(), 10);
164
+ } catch {
165
+ start = 0;
166
+ }
167
+ }
168
+ const slice = items.slice(start, start + limit);
169
+ const newToken = start + limit < items.length
170
+ ? Buffer.from(String(start + limit)).toString('base64')
171
+ : null;
172
+ return { items: slice, nextToken: newToken };
173
+ }
174
+
175
+ // ─── Simulator Principal ──────────────────────────────────────────────────────
176
+
177
+ class ConfigSimulator {
178
+ constructor(config, store, logger) {
179
+ this.config = config;
180
+ this.store = store;
181
+ this.logger = logger;
182
+
183
+ // State
184
+ this.recorders = new Map(); // name → recorder object
185
+ this.recorderStatus = new Map(); // name → status object
186
+ this.deliveryChannels = new Map(); // name → channel object
187
+ this.deliveryChannelStatus = new Map(); // name → status object
188
+ this.configRules = new Map(); // name → rule object
189
+ this.ruleEvaluationStatus = new Map(); // name → evaluation status
190
+ this.evaluationResults = new Map(); // ruleId → evaluation results[]
191
+ this.resourceConfigs = new Map(); // "type::id" → config history[]
192
+ this.discoveredResources = new Map(); // "type::id" → resource info
193
+ this.conformancePacks = new Map(); // name → pack object
194
+ this.conformancePackStatus = new Map(); // name → status
195
+ this.aggregators = new Map(); // name → aggregator object
196
+ this.remediationConfigs = new Map(); // ruleName → remediation config[]
197
+ this.remediationExecutions = new Map(); // ruleName → execution results[]
198
+ this.tags = new Map(); // arn → { key: value }
199
+
200
+ // Cross-service
201
+ this.s3Simulator = null;
202
+ this.snsSimulator = null;
203
+ this.cloudtrailSimulator = null;
204
+
205
+ // Recorder automático — inicia ao criar o primeiro recorder
206
+ this._recordingInterval = null;
207
+ }
208
+
209
+ // ─── Persistência ──────────────────────────────────────────────────────────
210
+
211
+ async load() {
212
+ try {
213
+ const data = await this.store.load('config');
214
+ if (data) {
215
+ if (data.recorders) this.recorders = new Map(Object.entries(data.recorders));
216
+ if (data.recorderStatus) this.recorderStatus = new Map(Object.entries(data.recorderStatus));
217
+ if (data.deliveryChannels) this.deliveryChannels = new Map(Object.entries(data.deliveryChannels));
218
+ if (data.deliveryChannelStatus) this.deliveryChannelStatus = new Map(Object.entries(data.deliveryChannelStatus));
219
+ if (data.configRules) this.configRules = new Map(Object.entries(data.configRules));
220
+ if (data.ruleEvaluationStatus) this.ruleEvaluationStatus = new Map(Object.entries(data.ruleEvaluationStatus));
221
+ if (data.evaluationResults) {
222
+ this.evaluationResults = new Map(
223
+ Object.entries(data.evaluationResults).map(([k, v]) => [k, v])
224
+ );
225
+ }
226
+ if (data.resourceConfigs) {
227
+ this.resourceConfigs = new Map(
228
+ Object.entries(data.resourceConfigs).map(([k, v]) => [k, v])
229
+ );
230
+ }
231
+ if (data.discoveredResources) {
232
+ this.discoveredResources = new Map(Object.entries(data.discoveredResources));
233
+ }
234
+ if (data.conformancePacks) this.conformancePacks = new Map(Object.entries(data.conformancePacks));
235
+ if (data.conformancePackStatus) this.conformancePackStatus = new Map(Object.entries(data.conformancePackStatus));
236
+ if (data.aggregators) this.aggregators = new Map(Object.entries(data.aggregators));
237
+ if (data.remediationConfigs) {
238
+ this.remediationConfigs = new Map(Object.entries(data.remediationConfigs));
239
+ }
240
+ if (data.tags) this.tags = new Map(Object.entries(data.tags));
241
+ this.logger.info('[Config] State loaded from store');
242
+ }
243
+ } catch (err) {
244
+ this.logger.warn('[Config] Could not load state:', err.message);
245
+ }
246
+ }
247
+
248
+ async save() {
249
+ try {
250
+ const data = {
251
+ recorders: Object.fromEntries(this.recorders),
252
+ recorderStatus: Object.fromEntries(this.recorderStatus),
253
+ deliveryChannels: Object.fromEntries(this.deliveryChannels),
254
+ deliveryChannelStatus: Object.fromEntries(this.deliveryChannelStatus),
255
+ configRules: Object.fromEntries(this.configRules),
256
+ ruleEvaluationStatus: Object.fromEntries(this.ruleEvaluationStatus),
257
+ evaluationResults: Object.fromEntries(this.evaluationResults),
258
+ resourceConfigs: Object.fromEntries(this.resourceConfigs),
259
+ discoveredResources: Object.fromEntries(this.discoveredResources),
260
+ conformancePacks: Object.fromEntries(this.conformancePacks),
261
+ conformancePackStatus: Object.fromEntries(this.conformancePackStatus),
262
+ aggregators: Object.fromEntries(this.aggregators),
263
+ remediationConfigs: Object.fromEntries(this.remediationConfigs),
264
+ tags: Object.fromEntries(this.tags),
265
+ };
266
+ await this.store.save('config', data);
267
+ } catch (err) {
268
+ this.logger.warn('[Config] Could not save state:', err.message);
269
+ }
270
+ }
271
+
272
+ reset() {
273
+ this.recorders.clear();
274
+ this.recorderStatus.clear();
275
+ this.deliveryChannels.clear();
276
+ this.deliveryChannelStatus.clear();
277
+ this.configRules.clear();
278
+ this.ruleEvaluationStatus.clear();
279
+ this.evaluationResults.clear();
280
+ this.resourceConfigs.clear();
281
+ this.discoveredResources.clear();
282
+ this.conformancePacks.clear();
283
+ this.conformancePackStatus.clear();
284
+ this.aggregators.clear();
285
+ this.remediationConfigs.clear();
286
+ this.remediationExecutions.clear();
287
+ this.tags.clear();
288
+ this._stopRecording();
289
+ this.logger.info('[Config] State reset');
290
+ }
291
+
292
+ // ─── Gravação automática de recursos ───────────────────────────────────────
293
+
294
+ _startRecording() {
295
+ if (this._recordingInterval) return;
296
+ this._recordingInterval = setInterval(() => {
297
+ this._recordResources();
298
+ }, 30000); // a cada 30s simula nova captura
299
+ }
300
+
301
+ _stopRecording() {
302
+ if (this._recordingInterval) {
303
+ clearInterval(this._recordingInterval);
304
+ this._recordingInterval = null;
305
+ }
306
+ }
307
+
308
+ _recordResources() {
309
+ // Registra recursos de serviços injetados
310
+ const timestamp = now();
311
+
312
+ // Lambda
313
+ if (this.lambdaSimulator) {
314
+ const functions = this.lambdaSimulator.functions || new Map();
315
+ for (const [name, fn] of functions) {
316
+ this._recordResourceConfig('AWS::Lambda::Function', name, fn, timestamp);
317
+ }
318
+ }
319
+
320
+ // DynamoDB
321
+ if (this.dynamoSimulator) {
322
+ const tables = this.dynamoSimulator.tables || new Map();
323
+ for (const [name, table] of tables) {
324
+ this._recordResourceConfig('AWS::DynamoDB::Table', name, table, timestamp);
325
+ }
326
+ }
327
+
328
+ // S3
329
+ if (this.s3Simulator) {
330
+ const buckets = this.s3Simulator.buckets || new Map();
331
+ for (const [name, bucket] of buckets) {
332
+ this._recordResourceConfig('AWS::S3::Bucket', name, bucket, timestamp);
333
+ }
334
+ }
335
+
336
+ // SNS
337
+ if (this.snsSimulator) {
338
+ const topics = this.snsSimulator.topics || new Map();
339
+ for (const [arn, topic] of topics) {
340
+ this._recordResourceConfig('AWS::SNS::Topic', arn, topic, timestamp);
341
+ }
342
+ }
343
+ }
344
+
345
+ _recordResourceConfig(resourceType, resourceId, configuration, timestamp) {
346
+ const key = `${resourceType}::${resourceId}`;
347
+ const configItem = {
348
+ version: '1.3',
349
+ accountId: ACCOUNT_ID,
350
+ configurationItemCaptureTime: timestamp || now(),
351
+ configurationItemStatus: 'OK',
352
+ configurationStateId: Date.now().toString(),
353
+ configurationItemMD5Hash: randomUUID().replace(/-/g, ''),
354
+ arn: resourceArn(resourceType, resourceId),
355
+ resourceType,
356
+ resourceId,
357
+ resourceName: resourceId,
358
+ awsRegion: REGION,
359
+ availabilityZone: 'us-east-1a',
360
+ tags: this.tags.get(resourceArn(resourceType, resourceId)) || {},
361
+ relatedEvents: [],
362
+ relationships: [],
363
+ configuration: typeof configuration === 'object' ? JSON.stringify(configuration) : configuration,
364
+ supplementaryConfiguration: {},
365
+ };
366
+
367
+ if (!this.resourceConfigs.has(key)) {
368
+ this.resourceConfigs.set(key, []);
369
+ }
370
+ const history = this.resourceConfigs.get(key);
371
+ history.push(configItem);
372
+ // Limita o histórico a 100 itens por recurso
373
+ if (history.length > 100) history.shift();
374
+
375
+ // Registra como recurso descoberto
376
+ this.discoveredResources.set(key, {
377
+ resourceType,
378
+ resourceId,
379
+ resourceName: resourceId,
380
+ resourceDeletionTime: null,
381
+ });
382
+ }
383
+
384
+ // ─── Configuration Recorders ────────────────────────────────────────────────
385
+
386
+ putConfigurationRecorder({ ConfigurationRecorder }) {
387
+ if (!ConfigurationRecorder || !ConfigurationRecorder.name) {
388
+ throw Errors.ValidationError('ConfigurationRecorder name is required');
389
+ }
390
+
391
+ const { name, roleARN, recordingGroup, recordingMode } = ConfigurationRecorder;
392
+
393
+ const recorder = {
394
+ name,
395
+ roleARN: roleARN || `arn:aws:iam::${ACCOUNT_ID}:role/aws-config-role`,
396
+ recordingGroup: recordingGroup || {
397
+ allSupported: true,
398
+ includeGlobalResourceTypes: false,
399
+ resourceTypes: [],
400
+ },
401
+ recordingMode: recordingMode || {
402
+ recordingFrequency: 'CONTINUOUS',
403
+ },
404
+ };
405
+
406
+ this.recorders.set(name, recorder);
407
+
408
+ if (!this.recorderStatus.has(name)) {
409
+ this.recorderStatus.set(name, {
410
+ name,
411
+ lastStartTime: null,
412
+ lastStopTime: null,
413
+ recording: false,
414
+ lastStatus: 'Pending',
415
+ lastStatusChangeTime: now(),
416
+ lastSuccessfulDeliveryTime: null,
417
+ lastErrorCode: null,
418
+ lastErrorMessage: null,
419
+ });
420
+ }
421
+
422
+ this.logger.info(`[Config] Configuration recorder '${name}' created/updated`);
423
+ this.save();
424
+ return {};
425
+ }
426
+
427
+ deleteConfigurationRecorder({ ConfigurationRecorderName }) {
428
+ if (!this.recorders.has(ConfigurationRecorderName)) {
429
+ throw Errors.NoSuchConfigurationRecorder(ConfigurationRecorderName);
430
+ }
431
+ this.recorders.delete(ConfigurationRecorderName);
432
+ this.recorderStatus.delete(ConfigurationRecorderName);
433
+ this.logger.info(`[Config] Configuration recorder '${ConfigurationRecorderName}' deleted`);
434
+ this.save();
435
+ return {};
436
+ }
437
+
438
+ describeConfigurationRecorders({ ConfigurationRecorderNames } = {}) {
439
+ let recorders = Array.from(this.recorders.values());
440
+ if (ConfigurationRecorderNames && ConfigurationRecorderNames.length > 0) {
441
+ recorders = recorders.filter(r => ConfigurationRecorderNames.includes(r.name));
442
+ }
443
+ return { ConfigurationRecorders: recorders };
444
+ }
445
+
446
+ describeConfigurationRecorderStatus({ ConfigurationRecorderNames } = {}) {
447
+ let statuses = Array.from(this.recorderStatus.values());
448
+ if (ConfigurationRecorderNames && ConfigurationRecorderNames.length > 0) {
449
+ statuses = statuses.filter(s => ConfigurationRecorderNames.includes(s.name));
450
+ }
451
+ return { ConfigurationRecordersStatus: statuses };
452
+ }
453
+
454
+ startConfigurationRecorder({ ConfigurationRecorderName }) {
455
+ if (!this.recorders.has(ConfigurationRecorderName)) {
456
+ throw Errors.NoSuchConfigurationRecorder(ConfigurationRecorderName);
457
+ }
458
+ const status = this.recorderStatus.get(ConfigurationRecorderName);
459
+ status.recording = true;
460
+ status.lastStartTime = now();
461
+ status.lastStatus = 'SUCCESS';
462
+ status.lastStatusChangeTime = now();
463
+ this.recorderStatus.set(ConfigurationRecorderName, status);
464
+ this._startRecording();
465
+ this.logger.info(`[Config] Recorder '${ConfigurationRecorderName}' started`);
466
+ this.save();
467
+ return {};
468
+ }
469
+
470
+ stopConfigurationRecorder({ ConfigurationRecorderName }) {
471
+ if (!this.recorders.has(ConfigurationRecorderName)) {
472
+ throw Errors.NoSuchConfigurationRecorder(ConfigurationRecorderName);
473
+ }
474
+ const status = this.recorderStatus.get(ConfigurationRecorderName);
475
+ status.recording = false;
476
+ status.lastStopTime = now();
477
+ status.lastStatus = 'SUCCESS';
478
+ status.lastStatusChangeTime = now();
479
+ this.recorderStatus.set(ConfigurationRecorderName, status);
480
+ this.logger.info(`[Config] Recorder '${ConfigurationRecorderName}' stopped`);
481
+ this.save();
482
+ return {};
483
+ }
484
+
485
+ // ─── Delivery Channels ──────────────────────────────────────────────────────
486
+
487
+ putDeliveryChannel({ DeliveryChannel }) {
488
+ if (!DeliveryChannel || !DeliveryChannel.name) {
489
+ throw Errors.ValidationError('DeliveryChannel name is required');
490
+ }
491
+ const { name, s3BucketName, s3KeyPrefix, snsTopicARN, configSnapshotDeliveryProperties } = DeliveryChannel;
492
+
493
+ const channel = {
494
+ name,
495
+ s3BucketName: s3BucketName || '',
496
+ s3KeyPrefix: s3KeyPrefix || '',
497
+ snsTopicARN: snsTopicARN || '',
498
+ configSnapshotDeliveryProperties: configSnapshotDeliveryProperties || {
499
+ deliveryFrequency: 'TwentyFour_Hours',
500
+ },
501
+ };
502
+
503
+ this.deliveryChannels.set(name, channel);
504
+
505
+ if (!this.deliveryChannelStatus.has(name)) {
506
+ this.deliveryChannelStatus.set(name, {
507
+ name,
508
+ configSnapshotDeliveryInfo: {
509
+ lastStatus: 'NOT_APPLICABLE',
510
+ lastStatusChangeTime: now(),
511
+ },
512
+ configHistoryDeliveryInfo: {
513
+ lastStatus: 'NOT_APPLICABLE',
514
+ lastStatusChangeTime: now(),
515
+ },
516
+ configStreamDeliveryInfo: {
517
+ lastStatus: 'SUCCESS',
518
+ lastStatusChangeTime: now(),
519
+ },
520
+ });
521
+ }
522
+
523
+ this.logger.info(`[Config] Delivery channel '${name}' created/updated`);
524
+ this.save();
525
+ return {};
526
+ }
527
+
528
+ deleteDeliveryChannel({ DeliveryChannelName }) {
529
+ if (!this.deliveryChannels.has(DeliveryChannelName)) {
530
+ throw Errors.NoSuchDeliveryChannel(DeliveryChannelName);
531
+ }
532
+ this.deliveryChannels.delete(DeliveryChannelName);
533
+ this.deliveryChannelStatus.delete(DeliveryChannelName);
534
+ this.logger.info(`[Config] Delivery channel '${DeliveryChannelName}' deleted`);
535
+ this.save();
536
+ return {};
537
+ }
538
+
539
+ describeDeliveryChannels({ DeliveryChannelNames } = {}) {
540
+ let channels = Array.from(this.deliveryChannels.values());
541
+ if (DeliveryChannelNames && DeliveryChannelNames.length > 0) {
542
+ channels = channels.filter(c => DeliveryChannelNames.includes(c.name));
543
+ }
544
+ return { DeliveryChannels: channels };
545
+ }
546
+
547
+ describeDeliveryChannelStatus({ DeliveryChannelNames } = {}) {
548
+ let statuses = Array.from(this.deliveryChannelStatus.values());
549
+ if (DeliveryChannelNames && DeliveryChannelNames.length > 0) {
550
+ statuses = statuses.filter(s => DeliveryChannelNames.includes(s.name));
551
+ }
552
+ return { DeliveryChannelsStatus: statuses };
553
+ }
554
+
555
+ deliverConfigSnapshot({ DeliveryChannelName }) {
556
+ if (!this.deliveryChannels.has(DeliveryChannelName)) {
557
+ throw Errors.NoSuchDeliveryChannel(DeliveryChannelName);
558
+ }
559
+ const configSnapshotId = randomUUID();
560
+ const channel = this.deliveryChannels.get(DeliveryChannelName);
561
+ const status = this.deliveryChannelStatus.get(DeliveryChannelName);
562
+
563
+ // Simula entrega para S3
564
+ if (this.s3Simulator && channel.s3BucketName) {
565
+ const snapshotKey = `${channel.s3KeyPrefix || 'AWSLogs'}/${ACCOUNT_ID}/Config/${REGION}/${now().split('T')[0]}/ConfigSnapshot/${configSnapshotId}.json`;
566
+ const snapshot = {
567
+ fileVersion: '1.0',
568
+ requestId: configSnapshotId,
569
+ configurationItems: Array.from(this.resourceConfigs.values()).flatMap(v => v.slice(-1)),
570
+ };
571
+ try {
572
+ this.s3Simulator.putObject({
573
+ Bucket: channel.s3BucketName,
574
+ Key: snapshotKey,
575
+ Body: JSON.stringify(snapshot),
576
+ ContentType: 'application/json',
577
+ });
578
+ } catch (err) {
579
+ this.logger.warn(`[Config] Could not deliver snapshot to S3: ${err.message}`);
580
+ }
581
+ }
582
+
583
+ // Atualiza status do canal
584
+ status.configSnapshotDeliveryInfo = {
585
+ lastStatus: 'SUCCESS',
586
+ lastStatusChangeTime: now(),
587
+ lastSuccessfulTime: now(),
588
+ nextDeliveryTime: new Date(Date.now() + 86400000).toISOString(),
589
+ };
590
+ this.deliveryChannelStatus.set(DeliveryChannelName, status);
591
+ this.save();
592
+
593
+ this.logger.info(`[Config] Config snapshot delivered — ID: ${configSnapshotId}`);
594
+ return { configSnapshotId };
595
+ }
596
+
597
+ // ─── Config Rules ───────────────────────────────────────────────────────────
598
+
599
+ putConfigRule({ ConfigRule, Tags }) {
600
+ if (!ConfigRule || !ConfigRule.ConfigRuleName) {
601
+ throw Errors.ValidationError('ConfigRule.ConfigRuleName is required');
602
+ }
603
+
604
+ if (this.configRules.size >= MAX_RULES && !this.configRules.has(ConfigRule.ConfigRuleName)) {
605
+ throw Errors.MaxActiveRulesExceeded();
606
+ }
607
+
608
+ const {
609
+ ConfigRuleName,
610
+ Description,
611
+ Scope,
612
+ Source,
613
+ InputParameters,
614
+ MaximumExecutionFrequency,
615
+ ConfigRuleState,
616
+ } = ConfigRule;
617
+
618
+ const ruleId = `config-rule-${randomUUID().substring(0, 8)}`;
619
+ const existing = this.configRules.get(ConfigRuleName);
620
+
621
+ const rule = {
622
+ ConfigRuleName,
623
+ ConfigRuleArn: configRuleArn(ConfigRuleName),
624
+ ConfigRuleId: existing ? existing.ConfigRuleId : ruleId,
625
+ Description: Description || '',
626
+ Scope: Scope || null,
627
+ Source: Source || { Owner: 'AWS', SourceIdentifier: 'REQUIRED_TAGS' },
628
+ InputParameters: InputParameters || '{}',
629
+ MaximumExecutionFrequency: MaximumExecutionFrequency || 'TwentyFour_Hours',
630
+ ConfigRuleState: ConfigRuleState || 'ACTIVE',
631
+ CreatedBy: 'Local',
632
+ };
633
+
634
+ this.configRules.set(ConfigRuleName, rule);
635
+
636
+ // Inicializa status de avaliação
637
+ if (!this.ruleEvaluationStatus.has(ConfigRuleName)) {
638
+ this.ruleEvaluationStatus.set(ConfigRuleName, {
639
+ ConfigRuleName,
640
+ ConfigRuleArn: rule.ConfigRuleArn,
641
+ ConfigRuleId: rule.ConfigRuleId,
642
+ LastSuccessfulInvocationTime: null,
643
+ LastFailedInvocationTime: null,
644
+ LastSuccessfulEvaluationTime: null,
645
+ LastFailedEvaluationTime: null,
646
+ FirstActivatedTime: now(),
647
+ LastDeactivatedTime: null,
648
+ LastErrorCode: null,
649
+ LastErrorMessage: null,
650
+ FirstEvaluationStarted: false,
651
+ });
652
+ }
653
+
654
+ // Tags
655
+ if (Tags && Tags.length > 0) {
656
+ const arn = rule.ConfigRuleArn;
657
+ const tagMap = this.tags.get(arn) || {};
658
+ for (const { Key, Value } of Tags) tagMap[Key] = Value;
659
+ this.tags.set(arn, tagMap);
660
+ }
661
+
662
+ this.logger.info(`[Config] Config rule '${ConfigRuleName}' created/updated`);
663
+ this.save();
664
+ return {};
665
+ }
666
+
667
+ deleteConfigRule({ ConfigRuleName }) {
668
+ if (!this.configRules.has(ConfigRuleName)) {
669
+ throw Errors.NoSuchConfigRule(ConfigRuleName);
670
+ }
671
+ const rule = this.configRules.get(ConfigRuleName);
672
+ this.configRules.delete(ConfigRuleName);
673
+ this.ruleEvaluationStatus.delete(ConfigRuleName);
674
+ this.evaluationResults.delete(ConfigRuleName);
675
+ this.tags.delete(rule.ConfigRuleArn);
676
+ this.logger.info(`[Config] Config rule '${ConfigRuleName}' deleted`);
677
+ this.save();
678
+ return {};
679
+ }
680
+
681
+ describeConfigRules({ ConfigRuleNames, NextToken, Filters } = {}) {
682
+ let rules = Array.from(this.configRules.values());
683
+
684
+ if (ConfigRuleNames && ConfigRuleNames.length > 0) {
685
+ rules = rules.filter(r => ConfigRuleNames.includes(r.ConfigRuleName));
686
+ }
687
+
688
+ if (Filters) {
689
+ if (Filters.ConfigRuleName) {
690
+ rules = rules.filter(r => r.ConfigRuleName.includes(Filters.ConfigRuleName));
691
+ }
692
+ }
693
+
694
+ const { items, nextToken } = paginate(rules, NextToken);
695
+ return { ConfigRules: items, NextToken: nextToken };
696
+ }
697
+
698
+ describeConfigRuleEvaluationStatus({ ConfigRuleNames, NextToken, Limit } = {}) {
699
+ let statuses = Array.from(this.ruleEvaluationStatus.values());
700
+
701
+ if (ConfigRuleNames && ConfigRuleNames.length > 0) {
702
+ statuses = statuses.filter(s => ConfigRuleNames.includes(s.ConfigRuleName));
703
+ }
704
+
705
+ const limit = Limit || MAX_RESULTS_DEFAULT;
706
+ const { items, nextToken } = paginate(statuses, NextToken, limit);
707
+ return { ConfigRulesEvaluationStatus: items, NextToken: nextToken };
708
+ }
709
+
710
+ startConfigRulesEvaluation({ ConfigRuleNames }) {
711
+ if (!ConfigRuleNames || ConfigRuleNames.length === 0) {
712
+ throw Errors.ValidationError('ConfigRuleNames is required');
713
+ }
714
+
715
+ for (const ruleName of ConfigRuleNames) {
716
+ if (!this.configRules.has(ruleName)) {
717
+ throw Errors.NoSuchConfigRule(ruleName);
718
+ }
719
+
720
+ const status = this.ruleEvaluationStatus.get(ruleName);
721
+ status.FirstEvaluationStarted = true;
722
+ status.LastSuccessfulInvocationTime = now();
723
+ this.ruleEvaluationStatus.set(ruleName, status);
724
+
725
+ // Executa avaliação assíncrona
726
+ setImmediate(() => this._evaluateRule(ruleName));
727
+ }
728
+
729
+ this.save();
730
+ return {};
731
+ }
732
+
733
+ _evaluateRule(ruleName) {
734
+ const rule = this.configRules.get(ruleName);
735
+ if (!rule) return;
736
+
737
+ const results = [];
738
+ const resources = Array.from(this.discoveredResources.values());
739
+
740
+ for (const resource of resources) {
741
+ // Verifica se o escopo da regra se aplica ao recurso
742
+ let applicable = true;
743
+ if (rule.Scope) {
744
+ if (rule.Scope.ComplianceResourceTypes && rule.Scope.ComplianceResourceTypes.length > 0) {
745
+ applicable = rule.Scope.ComplianceResourceTypes.includes(resource.resourceType);
746
+ }
747
+ }
748
+
749
+ if (!applicable) continue;
750
+
751
+ // Simula avaliação — COMPLIANT por padrão, NON_COMPLIANT aleatório
752
+ const compliance = Math.random() > 0.2 ? 'COMPLIANT' : 'NON_COMPLIANT';
753
+
754
+ results.push({
755
+ EvaluationResultIdentifier: {
756
+ EvaluationResultQualifier: {
757
+ ConfigRuleName: ruleName,
758
+ ResourceType: resource.resourceType,
759
+ ResourceId: resource.resourceId,
760
+ EvaluationMode: 'DETECTIVE',
761
+ },
762
+ OrderingTimestamp: now(),
763
+ },
764
+ ComplianceType: compliance,
765
+ ResultRecordedTime: now(),
766
+ ConfigRuleInvokedTime: now(),
767
+ Annotation: compliance === 'NON_COMPLIANT' ? 'Resource does not comply with the rule' : '',
768
+ ResultToken: randomUUID(),
769
+ });
770
+ }
771
+
772
+ this.evaluationResults.set(ruleName, results);
773
+
774
+ const status = this.ruleEvaluationStatus.get(ruleName);
775
+ if (status) {
776
+ status.LastSuccessfulEvaluationTime = now();
777
+ this.ruleEvaluationStatus.set(ruleName, status);
778
+ }
779
+
780
+ this.save();
781
+ this.logger.debug(`[Config] Rule '${ruleName}' evaluated: ${results.length} results`);
782
+ }
783
+
784
+ getComplianceDetailsByConfigRule({ ConfigRuleName, ComplianceTypes, NextToken, Limit } = {}) {
785
+ if (!this.configRules.has(ConfigRuleName)) {
786
+ throw Errors.NoSuchConfigRule(ConfigRuleName);
787
+ }
788
+
789
+ let results = this.evaluationResults.get(ConfigRuleName) || [];
790
+
791
+ if (ComplianceTypes && ComplianceTypes.length > 0) {
792
+ results = results.filter(r => ComplianceTypes.includes(r.ComplianceType));
793
+ }
794
+
795
+ const limit = Limit || MAX_RESULTS_DEFAULT;
796
+ const { items, nextToken } = paginate(results, NextToken, limit);
797
+ return { EvaluationResults: items, NextToken: nextToken };
798
+ }
799
+
800
+ getComplianceDetailsByResource({ ResourceType, ResourceId, ComplianceTypes, NextToken } = {}) {
801
+ if (!ResourceType || !ResourceId) {
802
+ throw Errors.ValidationError('ResourceType and ResourceId are required');
803
+ }
804
+
805
+ let results = [];
806
+ for (const [ruleName, ruleResults] of this.evaluationResults) {
807
+ const filtered = ruleResults.filter(
808
+ r =>
809
+ r.EvaluationResultIdentifier.EvaluationResultQualifier.ResourceType === ResourceType &&
810
+ r.EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId === ResourceId
811
+ );
812
+ results.push(...filtered);
813
+ }
814
+
815
+ if (ComplianceTypes && ComplianceTypes.length > 0) {
816
+ results = results.filter(r => ComplianceTypes.includes(r.ComplianceType));
817
+ }
818
+
819
+ const { items, nextToken } = paginate(results, NextToken);
820
+ return { EvaluationResults: items, NextToken: nextToken };
821
+ }
822
+
823
+ getComplianceSummaryByConfigRule() {
824
+ let compliantCount = 0;
825
+ let nonCompliantCount = 0;
826
+
827
+ for (const [, results] of this.evaluationResults) {
828
+ for (const r of results) {
829
+ if (r.ComplianceType === 'COMPLIANT') compliantCount++;
830
+ else if (r.ComplianceType === 'NON_COMPLIANT') nonCompliantCount++;
831
+ }
832
+ }
833
+
834
+ const summaries = Array.from(this.configRules.keys()).map(ruleName => {
835
+ const results = this.evaluationResults.get(ruleName) || [];
836
+ const compliant = results.filter(r => r.ComplianceType === 'COMPLIANT').length;
837
+ const nonCompliant = results.filter(r => r.ComplianceType === 'NON_COMPLIANT').length;
838
+ const overallCompliance = nonCompliant === 0 && compliant > 0 ? 'COMPLIANT' :
839
+ nonCompliant > 0 ? 'NON_COMPLIANT' : 'INSUFFICIENT_DATA';
840
+
841
+ return {
842
+ ConfigRuleName: ruleName,
843
+ Compliance: {
844
+ ComplianceType: overallCompliance,
845
+ ComplianceContributorCount: {
846
+ CappedCount: nonCompliant,
847
+ CapExceeded: false,
848
+ },
849
+ },
850
+ };
851
+ });
852
+
853
+ return { ComplianceSummariesByConfigRule: summaries };
854
+ }
855
+
856
+ getComplianceSummaryByResourceType({ ResourceTypes } = {}) {
857
+ const summaryMap = new Map();
858
+
859
+ for (const [, results] of this.evaluationResults) {
860
+ for (const r of results) {
861
+ const type = r.EvaluationResultIdentifier.EvaluationResultQualifier.ResourceType;
862
+ if (ResourceTypes && ResourceTypes.length > 0 && !ResourceTypes.includes(type)) continue;
863
+
864
+ if (!summaryMap.has(type)) {
865
+ summaryMap.set(type, { compliant: 0, nonCompliant: 0 });
866
+ }
867
+ const s = summaryMap.get(type);
868
+ if (r.ComplianceType === 'COMPLIANT') s.compliant++;
869
+ else if (r.ComplianceType === 'NON_COMPLIANT') s.nonCompliant++;
870
+ }
871
+ }
872
+
873
+ const summaries = Array.from(summaryMap.entries()).map(([type, counts]) => ({
874
+ ResourceType: type,
875
+ ComplianceSummary: {
876
+ CompliantResourceCount: { CappedCount: counts.compliant, CapExceeded: false },
877
+ NonCompliantResourceCount: { CappedCount: counts.nonCompliant, CapExceeded: false },
878
+ ComplianceSummaryTimestamp: now(),
879
+ },
880
+ }));
881
+
882
+ return { ComplianceSummariesByResourceType: summaries };
883
+ }
884
+
885
+ // ─── Resource Configuration ─────────────────────────────────────────────────
886
+
887
+ getResourceConfigHistory({ resourceType, resourceId, laterTime, earlierTime, chronologicalOrder, limit, nextToken } = {}) {
888
+ if (!resourceType || !resourceId) {
889
+ throw Errors.ValidationError('resourceType and resourceId are required');
890
+ }
891
+
892
+ const key = `${resourceType}::${resourceId}`;
893
+ let history = this.resourceConfigs.get(key) || [];
894
+
895
+ if (earlierTime) {
896
+ history = history.filter(h => new Date(h.configurationItemCaptureTime) >= new Date(earlierTime));
897
+ }
898
+ if (laterTime) {
899
+ history = history.filter(h => new Date(h.configurationItemCaptureTime) <= new Date(laterTime));
900
+ }
901
+
902
+ if (chronologicalOrder === 'Forward') {
903
+ history = history.slice().sort((a, b) =>
904
+ new Date(a.configurationItemCaptureTime) - new Date(b.configurationItemCaptureTime)
905
+ );
906
+ } else {
907
+ history = history.slice().sort((a, b) =>
908
+ new Date(b.configurationItemCaptureTime) - new Date(a.configurationItemCaptureTime)
909
+ );
910
+ }
911
+
912
+ const { items, nextToken: newToken } = paginate(history, nextToken, limit || MAX_RESULTS_DEFAULT);
913
+ return { configurationItems: items, nextToken: newToken };
914
+ }
915
+
916
+ listDiscoveredResources({ resourceType, resourceIds, resourceName, limit, nextToken, includeDeletedResources } = {}) {
917
+ if (!resourceType) {
918
+ throw Errors.ValidationError('resourceType is required');
919
+ }
920
+
921
+ let resources = Array.from(this.discoveredResources.values()).filter(
922
+ r => r.resourceType === resourceType
923
+ );
924
+
925
+ if (resourceIds && resourceIds.length > 0) {
926
+ resources = resources.filter(r => resourceIds.includes(r.resourceId));
927
+ }
928
+ if (resourceName) {
929
+ resources = resources.filter(r => r.resourceName === resourceName);
930
+ }
931
+ if (!includeDeletedResources) {
932
+ resources = resources.filter(r => !r.resourceDeletionTime);
933
+ }
934
+
935
+ const { items, nextToken: newToken } = paginate(resources, nextToken, limit || MAX_RESULTS_DEFAULT);
936
+ return {
937
+ resourceIdentifiers: items.map(r => ({
938
+ resourceType: r.resourceType,
939
+ resourceId: r.resourceId,
940
+ resourceName: r.resourceName,
941
+ resourceDeletionTime: r.resourceDeletionTime,
942
+ })),
943
+ nextToken: newToken,
944
+ };
945
+ }
946
+
947
+ getDiscoveredResourceCounts({ resourceTypes, nextToken, limit } = {}) {
948
+ const countMap = new Map();
949
+
950
+ for (const [, resource] of this.discoveredResources) {
951
+ if (resourceTypes && resourceTypes.length > 0 && !resourceTypes.includes(resource.resourceType)) continue;
952
+ const count = countMap.get(resource.resourceType) || 0;
953
+ countMap.set(resource.resourceType, count + 1);
954
+ }
955
+
956
+ const counts = Array.from(countMap.entries()).map(([resourceType, count]) => ({
957
+ resourceType,
958
+ count,
959
+ }));
960
+
961
+ const { items, nextToken: newToken } = paginate(counts, nextToken, limit || MAX_RESULTS_DEFAULT);
962
+ return {
963
+ totalDiscoveredResources: this.discoveredResources.size,
964
+ resourceCounts: items,
965
+ nextToken: newToken,
966
+ };
967
+ }
968
+
969
+ batchGetResourceConfig({ resourceKeys } = {}) {
970
+ if (!resourceKeys || resourceKeys.length === 0) {
971
+ throw Errors.ValidationError('resourceKeys is required');
972
+ }
973
+
974
+ const baseConfigurationItems = [];
975
+ const unprocessedResourceKeys = [];
976
+
977
+ for (const key of resourceKeys) {
978
+ const mapKey = `${key.resourceType}::${key.resourceId}`;
979
+ const history = this.resourceConfigs.get(mapKey);
980
+ if (history && history.length > 0) {
981
+ baseConfigurationItems.push(history[history.length - 1]);
982
+ } else {
983
+ unprocessedResourceKeys.push(key);
984
+ }
985
+ }
986
+
987
+ return { baseConfigurationItems, unprocessedResourceKeys };
988
+ }
989
+
990
+ // ─── Conformance Packs ──────────────────────────────────────────────────────
991
+
992
+ putConformancePack({ ConformancePackName, TemplateS3Uri, TemplateBody, DeliveryS3Bucket, DeliveryS3KeyPrefix, ConformancePackInputParameters } = {}) {
993
+ if (!ConformancePackName) {
994
+ throw Errors.ValidationError('ConformancePackName is required');
995
+ }
996
+
997
+ const pack = {
998
+ ConformancePackName,
999
+ ConformancePackArn: conformancePackArn(ConformancePackName),
1000
+ ConformancePackId: `cp-${randomUUID().substring(0, 8)}`,
1001
+ DeliveryS3Bucket: DeliveryS3Bucket || '',
1002
+ DeliveryS3KeyPrefix: DeliveryS3KeyPrefix || '',
1003
+ ConformancePackInputParameters: ConformancePackInputParameters || [],
1004
+ CreatedBy: 'Local',
1005
+ LastUpdateRequestedTime: now(),
1006
+ };
1007
+
1008
+ this.conformancePacks.set(ConformancePackName, pack);
1009
+ this.conformancePackStatus.set(ConformancePackName, {
1010
+ ConformancePackName,
1011
+ ConformancePackId: pack.ConformancePackId,
1012
+ ConformancePackArn: pack.ConformancePackArn,
1013
+ ConformancePackState: 'CREATE_COMPLETE',
1014
+ ConformancePackStatusReason: '',
1015
+ LastUpdateRequestedTime: now(),
1016
+ LastUpdateCompletedTime: now(),
1017
+ });
1018
+
1019
+ this.logger.info(`[Config] Conformance pack '${ConformancePackName}' created`);
1020
+ this.save();
1021
+ return { ConformancePackArn: pack.ConformancePackArn };
1022
+ }
1023
+
1024
+ deleteConformancePack({ ConformancePackName }) {
1025
+ if (!this.conformancePacks.has(ConformancePackName)) {
1026
+ throw Errors.NoSuchConformancePack(ConformancePackName);
1027
+ }
1028
+ this.conformancePacks.delete(ConformancePackName);
1029
+ this.conformancePackStatus.delete(ConformancePackName);
1030
+ this.logger.info(`[Config] Conformance pack '${ConformancePackName}' deleted`);
1031
+ this.save();
1032
+ return {};
1033
+ }
1034
+
1035
+ describeConformancePacks({ ConformancePackNames, NextToken, Limit } = {}) {
1036
+ let packs = Array.from(this.conformancePacks.values());
1037
+ if (ConformancePackNames && ConformancePackNames.length > 0) {
1038
+ packs = packs.filter(p => ConformancePackNames.includes(p.ConformancePackName));
1039
+ }
1040
+ const { items, nextToken } = paginate(packs, NextToken, Limit || MAX_RESULTS_DEFAULT);
1041
+ return { ConformancePackDetails: items, NextToken: nextToken };
1042
+ }
1043
+
1044
+ describeConformancePackStatus({ ConformancePackNames, NextToken, Limit } = {}) {
1045
+ let statuses = Array.from(this.conformancePackStatus.values());
1046
+ if (ConformancePackNames && ConformancePackNames.length > 0) {
1047
+ statuses = statuses.filter(s => ConformancePackNames.includes(s.ConformancePackName));
1048
+ }
1049
+ const { items, nextToken } = paginate(statuses, NextToken, Limit || MAX_RESULTS_DEFAULT);
1050
+ return { ConformancePackStatusDetails: items, NextToken: nextToken };
1051
+ }
1052
+
1053
+ getConformancePackComplianceSummary({ ConformancePackNames, NextToken, Limit } = {}) {
1054
+ let packs = Array.from(this.conformancePacks.values());
1055
+ if (ConformancePackNames && ConformancePackNames.length > 0) {
1056
+ packs = packs.filter(p => ConformancePackNames.includes(p.ConformancePackName));
1057
+ }
1058
+
1059
+ const summaries = packs.map(pack => ({
1060
+ ConformancePackName: pack.ConformancePackName,
1061
+ ConformancePackComplianceSummary: {
1062
+ ConformancePackName: pack.ConformancePackName,
1063
+ ConformancePackComplianceStatus: 'COMPLIANT',
1064
+ },
1065
+ }));
1066
+
1067
+ const { items, nextToken } = paginate(summaries, NextToken, Limit || MAX_RESULTS_DEFAULT);
1068
+ return { ConformancePackComplianceSummaryList: items, NextToken: nextToken };
1069
+ }
1070
+
1071
+ // ─── Configuration Aggregators ─────────────────────────────────────────────
1072
+
1073
+ putConfigurationAggregator({ ConfigurationAggregatorName, AccountAggregationSources, OrganizationAggregationSource, Tags } = {}) {
1074
+ if (!ConfigurationAggregatorName) {
1075
+ throw Errors.ValidationError('ConfigurationAggregatorName is required');
1076
+ }
1077
+
1078
+ const aggregator = {
1079
+ ConfigurationAggregatorName,
1080
+ ConfigurationAggregatorArn: aggregatorArn(ConfigurationAggregatorName),
1081
+ AccountAggregationSources: AccountAggregationSources || [],
1082
+ OrganizationAggregationSource: OrganizationAggregationSource || null,
1083
+ CreationTime: now(),
1084
+ LastUpdatedTime: now(),
1085
+ };
1086
+
1087
+ this.aggregators.set(ConfigurationAggregatorName, aggregator);
1088
+
1089
+ if (Tags && Tags.length > 0) {
1090
+ const arn = aggregator.ConfigurationAggregatorArn;
1091
+ const tagMap = this.tags.get(arn) || {};
1092
+ for (const { Key, Value } of Tags) tagMap[Key] = Value;
1093
+ this.tags.set(arn, tagMap);
1094
+ }
1095
+
1096
+ this.logger.info(`[Config] Aggregator '${ConfigurationAggregatorName}' created`);
1097
+ this.save();
1098
+ return { ConfigurationAggregator: aggregator };
1099
+ }
1100
+
1101
+ deleteConfigurationAggregator({ ConfigurationAggregatorName }) {
1102
+ if (!this.aggregators.has(ConfigurationAggregatorName)) {
1103
+ throw Errors.NoSuchConfigurationAggregator(ConfigurationAggregatorName);
1104
+ }
1105
+ this.aggregators.delete(ConfigurationAggregatorName);
1106
+ this.logger.info(`[Config] Aggregator '${ConfigurationAggregatorName}' deleted`);
1107
+ this.save();
1108
+ return {};
1109
+ }
1110
+
1111
+ describeConfigurationAggregators({ ConfigurationAggregatorNames, NextToken, Limit } = {}) {
1112
+ let aggregators = Array.from(this.aggregators.values());
1113
+ if (ConfigurationAggregatorNames && ConfigurationAggregatorNames.length > 0) {
1114
+ aggregators = aggregators.filter(a => ConfigurationAggregatorNames.includes(a.ConfigurationAggregatorName));
1115
+ }
1116
+ const { items, nextToken } = paginate(aggregators, NextToken, Limit || MAX_RESULTS_DEFAULT);
1117
+ return { ConfigurationAggregators: items, NextToken: nextToken };
1118
+ }
1119
+
1120
+ // ─── Remediation ────────────────────────────────────────────────────────────
1121
+
1122
+ putRemediationConfigurations({ RemediationConfigurations } = {}) {
1123
+ if (!RemediationConfigurations || RemediationConfigurations.length === 0) {
1124
+ throw Errors.ValidationError('RemediationConfigurations is required');
1125
+ }
1126
+
1127
+ const failures = [];
1128
+ for (const config of RemediationConfigurations) {
1129
+ if (!config.ConfigRuleName) {
1130
+ failures.push({ ConfigRuleName: '', ErrorMessage: 'ConfigRuleName is required' });
1131
+ continue;
1132
+ }
1133
+ const existing = this.remediationConfigs.get(config.ConfigRuleName) || [];
1134
+ existing.push({
1135
+ ...config,
1136
+ Arn: `arn:aws:config:${REGION}:${ACCOUNT_ID}:remediation-configuration/${config.ConfigRuleName}`,
1137
+ CreatedByService: 'Local',
1138
+ });
1139
+ this.remediationConfigs.set(config.ConfigRuleName, existing);
1140
+ }
1141
+
1142
+ this.save();
1143
+ this.logger.info(`[Config] Remediation configurations added`);
1144
+ return { FailedBatches: failures };
1145
+ }
1146
+
1147
+ deleteRemediationConfigurations({ ConfigRuleNames } = {}) {
1148
+ if (!ConfigRuleNames || ConfigRuleNames.length === 0) {
1149
+ throw Errors.ValidationError('ConfigRuleNames is required');
1150
+ }
1151
+ const failures = [];
1152
+ for (const name of ConfigRuleNames) {
1153
+ if (!this.remediationConfigs.has(name)) {
1154
+ failures.push({ ConfigRuleName: name, ErrorMessage: 'Remediation configuration not found' });
1155
+ } else {
1156
+ this.remediationConfigs.delete(name);
1157
+ }
1158
+ }
1159
+ this.save();
1160
+ return { FailedBatches: failures };
1161
+ }
1162
+
1163
+ describeRemediationConfigurations({ ConfigRuleNames } = {}) {
1164
+ if (!ConfigRuleNames || ConfigRuleNames.length === 0) {
1165
+ throw Errors.ValidationError('ConfigRuleNames is required');
1166
+ }
1167
+ const configs = [];
1168
+ for (const name of ConfigRuleNames) {
1169
+ const c = this.remediationConfigs.get(name) || [];
1170
+ configs.push(...c);
1171
+ }
1172
+ return { RemediationConfigurations: configs };
1173
+ }
1174
+
1175
+ startRemediationExecution({ ConfigRuleName, ResourceKeys } = {}) {
1176
+ if (!ConfigRuleName) {
1177
+ throw Errors.ValidationError('ConfigRuleName is required');
1178
+ }
1179
+ if (!this.remediationConfigs.has(ConfigRuleName)) {
1180
+ throw Errors.NoSuchRemediationConfiguration(ConfigRuleName);
1181
+ }
1182
+
1183
+ const failures = [];
1184
+ const executions = this.remediationExecutions.get(ConfigRuleName) || [];
1185
+
1186
+ for (const key of (ResourceKeys || [])) {
1187
+ executions.push({
1188
+ ResourceKey: key,
1189
+ State: 'IN_QUEUE',
1190
+ StepDetails: [],
1191
+ InvocationTime: now(),
1192
+ LastUpdatedTime: now(),
1193
+ });
1194
+ }
1195
+
1196
+ this.remediationExecutions.set(ConfigRuleName, executions);
1197
+
1198
+ // Simula conclusão
1199
+ setImmediate(() => {
1200
+ const execs = this.remediationExecutions.get(ConfigRuleName) || [];
1201
+ for (const exec of execs) {
1202
+ exec.State = 'SUCCEEDED';
1203
+ exec.LastUpdatedTime = now();
1204
+ }
1205
+ this.remediationExecutions.set(ConfigRuleName, execs);
1206
+ this.save();
1207
+ });
1208
+
1209
+ return { FailedItems: failures };
1210
+ }
1211
+
1212
+ // ─── Tags ────────────────────────────────────────────────────────────────────
1213
+
1214
+ tagResource({ ResourceArn, Tags } = {}) {
1215
+ if (!ResourceArn) throw Errors.ValidationError('ResourceArn is required');
1216
+ if (!Tags || Tags.length === 0) throw Errors.ValidationError('Tags is required');
1217
+ const tagMap = this.tags.get(ResourceArn) || {};
1218
+ for (const { Key, Value } of Tags) tagMap[Key] = Value;
1219
+ this.tags.set(ResourceArn, tagMap);
1220
+ this.save();
1221
+ return {};
1222
+ }
1223
+
1224
+ untagResource({ ResourceArn, TagKeys } = {}) {
1225
+ if (!ResourceArn) throw Errors.ValidationError('ResourceArn is required');
1226
+ const tagMap = this.tags.get(ResourceArn) || {};
1227
+ for (const key of (TagKeys || [])) delete tagMap[key];
1228
+ this.tags.set(ResourceArn, tagMap);
1229
+ this.save();
1230
+ return {};
1231
+ }
1232
+
1233
+ listTagsForResource({ ResourceArn, NextToken, Limit } = {}) {
1234
+ if (!ResourceArn) throw Errors.ValidationError('ResourceArn is required');
1235
+ const tagMap = this.tags.get(ResourceArn) || {};
1236
+ const tags = Object.entries(tagMap).map(([Key, Value]) => ({ Key, Value }));
1237
+ const { items, nextToken } = paginate(tags, NextToken, Limit || MAX_RESULTS_DEFAULT);
1238
+ return { Tags: items, NextToken: nextToken };
1239
+ }
1240
+
1241
+ // ─── Método interno: registrar recurso de outro serviço ───────────────────
1242
+
1243
+ recordResource(resourceType, resourceId, configuration) {
1244
+ this._recordResourceConfig(resourceType, resourceId, configuration);
1245
+ }
1246
+
1247
+ getStatus() {
1248
+ return {
1249
+ recorders: this.recorders.size,
1250
+ activeRecorders: Array.from(this.recorderStatus.values()).filter(s => s.recording).length,
1251
+ deliveryChannels: this.deliveryChannels.size,
1252
+ configRules: this.configRules.size,
1253
+ discoveredResources: this.discoveredResources.size,
1254
+ conformancePacks: this.conformancePacks.size,
1255
+ aggregators: this.aggregators.size,
1256
+ };
1257
+ }
1258
+ }
1259
+
1260
+ module.exports = { ConfigSimulator };