@gugananuvem/aws-local-simulator 1.0.35 → 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.
- package/README.md +662 -658
- package/package.json +10 -3
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +12 -1
- package/src/services/lambda/simulator.js +4 -0
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- 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
|
-
|
|
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
|
-
|
|
548
|
+
const eventValue = key === 'detail' ? event.detail : event[key];
|
|
526
549
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
//
|
|
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)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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)
|
|
609
|
-
|
|
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:
|
|
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
|
|
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
|
|
795
|
+
for (const t of tokens) {
|
|
631
796
|
if (current === null || current === undefined) return null;
|
|
632
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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 };
|