@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39

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 (150) hide show
  1. package/README.md +662 -658
  2. package/package.json +11 -4
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/handler-loader.js +50 -5
  100. package/src/services/lambda/index.js +26 -0
  101. package/src/services/lambda/server.js +18 -12
  102. package/src/services/lambda/simulator.js +73 -17
  103. package/src/services/opensearch/index.js +105 -0
  104. package/src/services/opensearch/models/document.js +115 -0
  105. package/src/services/opensearch/models/index.js +73 -0
  106. package/src/services/opensearch/server.js +310 -0
  107. package/src/services/opensearch/simulator.js +605 -0
  108. package/src/services/opensearch/store.js +162 -0
  109. package/src/services/organizations/control-tower-simulator.js +321 -0
  110. package/src/services/organizations/control-tower-store.js +182 -0
  111. package/src/services/organizations/iam-integration.js +413 -0
  112. package/src/services/organizations/index.js +172 -0
  113. package/src/services/organizations/models/index.js +13 -0
  114. package/src/services/organizations/models/invitation.js +48 -0
  115. package/src/services/organizations/models/organization.js +566 -0
  116. package/src/services/organizations/models/role.js +244 -0
  117. package/src/services/organizations/models/scp.js +131 -0
  118. package/src/services/organizations/server.js +736 -0
  119. package/src/services/organizations/simulator.js +593 -0
  120. package/src/services/organizations/store.js +235 -0
  121. package/src/services/organizations/vpc-integration.js +453 -0
  122. package/src/services/route53/index.js +67 -0
  123. package/src/services/route53/models/healthCheck.js +101 -0
  124. package/src/services/route53/models/hostedZone.js +37 -0
  125. package/src/services/route53/models/resourceRecord.js +106 -0
  126. package/src/services/route53/server.js +841 -0
  127. package/src/services/route53/simulator.js +350 -0
  128. package/src/services/s3/index.js +62 -0
  129. package/src/services/s3/presigned-url-generator.js +337 -0
  130. package/src/services/s3/server.js +231 -0
  131. package/src/services/s3/simulator.js +551 -4
  132. package/src/services/sns/index.js +31 -0
  133. package/src/services/sns/server.js +9 -0
  134. package/src/services/sqs/index.js +38 -0
  135. package/src/services/sqs/server.js +57 -4
  136. package/src/services/sqs/simulator.js +423 -33
  137. package/src/services/sts/simulator.js +209 -17
  138. package/src/services/vpc/README.md +459 -0
  139. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  140. package/src/services/vpc/index.js +90 -0
  141. package/src/services/vpc/models/internetGateway.js +71 -0
  142. package/src/services/vpc/models/routeTable.js +160 -0
  143. package/src/services/vpc/models/subnet.js +48 -0
  144. package/src/services/vpc/models/vpc.js +37 -0
  145. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  146. package/src/services/vpc/models/vpcGateway.js +134 -0
  147. package/src/services/vpc/server.js +836 -0
  148. package/src/services/vpc/simulator.js +613 -0
  149. package/src/utils/policy-cache.js +215 -0
  150. package/aws-config +0 -154
@@ -364,7 +364,15 @@ class EventBridgeSimulator {
364
364
  InputTransformer: target.InputTransformer || null,
365
365
  RoleArn: target.RoleArn || null,
366
366
  RetryPolicy: target.RetryPolicy || { MaximumRetryAttempts: 185, MaximumEventAgeInSeconds: 86400 },
367
- DeadLetterConfig: target.DeadLetterConfig || null
367
+ DeadLetterConfig: target.DeadLetterConfig || null,
368
+ MessageGroupId: target.MessageGroupId || null,
369
+ KinesisParameters: target.KinesisParameters || null,
370
+ EcsParameters: target.EcsParameters || null,
371
+ BatchParameters: target.BatchParameters || null,
372
+ HttpParameters: target.HttpParameters || null,
373
+ RedshiftDataParameters: target.RedshiftDataParameters || null,
374
+ SageMakerPipelineParameters: target.SageMakerPipelineParameters || null,
375
+ AppSyncParameters: target.AppSyncParameters || null
368
376
  });
369
377
  }
370
378
 
@@ -497,9 +505,15 @@ class EventBridgeSimulator {
497
505
  const targets = this.targets.get(rule.Arn) || [];
498
506
  for (const target of targets) {
499
507
  try {
500
- await this._deliverToTarget(target, event);
508
+ await this._deliverToTarget(target, event, { ruleArn: rule.Arn, ruleName: rule.Name, eventBusName: busName });
501
509
  } catch (err) {
502
510
  this.logger.warn('EventBridge', `Target delivery failed: ${err.message}`);
511
+ // DeadLetterConfig: AWS moves failed events to the DLQ SQS ARN
512
+ const dlqArn = target.DeadLetterConfig && target.DeadLetterConfig.Arn;
513
+ if (dlqArn && this.sqsService) {
514
+ try { await this._deliverToSQS(dlqArn, JSON.stringify(event), { _eventbridge_dlq_original_target: target.Arn }); }
515
+ catch (dlqErr) { this.logger.warn('EventBridge DLQ', `DLQ delivery also failed: ${dlqErr.message}`); }
516
+ }
503
517
  }
504
518
  }
505
519
  }
@@ -513,38 +527,83 @@ class EventBridgeSimulator {
513
527
  * @returns {boolean}
514
528
  */
515
529
  _matchesPattern(event, pattern) {
530
+ // AWS EventBridge $or (array of pattern objects, match any)
531
+ if (Array.isArray(pattern)) {
532
+ return pattern.some(subPattern => this._matchesPattern(event, subPattern));
533
+ }
534
+
516
535
  for (const [key, matchers] of Object.entries(pattern)) {
517
- const eventValue = key === 'detail' ? event.detail : event[key];
536
+ if (key === '$or') {
537
+ if (!Array.isArray(matchers)) continue;
538
+ const orMatch = matchers.some(sub => this._matchesPattern(event, sub));
539
+ if (!orMatch) return false;
540
+ continue;
541
+ }
518
542
 
519
543
  if (key === 'detail' && typeof matchers === 'object' && !Array.isArray(matchers)) {
520
- // Recursive detail matching
521
544
  if (!this._matchesPattern(event.detail || {}, matchers)) return false;
522
545
  continue;
523
546
  }
524
547
 
525
- if (!Array.isArray(matchers)) continue;
548
+ const eventValue = key === 'detail' ? event.detail : event[key];
526
549
 
527
- const matches = matchers.some(matcher => {
528
- if (matcher === null) return eventValue === null;
529
- if (typeof matcher === 'string') return eventValue === matcher;
530
- if (typeof matcher === 'object') {
531
- if (matcher.prefix) return String(eventValue).startsWith(matcher.prefix);
532
- if (matcher['anything-but']) {
533
- return !matcher['anything-but'].includes(eventValue);
534
- }
535
- if (matcher.exists !== undefined) {
536
- return matcher.exists ? eventValue !== undefined : eventValue === undefined;
537
- }
538
- if (matcher.numeric) return this._checkNumeric(parseFloat(eventValue), matcher.numeric);
539
- }
540
- return false;
541
- });
550
+ // AWS: if matcher is array and field value is also array → any element matches any matcher
551
+ if (Array.isArray(matchers) && Array.isArray(eventValue)) {
552
+ const arrMatch = eventValue.some(v => matchers.some(m => this._singleMatcherMatches(v, m)));
553
+ if (!arrMatch) return false;
554
+ continue;
555
+ }
542
556
 
557
+ if (!Array.isArray(matchers)) continue;
558
+
559
+ const matches = matchers.some(matcher => this._singleMatcherMatches(eventValue, matcher));
543
560
  if (!matches) return false;
544
561
  }
545
562
  return true;
546
563
  }
547
564
 
565
+ _singleMatcherMatches(eventValue, matcher) {
566
+ if (matcher === null) return eventValue === null;
567
+ if (typeof matcher === 'string') {
568
+ if (matcher.endsWith('*')) {
569
+ const prefix = matcher.slice(0, -1);
570
+ return eventValue != null && String(eventValue).startsWith(prefix);
571
+ }
572
+ if (matcher.startsWith('*')) {
573
+ const suffix = matcher.slice(1);
574
+ return eventValue != null && String(eventValue).endsWith(suffix);
575
+ }
576
+ return eventValue === matcher;
577
+ }
578
+ if (typeof matcher === 'object') {
579
+ if (matcher.prefix) return String(eventValue).startsWith(matcher.prefix);
580
+ if (matcher.suffix) return String(eventValue).endsWith(matcher.suffix);
581
+ if (matcher.wildcard) {
582
+ const wc = matcher.wildcard;
583
+ let regex = '^';
584
+ for (let i = 0; i < wc.length; i++) {
585
+ const c = wc[i];
586
+ if (c === '*') regex += '.*';
587
+ else if (c === '?') regex += '.';
588
+ else regex += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
589
+ }
590
+ regex += '$';
591
+ try { return new RegExp(regex).test(String(eventValue)); } catch (_e) { return false; }
592
+ }
593
+ if (matcher['anything-but']) {
594
+ const forbidden = matcher['anything-but'];
595
+ if (Array.isArray(forbidden)) return !forbidden.includes(eventValue);
596
+ return eventValue !== forbidden;
597
+ }
598
+ if (matcher.exists !== undefined) {
599
+ return matcher.exists ? eventValue !== undefined : eventValue === undefined;
600
+ }
601
+ if (matcher.numeric) return this._checkNumeric(parseFloat(eventValue), matcher.numeric);
602
+ if (matcher.ctype === 'string') return typeof eventValue === 'string';
603
+ }
604
+ return false;
605
+ }
606
+
548
607
  /**
549
608
  * Check numeric conditions
550
609
  * @param {number} value
@@ -564,57 +623,137 @@ class EventBridgeSimulator {
564
623
  return true;
565
624
  }
566
625
 
626
+ /**
627
+ * Resolve SQS ARN or URL → SQS QueueUrl (formato aceito pelo SQS simulator)
628
+ */
629
+ _resolveSqsQueueUrl(arnOrUrlOrName) {
630
+ if (!arnOrUrlOrName) return null;
631
+ const sqs = this.sqsService && this.sqsService.simulator;
632
+ if (!sqs) return null;
633
+
634
+ if (typeof arnOrUrlOrName !== 'string') return null;
635
+ if (/^https?:\/\//i.test(arnOrUrlOrName) || arnOrUrlOrName.includes('/queue/')) {
636
+ return arnOrUrlOrName;
637
+ }
638
+ let name = arnOrUrlOrName;
639
+ const arnMatch = arnOrUrlOrName.match(/:queue:([^:]+)$|:sqs:[^:]*:[^:]*:([^:/]+)/);
640
+ if (arnMatch) name = arnMatch[1] || arnMatch[2];
641
+
642
+ const q = sqs.queues && sqs.queues.get(name);
643
+ return (q && q.url) || (sqs.endpoint ? `${sqs.endpoint}/queue/${name}` : null);
644
+ }
645
+
646
+ /**
647
+ * Deliver directly to SQS with ARN/URL resolution + AWS-style MessageAttributes
648
+ */
649
+ async _deliverToSQS(queueArnOrUrl, messageBody, extraAttributes = {}) {
650
+ if (!this.sqsService) throw new Error('SQS service not available');
651
+ const sqs = this.sqsService.simulator;
652
+ const queueUrl = this._resolveSqsQueueUrl(queueArnOrUrl);
653
+ if (!queueUrl) throw new Error(`Unable to resolve SQS queue: ${queueArnOrUrl}`);
654
+
655
+ const traceId = crypto.randomUUID();
656
+ const MessageAttributes = {
657
+ AWSTraceHeader: {
658
+ DataType: 'String',
659
+ StringValue: `Root=1-${Date.now().toString(16)}-${traceId.replace(/-/g, '')}`
660
+ },
661
+ ...Object.fromEntries(Object.entries(extraAttributes || {}).map(([k, v]) => [
662
+ k,
663
+ typeof v === 'string' ? { DataType: 'String', StringValue: v } : { DataType: 'String', StringValue: JSON.stringify(v) }
664
+ ]))
665
+ };
666
+
667
+ const result = sqs.sendMessage({
668
+ QueueUrl: queueUrl,
669
+ MessageBody: messageBody,
670
+ MessageAttributes
671
+ });
672
+
673
+ if (!result || result.error) throw new Error((result && result.error && result.error.message) || 'SQS sendMessage failed');
674
+ return result;
675
+ }
676
+
567
677
  /**
568
678
  * Deliver event to target
569
679
  * @param {Object} target
570
680
  * @param {Object} event
681
+ * @param {Object} [ruleInfo]
571
682
  */
572
- async _deliverToTarget(target, event) {
573
- // Transform input
683
+ async _deliverToTarget(target, event, ruleInfo) {
684
+ // Apply Input / InputPath / InputTransformer (AWS precedence)
574
685
  let inputEvent = event;
575
686
  if (target.Input) {
576
687
  try { inputEvent = JSON.parse(target.Input); } catch { inputEvent = target.Input; }
577
688
  } else if (target.InputPath) {
578
689
  inputEvent = this._extractPath(event, target.InputPath);
579
690
  } else if (target.InputTransformer) {
580
- inputEvent = this._transformInput(event, target.InputTransformer);
691
+ inputEvent = this._transformInput(event, target.InputTransformer, ruleInfo);
581
692
  }
582
693
 
583
694
  const arn = target.Arn;
584
695
 
585
696
  // Lambda target
586
- if (arn.includes(':lambda:') || arn.includes('function:')) {
587
- if (!this.lambdaService) return;
588
- const match = arn.match(/function:([^:]+)/);
589
- if (!match) return;
590
- await this.lambdaService.simulator.invokeFunction(match[1], inputEvent);
591
- this.logger.debug('EventBridge', `Delivered to Lambda: ${match[1]}`);
697
+ if (arn.includes(':lambda:') || arn.includes(':function:') || (arn.startsWith('arn:aws:lambda') || arn.startsWith('arn:aws-cn:lambda') || arn.startsWith('arn:aws-us-gov:lambda'))) {
698
+ if (!this.lambdaService) throw new Error('Lambda service not available');
699
+ let functionName = null;
700
+ const match = arn.match(/:function:([^:/]+)/);
701
+ if (match) {
702
+ functionName = match[1];
703
+ } else {
704
+ functionName = arn;
705
+ }
706
+ if (!functionName) throw new Error(`Could not resolve Lambda function from ARN: ${arn}`);
707
+ // AWS EventBridge invokes Lambda asynchronously with invocation-type=Event
708
+ await this.lambdaService.simulator.invokeFunction(functionName, inputEvent);
709
+ this.logger.debug('EventBridge', `Delivered to Lambda: ${functionName}`);
592
710
  return;
593
711
  }
594
712
 
595
713
  // SQS target
596
714
  if (arn.includes(':sqs:') || arn.includes(':queue:')) {
597
- if (!this.sqsService) return;
598
- await this.sqsService.simulator.sendMessage({
599
- QueueUrl: arn,
600
- MessageBody: JSON.stringify(inputEvent)
601
- });
715
+ const isFifo = arn.endsWith('.fifo');
716
+ const msgBody = typeof inputEvent === 'string' ? inputEvent : JSON.stringify(inputEvent);
717
+ const extraAttrs = {};
718
+ if (ruleInfo && ruleInfo.ruleArn) extraAttrs['EventBridgeRuleArn'] = ruleInfo.ruleArn;
719
+ const sendParams = {
720
+ QueueUrl: this._resolveSqsQueueUrl(arn) || arn,
721
+ MessageBody: msgBody
722
+ };
723
+ if (isFifo) {
724
+ sendParams.MessageGroupId = target.MessageGroupId || `${ruleInfo ? ruleInfo.ruleArn : 'eventbridge'}-${target.Id || 'tgt'}`;
725
+ if (target.Id) extraAttrs['TargetId'] = target.Id;
726
+ }
727
+ const result = await this._deliverToSQS(sendParams.QueueUrl, sendParams.MessageBody, extraAttrs);
602
728
  this.logger.debug('EventBridge', `Delivered to SQS: ${arn}`);
603
729
  return;
604
730
  }
605
731
 
606
732
  // SNS target
607
733
  if (arn.includes(':sns:')) {
608
- if (!this.snsService) return;
609
- await this.snsService.simulator.publish({
734
+ if (!this.snsService) throw new Error('SNS service not available');
735
+ const message = typeof inputEvent === 'string' ? inputEvent : JSON.stringify(inputEvent);
736
+ const traceId = crypto.randomUUID();
737
+ const attrs = {
738
+ AWSTraceHeader: { DataType: 'String', StringValue: traceId }
739
+ };
740
+ if (ruleInfo && ruleInfo.ruleArn) {
741
+ attrs['EventBridgeRuleArn'] = { DataType: 'String', StringValue: ruleInfo.ruleArn };
742
+ }
743
+ const result = await this.snsService.simulator.publish({
610
744
  TopicArn: arn,
611
- Message: JSON.stringify(inputEvent)
745
+ Message: message,
746
+ MessageAttributes: attrs
612
747
  });
748
+ if (!result || (result && result.error)) {
749
+ throw new Error((result && result.error && result.error.message) || 'SNS publish failed');
750
+ }
613
751
  this.logger.debug('EventBridge', `Delivered to SNS: ${arn}`);
614
752
  return;
615
753
  }
616
754
 
617
755
  this.logger.warn('EventBridge', `Unsupported target ARN: ${arn}`);
756
+ throw new Error(`Unsupported target type for ARN: ${arn}`);
618
757
  }
619
758
 
620
759
  /**
@@ -624,30 +763,76 @@ class EventBridgeSimulator {
624
763
  * @returns {*}
625
764
  */
626
765
  _extractPath(obj, path) {
766
+ if (!path) return obj;
627
767
  if (path === '$') return obj;
628
- const parts = path.replace(/^\$\./, '').split('.');
768
+ const cleaned = path.replace(/^\$\.?/, '');
769
+ if (!cleaned) return obj;
770
+
771
+ // Tokenize: suporta dotted keys e bracket array notation como resources[0], [0]
772
+ const tokens = [];
773
+ let buf = '';
774
+ for (let i = 0; i < cleaned.length; i++) {
775
+ const c = cleaned[i];
776
+ if (c === '.') {
777
+ if (buf) tokens.push({ type: 'key', value: buf });
778
+ buf = '';
779
+ } else if (c === '[') {
780
+ if (buf) tokens.push({ type: 'key', value: buf });
781
+ buf = '';
782
+ } else if (c === ']') {
783
+ if (buf.length > 0) {
784
+ const n = Number(buf);
785
+ tokens.push({ type: Number.isNaN(n) ? 'key' : 'index', value: Number.isNaN(n) ? buf : n });
786
+ }
787
+ buf = '';
788
+ } else {
789
+ buf += c;
790
+ }
791
+ }
792
+ if (buf) tokens.push({ type: 'key', value: buf });
793
+
629
794
  let current = obj;
630
- for (const part of parts) {
795
+ for (const t of tokens) {
631
796
  if (current === null || current === undefined) return null;
632
- current = current[part];
797
+ if (t.type === 'index') {
798
+ if (!Array.isArray(current)) return null;
799
+ current = current[t.value];
800
+ } else {
801
+ current = current[t.value];
802
+ }
633
803
  }
634
804
  return current;
635
805
  }
636
806
 
637
807
  /**
638
- * Transform input using InputTransformer
808
+ * Transform input using InputTransformer (AWS style)
639
809
  * @param {Object} event
640
810
  * @param {Object} transformer
811
+ * @param {Object} [ruleInfo] optional rule info for <aws.events.rule-arn>, etc.
641
812
  * @returns {*}
642
813
  */
643
- _transformInput(event, transformer) {
814
+ _transformInput(event, transformer, ruleInfo) {
644
815
  const { InputPathsMap, InputTemplate } = transformer;
645
- let result = InputTemplate;
816
+ if (!InputTemplate) return event;
817
+ let result = String(InputTemplate);
818
+
819
+ // AWS reserved placeholders
820
+ const reserved = {
821
+ 'aws.events.event': JSON.stringify(event),
822
+ 'aws.events.event.json': JSON.stringify(event),
823
+ 'aws.events.rule-arn': (ruleInfo && ruleInfo.ruleArn) || '',
824
+ 'aws.events.rule-name': (ruleInfo && ruleInfo.ruleName) || '',
825
+ 'aws.events.eventbus-name': (ruleInfo && ruleInfo.eventBusName) || ''
826
+ };
827
+ for (const [k, v] of Object.entries(reserved)) {
828
+ result = result.split(`<${k}>`).join(typeof v === 'string' ? v : JSON.stringify(v));
829
+ }
646
830
 
647
- if (InputPathsMap && InputTemplate) {
831
+ if (InputPathsMap && typeof InputPathsMap === 'object') {
648
832
  for (const [key, path] of Object.entries(InputPathsMap)) {
649
833
  const value = this._extractPath(event, path);
650
- result = result.replace(new RegExp(`<${key}>`, 'g'), JSON.stringify(value));
834
+ const safeValue = typeof value === 'string' ? value : JSON.stringify(value);
835
+ result = result.split(`<${key}>`).join(safeValue);
651
836
  }
652
837
  }
653
838
 
@@ -0,0 +1,65 @@
1
+ const IAMSimulator = require('./simulator');
2
+ const logger = require('../../utils/logger');
3
+
4
+ class IAMService {
5
+ constructor(config, dependencies) {
6
+ this.config = config;
7
+ this.dependencies = dependencies || {};
8
+ this.name = 'iam';
9
+ this.port = config?.ports?.iam || 4003;
10
+ this.simulator = null;
11
+ this.server = null;
12
+ this.httpServer = null;
13
+ this.isRunning = false;
14
+ }
15
+
16
+ async initialize() {
17
+ this.simulator = new IAMSimulator(this.config);
18
+ await this.simulator.initialize();
19
+
20
+ const IAMServer = require('./server');
21
+ this.server = new IAMServer(this.simulator, logger, this.config);
22
+ }
23
+
24
+ async start() {
25
+ if (this.isRunning) return;
26
+
27
+ this.httpServer = this.server.getApp().listen(this.port);
28
+ logger.info(`🔐 IAM rodando em http://localhost:${this.port}`);
29
+ this.isRunning = true;
30
+ }
31
+
32
+ async stop() {
33
+ if (!this.isRunning) return;
34
+
35
+ await new Promise((resolve, reject) => {
36
+ this.httpServer.close((err) => {
37
+ if (err) reject(err);
38
+ else resolve();
39
+ });
40
+ });
41
+ this.isRunning = false;
42
+ }
43
+
44
+ async reset() {
45
+ await this.simulator.reset();
46
+ }
47
+
48
+ getStatus() {
49
+ return {
50
+ running: this.isRunning,
51
+ port: this.port,
52
+ endpoint: `http://localhost:${this.port}`
53
+ };
54
+ }
55
+
56
+ getSimulator() {
57
+ return this.simulator;
58
+ }
59
+
60
+ injectDependencies(server) {
61
+ this.stsService = server.getService('sts');
62
+ }
63
+ }
64
+
65
+ module.exports = IAMService;
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extracts AWS AccessKeyId from the request.
5
+ * Checks Authorization header (AWS Signature V4) and fallback to query params.
6
+ *
7
+ * @param {object} req - Express request object
8
+ * @returns {string|null} The extracted AccessKeyId or null if not found
9
+ */
10
+ function extractCredentials(req) {
11
+ // Check Authorization header: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, ...
12
+ const authHeader = req.headers['authorization'] || req.headers['Authorization'];
13
+ if (authHeader && authHeader.startsWith('AWS4-HMAC-SHA256')) {
14
+ const credMatch = authHeader.match(/Credential=([A-Z0-9]+)\//);
15
+ if (credMatch) return credMatch[1];
16
+ }
17
+
18
+ // Fallback: X-Amz-Credential query parameter (presigned URLs)
19
+ const xAmzCredential = req.query && req.query['X-Amz-Credential'];
20
+ if (xAmzCredential) {
21
+ const parts = xAmzCredential.split('/');
22
+ if (parts.length > 0) return parts[0];
23
+ }
24
+
25
+ // Fallback: AWSAccessKeyId query parameter (older signature versions)
26
+ if (req.query && req.query['AWSAccessKeyId']) {
27
+ return req.query['AWSAccessKeyId'];
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Creates an IAM enforcement middleware for a specific AWS service.
35
+ *
36
+ * @param {object} options
37
+ * @param {object} options.iamSimulator - Reference to IAM simulator instance
38
+ * @param {string} options.servicePrefix - IAM action prefix (e.g., 'sqs', 'sns', 'lambda', 's3')
39
+ * @param {function} options.extractAction - (req) => string - extracts AWS action name from request
40
+ * @param {function} options.extractResource - (req) => string - builds resource ARN from request
41
+ * @param {function} [options.extractConditionContext] - (req) => object - additional condition context keys
42
+ * @param {function} options.formatError - (res, code, message) => void - formats service-specific error response
43
+ * @returns {function} Express middleware function
44
+ */
45
+ function createIAMEnforcementMiddleware(options) {
46
+ const {
47
+ iamSimulator,
48
+ servicePrefix,
49
+ extractAction,
50
+ extractResource,
51
+ extractConditionContext,
52
+ formatError
53
+ } = options;
54
+
55
+ return (req, res, next) => {
56
+ // 1. Extract credentials from Authorization header or query parameters
57
+ const accessKeyId = extractCredentials(req);
58
+
59
+ // 2. If no credentials present → 403 AccessDenied
60
+ if (!accessKeyId) {
61
+ return formatError(res, 'AccessDenied', 'Missing authentication token');
62
+ }
63
+
64
+ // 3. Resolve identity via IAM service
65
+ const identity = iamSimulator.resolveIdentity(accessKeyId);
66
+ if (!identity) {
67
+ return formatError(res, 'AccessDenied', 'The security token included in the request is invalid');
68
+ }
69
+
70
+ // 4. Build evaluation context
71
+ const action = `${servicePrefix}:${extractAction(req)}`;
72
+ const resource = extractResource(req);
73
+ const context = {
74
+ 'aws:SourceIp': req.ip || req.connection?.remoteAddress || '127.0.0.1',
75
+ 'aws:CurrentTime': new Date().toISOString(),
76
+ 'aws:EpochTime': Math.floor(Date.now() / 1000).toString(),
77
+ ...(extractConditionContext ? extractConditionContext(req) : {})
78
+ };
79
+
80
+ // 5. Evaluate access using the policy engine
81
+ const decision = iamSimulator.evaluateAccess({
82
+ accessKeyId,
83
+ action,
84
+ resource,
85
+ context
86
+ });
87
+
88
+ // 6. Deny → 403, Allow → set identity and call next()
89
+ if (decision === 'Deny') {
90
+ return formatError(res, 'AccessDenied',
91
+ `User is not authorized to perform: ${action} on resource: ${resource}`);
92
+ }
93
+
94
+ req.iamIdentity = identity;
95
+ next();
96
+ };
97
+ }
98
+
99
+ module.exports = { createIAMEnforcementMiddleware, extractCredentials };