@gugananuvem/aws-local-simulator 1.0.15 → 1.0.16

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 (77) hide show
  1. package/README.md +789 -594
  2. package/bin/aws-local-simulator.js +63 -63
  3. package/package.json +2 -2
  4. package/src/config/config-loader.js +114 -114
  5. package/src/config/default-config.js +68 -68
  6. package/src/config/env-loader.js +68 -68
  7. package/src/index.js +146 -146
  8. package/src/index.mjs +123 -123
  9. package/src/server.js +227 -227
  10. package/src/services/apigateway/index.js +75 -73
  11. package/src/services/apigateway/server.js +570 -507
  12. package/src/services/apigateway/simulator.js +1261 -1261
  13. package/src/services/athena/index.js +75 -75
  14. package/src/services/athena/server.js +101 -101
  15. package/src/services/athena/simulador.js +998 -998
  16. package/src/services/athena/simulator.js +346 -346
  17. package/src/services/cloudformation/index.js +106 -106
  18. package/src/services/cloudformation/server.js +417 -417
  19. package/src/services/cloudformation/simulador.js +1045 -1045
  20. package/src/services/cloudtrail/index.js +84 -84
  21. package/src/services/cloudtrail/server.js +235 -235
  22. package/src/services/cloudtrail/simulador.js +719 -719
  23. package/src/services/cloudwatch/index.js +84 -84
  24. package/src/services/cloudwatch/server.js +366 -366
  25. package/src/services/cloudwatch/simulador.js +1173 -1173
  26. package/src/services/cognito/index.js +79 -79
  27. package/src/services/cognito/server.js +301 -301
  28. package/src/services/cognito/simulator.js +1655 -1655
  29. package/src/services/config/index.js +96 -96
  30. package/src/services/config/server.js +215 -215
  31. package/src/services/config/simulador.js +1260 -1260
  32. package/src/services/dynamodb/index.js +74 -74
  33. package/src/services/dynamodb/server.js +125 -125
  34. package/src/services/dynamodb/simulator.js +630 -630
  35. package/src/services/ecs/index.js +65 -65
  36. package/src/services/ecs/server.js +235 -235
  37. package/src/services/ecs/simulator.js +844 -844
  38. package/src/services/eventbridge/index.js +89 -89
  39. package/src/services/eventbridge/server.js +209 -209
  40. package/src/services/eventbridge/simulator.js +684 -684
  41. package/src/services/index.js +45 -45
  42. package/src/services/kms/index.js +75 -75
  43. package/src/services/kms/server.js +67 -67
  44. package/src/services/kms/simulator.js +324 -324
  45. package/src/services/lambda/handler-loader.js +183 -183
  46. package/src/services/lambda/index.js +78 -78
  47. package/src/services/lambda/route-registry.js +274 -274
  48. package/src/services/lambda/server.js +145 -145
  49. package/src/services/lambda/simulator.js +199 -199
  50. package/src/services/parameter-store/index.js +80 -80
  51. package/src/services/parameter-store/server.js +50 -50
  52. package/src/services/parameter-store/simulator.js +201 -201
  53. package/src/services/s3/index.js +73 -73
  54. package/src/services/s3/server.js +329 -329
  55. package/src/services/s3/simulator.js +565 -565
  56. package/src/services/secret-manager/index.js +80 -80
  57. package/src/services/secret-manager/server.js +50 -50
  58. package/src/services/secret-manager/simulator.js +171 -171
  59. package/src/services/sns/index.js +89 -89
  60. package/src/services/sns/server.js +580 -580
  61. package/src/services/sns/simulator.js +1482 -1482
  62. package/src/services/sqs/index.js +98 -93
  63. package/src/services/sqs/server.js +349 -349
  64. package/src/services/sqs/simulator.js +441 -441
  65. package/src/services/sts/index.js +37 -37
  66. package/src/services/sts/server.js +144 -144
  67. package/src/services/sts/simulator.js +69 -69
  68. package/src/services/xray/index.js +83 -83
  69. package/src/services/xray/server.js +308 -308
  70. package/src/services/xray/simulador.js +994 -994
  71. package/src/template/aws-config-template.js +87 -87
  72. package/src/template/aws-config-template.mjs +90 -90
  73. package/src/template/config-template.json +203 -203
  74. package/src/utils/aws-config.js +91 -91
  75. package/src/utils/cloudtrail-audit.js +129 -129
  76. package/src/utils/local-store.js +83 -83
  77. package/src/utils/logger.js +59 -59
@@ -1,1260 +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 };
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 };