@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
@@ -8,6 +8,7 @@ const path = require("path");
8
8
  const LocalStore = require("../../utils/local-store");
9
9
  const logger = require("../../utils/logger");
10
10
  const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
11
+ const PresignedUrlGenerator = require("./presigned-url-generator");
11
12
 
12
13
  class S3Simulator {
13
14
  constructor(config) {
@@ -16,8 +17,20 @@ class S3Simulator {
16
17
  this.store = new LocalStore(this.dataDir);
17
18
  this.buckets = new Map();
18
19
  this.audit = new CloudTrailAudit("s3.amazonaws.com");
20
+ this.lambdaService = null;
21
+ this.snsService = null;
22
+ this.sqsService = null;
23
+ this.eventbridgeService = null;
24
+ this.region = "us-east-1";
25
+ this.accountId = "000000000000";
26
+ this.presignedUrlGenerator = new PresignedUrlGenerator(config);
19
27
  }
20
28
 
29
+ setLambdaService(service) { this.lambdaService = service; }
30
+ setSnsService(service) { this.snsService = service; }
31
+ setSqsService(service) { this.sqsService = service; }
32
+ setEventbridgeService(service) { this.eventbridgeService = service; }
33
+
21
34
  async initialize() {
22
35
  logger.debug("Inicializando S3 Simulator...");
23
36
  this.loadBuckets();
@@ -49,6 +62,7 @@ class S3Simulator {
49
62
  objects,
50
63
  objectCount: data.objectCount || 0,
51
64
  totalSize: data.totalSize || 0,
65
+ notificationConfiguration: data.notificationConfiguration || null,
52
66
  });
53
67
  }
54
68
  }
@@ -57,13 +71,14 @@ class S3Simulator {
57
71
  // Cria buckets definidos na config que ainda não existem
58
72
  if (this.config.s3?.buckets) {
59
73
  for (const bucketDef of this.config.s3.buckets) {
60
- // suporta string simples ou objeto { name, region, websiteConfiguration }
74
+ // suporta string simples ou objeto { name, region, websiteConfiguration, notificationConfiguration }
61
75
  const bucketName = typeof bucketDef === "string" ? bucketDef : bucketDef.name;
62
76
  const region = typeof bucketDef === "object" ? (bucketDef.region || "us-east-1") : "us-east-1";
63
77
  const websiteConfig = typeof bucketDef === "object" ? (bucketDef.websiteConfiguration || null) : null;
78
+ const notificationConfig = typeof bucketDef === "object" ? (bucketDef.notificationConfiguration || null) : null;
64
79
 
65
80
  if (!this.buckets.has(bucketName)) {
66
- this.createBucket(bucketName, { region, websiteConfiguration: websiteConfig });
81
+ this.createBucket(bucketName, { region, websiteConfiguration: websiteConfig, notificationConfiguration: notificationConfig });
67
82
  }
68
83
  }
69
84
  }
@@ -111,7 +126,7 @@ class S3Simulator {
111
126
  return { error: { code: "BucketAlreadyExists", message: "Bucket already exists" }, status: 409 };
112
127
  }
113
128
 
114
- const { region = "us-east-1", websiteConfiguration = null } = options;
129
+ const { region = "us-east-1", websiteConfiguration = null, notificationConfiguration = null } = options;
115
130
 
116
131
  const bucket = {
117
132
  name: bucketName,
@@ -127,11 +142,13 @@ class S3Simulator {
127
142
  objects: new Map(),
128
143
  objectCount: 0,
129
144
  totalSize: 0,
145
+ notificationConfiguration: this._normalizeNotificationConfig(notificationConfiguration),
146
+ policy: null, // Bucket policy para controle de acesso
130
147
  };
131
148
 
132
149
  this.buckets.set(bucketName, bucket);
133
150
  this.persistBuckets();
134
- logger.debug(`✅ Bucket S3 criado: ${bucketName} (region: ${region}${websiteConfiguration ? ", website: enabled" : ""})`);
151
+ logger.debug(`✅ Bucket S3 criado: ${bucketName} (region: ${region}${websiteConfiguration ? ", website: enabled" : ""}${bucket.notificationConfiguration ? ", notifications: enabled" : ""})`);
135
152
  this.audit.record({ eventName: "CreateBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }], requestParameters: { bucketName } });
136
153
  return { bucket };
137
154
  }
@@ -152,6 +169,82 @@ class S3Simulator {
152
169
  return { success: true };
153
170
  }
154
171
 
172
+ /**
173
+ * Definir política do bucket (bucket policy)
174
+ * Controla quem pode acessar o bucket e seus objetos
175
+ */
176
+ putBucketPolicy(bucketName, policy) {
177
+ const bucket = this.buckets.get(bucketName);
178
+ if (!bucket) {
179
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
180
+ }
181
+
182
+ try {
183
+ // Validar que é um JSON válido
184
+ const policyObject = typeof policy === 'string' ? JSON.parse(policy) : policy;
185
+
186
+ if (!policyObject.Statement || !Array.isArray(policyObject.Statement)) {
187
+ return { error: { code: "InvalidPolicyDocument", message: "Invalid bucket policy" }, status: 400 };
188
+ }
189
+
190
+ bucket.policy = policyObject;
191
+ this.persistBuckets();
192
+
193
+ logger.debug(`📋 Política de bucket definida: ${bucketName}`);
194
+ this.audit.record({
195
+ eventName: "PutBucketPolicy",
196
+ readOnly: false,
197
+ resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }],
198
+ requestParameters: { bucketName, policy: policyObject }
199
+ });
200
+
201
+ return { success: true };
202
+ } catch (error) {
203
+ return { error: { code: "InvalidPolicyDocument", message: error.message }, status: 400 };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Obter política do bucket
209
+ */
210
+ getBucketPolicy(bucketName) {
211
+ const bucket = this.buckets.get(bucketName);
212
+ if (!bucket) {
213
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
214
+ }
215
+
216
+ if (!bucket.policy) {
217
+ return { error: { code: "NoSuchBucketPolicy", message: "The bucket policy does not exist" }, status: 404 };
218
+ }
219
+
220
+ return {
221
+ policy: bucket.policy,
222
+ status: 200
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Deletar política do bucket
228
+ */
229
+ deleteBucketPolicy(bucketName) {
230
+ const bucket = this.buckets.get(bucketName);
231
+ if (!bucket) {
232
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
233
+ }
234
+
235
+ bucket.policy = null;
236
+ this.persistBuckets();
237
+
238
+ logger.debug(`🗑️ Política de bucket deletada: ${bucketName}`);
239
+ this.audit.record({
240
+ eventName: "DeleteBucketPolicy",
241
+ readOnly: false,
242
+ resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }]
243
+ });
244
+
245
+ return { success: true };
246
+ }
247
+
155
248
  listBuckets() {
156
249
  return Array.from(this.buckets.values()).map((bucket) => ({
157
250
  Name: bucket.name,
@@ -180,6 +273,7 @@ class S3Simulator {
180
273
  region: bucket.region,
181
274
  website: bucket.website,
182
275
  websiteConfiguration: bucket.websiteConfiguration,
276
+ notificationConfiguration: bucket.notificationConfiguration,
183
277
  creationDate: bucket.creationDate,
184
278
  objectCount: bucket.objectCount,
185
279
  totalSize: bucket.totalSize,
@@ -267,6 +361,20 @@ class S3Simulator {
267
361
 
268
362
  logger.verboso(`📤 Upload S3: ${bucketName}/${key} (${body.length} bytes)`);
269
363
  this.audit.record({ eventName: "PutObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key, contentType }, responseElements: { etag } });
364
+
365
+ this._triggerS3Event(bucketName, key, {
366
+ eventName: oldObject ? "ObjectCreated:Put" : "ObjectCreated:Put",
367
+ eventType: "ObjectCreated",
368
+ objectInfo: {
369
+ key,
370
+ size: body.length,
371
+ etag,
372
+ contentType,
373
+ metadata,
374
+ sequencer: crypto.randomBytes(8).toString("hex").toUpperCase(),
375
+ },
376
+ });
377
+
270
378
  return { etag };
271
379
  }
272
380
 
@@ -338,6 +446,19 @@ class S3Simulator {
338
446
  this.persistBucket(bucketName);
339
447
  logger.verboso(`🗑️ Delete S3: ${bucketName}/${key}`);
340
448
  this.audit.record({ eventName: "DeleteObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key } });
449
+
450
+ this._triggerS3Event(bucketName, key, {
451
+ eventName: "ObjectRemoved:Delete",
452
+ eventType: "ObjectRemoved",
453
+ objectInfo: {
454
+ key,
455
+ size: object.size,
456
+ etag: object.etag,
457
+ contentType: object.contentType,
458
+ metadata: object.metadata,
459
+ sequencer: crypto.randomBytes(8).toString("hex").toUpperCase(),
460
+ },
461
+ });
341
462
  }
342
463
  return { success: true };
343
464
  }
@@ -397,6 +518,302 @@ class S3Simulator {
397
518
  }));
398
519
  }
399
520
 
521
+ // ─── Notification Configuration ───────────────────────────────────────────
522
+
523
+ _normalizeNotificationConfig(config) {
524
+ if (!config) return null;
525
+ const normalized = {
526
+ LambdaFunctionConfigurations: [],
527
+ QueueConfigurations: [],
528
+ TopicConfigurations: [],
529
+ };
530
+ const validEvents = [
531
+ "s3:ObjectCreated:*",
532
+ "s3:ObjectCreated:Put",
533
+ "s3:ObjectCreated:Post",
534
+ "s3:ObjectCreated:Copy",
535
+ "s3:ObjectCreated:CompleteMultipartUpload",
536
+ "s3:ObjectRemoved:*",
537
+ "s3:ObjectRemoved:Delete",
538
+ "s3:ObjectRemoved:DeleteMarkerCreated",
539
+ "s3:ObjectRestore:*",
540
+ "s3:ObjectRestore:Post",
541
+ "s3:ObjectRestore:Completed",
542
+ "s3:ReducedRedundancyLostObject",
543
+ "s3:Replication:*",
544
+ "s3:Replication:OperationFailedReplication",
545
+ "s3:Replication:OperationNotTracked",
546
+ "s3:Replication:OperationMissedThreshold",
547
+ "s3:Replication:OperationReplicatedAfterThreshold",
548
+ "s3:LifecycleExpiration:*",
549
+ "s3:LifecycleExpiration:Delete",
550
+ "s3:LifecycleExpiration:DeleteMarkerCreated",
551
+ "s3:LifecycleTransition",
552
+ "s3:IntelligentTiering",
553
+ "s3:ObjectTagging:*",
554
+ "s3:ObjectTagging:Put",
555
+ "s3:ObjectTagging:Delete",
556
+ "s3:ObjectAcl:Put",
557
+ ];
558
+
559
+ if (Array.isArray(config.LambdaFunctionConfigurations)) {
560
+ for (const cfg of config.LambdaFunctionConfigurations) {
561
+ if (!cfg.LambdaFunctionArn) continue;
562
+ normalized.LambdaFunctionConfigurations.push({
563
+ Id: cfg.Id || crypto.randomUUID(),
564
+ LambdaFunctionArn: cfg.LambdaFunctionArn,
565
+ Events: cfg.Events?.filter((e) => validEvents.includes(e)) || ["s3:ObjectCreated:*"],
566
+ Filter: this._normalizeFilter(cfg.Filter),
567
+ });
568
+ }
569
+ }
570
+ if (Array.isArray(config.QueueConfigurations)) {
571
+ for (const cfg of config.QueueConfigurations) {
572
+ if (!cfg.QueueArn) continue;
573
+ normalized.QueueConfigurations.push({
574
+ Id: cfg.Id || crypto.randomUUID(),
575
+ QueueArn: cfg.QueueArn,
576
+ Events: cfg.Events?.filter((e) => validEvents.includes(e)) || ["s3:ObjectCreated:*"],
577
+ Filter: this._normalizeFilter(cfg.Filter),
578
+ });
579
+ }
580
+ }
581
+ if (Array.isArray(config.TopicConfigurations)) {
582
+ for (const cfg of config.TopicConfigurations) {
583
+ if (!cfg.TopicArn) continue;
584
+ normalized.TopicConfigurations.push({
585
+ Id: cfg.Id || crypto.randomUUID(),
586
+ TopicArn: cfg.TopicArn,
587
+ Events: cfg.Events?.filter((e) => validEvents.includes(e)) || ["s3:ObjectCreated:*"],
588
+ Filter: this._normalizeFilter(cfg.Filter),
589
+ });
590
+ }
591
+ }
592
+
593
+ const hasAny =
594
+ normalized.LambdaFunctionConfigurations.length > 0 ||
595
+ normalized.QueueConfigurations.length > 0 ||
596
+ normalized.TopicConfigurations.length > 0;
597
+ return hasAny ? normalized : null;
598
+ }
599
+
600
+ _normalizeFilter(filter) {
601
+ if (!filter || !filter.Key) return null;
602
+ const rules = [];
603
+ if (Array.isArray(filter.Key.FilterRules)) {
604
+ for (const r of filter.Key.FilterRules) {
605
+ if (r.Name === "prefix" || r.Name === "suffix") {
606
+ rules.push({ Name: r.Name, Value: r.Value });
607
+ }
608
+ }
609
+ }
610
+ return rules.length > 0 ? { Key: { FilterRules: rules } } : null;
611
+ }
612
+
613
+ _matchesFilter(filter, key) {
614
+ if (!filter || !filter.Key || !Array.isArray(filter.Key.FilterRules)) return true;
615
+ for (const rule of filter.Key.FilterRules) {
616
+ const val = rule.Value || "";
617
+ if (rule.Name === "prefix" && !key.startsWith(val)) return false;
618
+ if (rule.Name === "suffix" && !key.endsWith(val)) return false;
619
+ }
620
+ return true;
621
+ }
622
+
623
+ _matchesEvent(configuredEvents, eventName, eventType) {
624
+ if (!Array.isArray(configuredEvents) || configuredEvents.length === 0) return true;
625
+ const s3EventName = `s3:${eventName}`;
626
+ const s3EventType = `s3:${eventType}:*`;
627
+ for (const ev of configuredEvents) {
628
+ if (ev === s3EventName) return true;
629
+ if (ev === s3EventType) return true;
630
+ if (ev === "s3:ObjectCreated:*" && eventType === "ObjectCreated") return true;
631
+ if (ev === "s3:ObjectRemoved:*" && eventType === "ObjectRemoved") return true;
632
+ }
633
+ return false;
634
+ }
635
+
636
+ _buildS3EventRecord(bucketName, key, ctx) {
637
+ const bucket = this.buckets.get(bucketName);
638
+ const region = bucket?.region || this.region;
639
+ const info = ctx.objectInfo || {};
640
+ return {
641
+ eventVersion: "2.1",
642
+ eventSource: "aws:s3",
643
+ awsRegion: region,
644
+ eventTime: new Date().toISOString(),
645
+ eventName: ctx.eventName,
646
+ userIdentity: { principalId: "AWSLocalSimulator" },
647
+ requestParameters: { sourceIPAddress: "127.0.0.1" },
648
+ responseElements: {
649
+ "x-amz-request-id": crypto.randomUUID(),
650
+ "x-amz-id-2": crypto.randomBytes(24).toString("base64"),
651
+ },
652
+ s3: {
653
+ s3SchemaVersion: "1.0",
654
+ configurationId: ctx.configId || "bucket-notification-config",
655
+ bucket: {
656
+ name: bucketName,
657
+ ownerIdentity: { principalId: "AIDAJDPLRKLG7UEXAMPLE" },
658
+ arn: `arn:aws:s3:::${bucketName}`,
659
+ },
660
+ object: {
661
+ key: encodeURIComponent(key),
662
+ size: info.size || 0,
663
+ eTag: info.etag ? `"${info.etag}"` : undefined,
664
+ versionId: undefined,
665
+ sequencer: info.sequencer || crypto.randomBytes(8).toString("hex").toUpperCase(),
666
+ },
667
+ },
668
+ };
669
+ }
670
+
671
+ async _triggerS3Event(bucketName, key, ctx) {
672
+ try {
673
+ const bucket = this.buckets.get(bucketName);
674
+ if (!bucket) return;
675
+
676
+ const record = this._buildS3EventRecord(bucketName, key, ctx);
677
+ const s3EventPayload = { Records: [record] };
678
+
679
+ if (bucket.notificationConfiguration) {
680
+ for (const cfg of bucket.notificationConfiguration.LambdaFunctionConfigurations || []) {
681
+ if (!this._matchesEvent(cfg.Events, ctx.eventName, ctx.eventType)) continue;
682
+ if (!this._matchesFilter(cfg.Filter, key)) continue;
683
+ record.s3.configurationId = cfg.Id;
684
+ this._deliverToLambda(cfg.LambdaFunctionArn, { Records: [this._buildS3EventRecord(bucketName, key, { ...ctx, configId: cfg.Id })] });
685
+ }
686
+ for (const cfg of bucket.notificationConfiguration.QueueConfigurations || []) {
687
+ if (!this._matchesEvent(cfg.Events, ctx.eventName, ctx.eventType)) continue;
688
+ if (!this._matchesFilter(cfg.Filter, key)) continue;
689
+ this._deliverToSQS(cfg.QueueArn, { Records: [this._buildS3EventRecord(bucketName, key, { ...ctx, configId: cfg.Id })] });
690
+ }
691
+ for (const cfg of bucket.notificationConfiguration.TopicConfigurations || []) {
692
+ if (!this._matchesEvent(cfg.Events, ctx.eventName, ctx.eventType)) continue;
693
+ if (!this._matchesFilter(cfg.Filter, key)) continue;
694
+ this._deliverToSNS(cfg.TopicArn, { Records: [this._buildS3EventRecord(bucketName, key, { ...ctx, configId: cfg.Id })] });
695
+ }
696
+ }
697
+
698
+ this._deliverToEventBridge(bucketName, ctx, s3EventPayload);
699
+ } catch (err) {
700
+ logger.warn(`S3 Event trigger error (${bucketName}/${key}): ${err.message}`);
701
+ }
702
+ }
703
+
704
+ _deliverToLambda(functionArn, event) {
705
+ if (!this.lambdaService?.simulator) return;
706
+ try {
707
+ const match = functionArn.match(/function:([^:]+)/);
708
+ const functionName = match ? match[1] : functionArn;
709
+ this.lambdaService.simulator.invokeFunction(functionName, event);
710
+ logger.verboso(`📡 S3 → Lambda: ${functionArn}`);
711
+ } catch (err) {
712
+ logger.error(`❌ S3 → Lambda delivery failed: ${err.message}`);
713
+ }
714
+ }
715
+
716
+ _deliverToSQS(queueArn, event) {
717
+ if (!this.sqsService?.simulator) return;
718
+ try {
719
+ const match = queueArn.match(/:([^:]+)$/);
720
+ const queueName = match ? match[1] : queueArn;
721
+ const queue = this.sqsService.simulator.queues?.get(queueName);
722
+ const queueUrl = queue?.url || queueArn;
723
+ this.sqsService.simulator.sendMessage({
724
+ QueueUrl: queueUrl,
725
+ MessageBody: JSON.stringify(event),
726
+ });
727
+ logger.verboso(`📡 S3 → SQS: ${queueArn}`);
728
+ } catch (err) {
729
+ logger.error(`❌ S3 → SQS delivery failed: ${err.message}`);
730
+ }
731
+ }
732
+
733
+ _deliverToSNS(topicArn, event) {
734
+ if (!this.snsService?.simulator) return;
735
+ try {
736
+ this.snsService.simulator.publish({
737
+ TopicArn: topicArn,
738
+ Message: JSON.stringify(event),
739
+ });
740
+ logger.verboso(`📡 S3 → SNS: ${topicArn}`);
741
+ } catch (err) {
742
+ logger.error(`❌ S3 → SNS delivery failed: ${err.message}`);
743
+ }
744
+ }
745
+
746
+ _deliverToEventBridge(bucketName, ctx, s3EventPayload) {
747
+ if (!this.eventbridgeService?.simulator) return;
748
+ try {
749
+ const bucket = this.buckets.get(bucketName);
750
+ const region = bucket?.region || this.region;
751
+ const objectKey = ctx.objectInfo?.key;
752
+ const detailType = ctx.eventType === "ObjectCreated"
753
+ ? "Object Created"
754
+ : ctx.eventType === "ObjectRemoved"
755
+ ? "Object Deleted"
756
+ : ctx.eventName;
757
+ const resources = [
758
+ `arn:aws:s3:::${bucketName}`,
759
+ ];
760
+ if (objectKey) {
761
+ resources.push(`arn:aws:s3:::${bucketName}/${objectKey}`);
762
+ }
763
+ this.eventbridgeService.simulator.putEvents({
764
+ Entries: [{
765
+ Time: new Date().toISOString(),
766
+ Source: "aws.s3",
767
+ Resources: resources,
768
+ DetailType: detailType,
769
+ Detail: JSON.stringify({
770
+ version: "0",
771
+ bucket: { name: bucketName },
772
+ object: {
773
+ key: objectKey,
774
+ size: ctx.objectInfo?.size,
775
+ etag: ctx.objectInfo?.etag,
776
+ "version-id": ctx.objectInfo?.versionId,
777
+ sequencer: ctx.objectInfo?.sequencer,
778
+ },
779
+ "request-id": crypto.randomUUID(),
780
+ requester: this.accountId,
781
+ "source-ip-address": "127.0.0.1",
782
+ reason: ctx.eventName,
783
+ }),
784
+ EventBusName: "default",
785
+ }],
786
+ });
787
+ logger.verboso(`📡 S3 → EventBridge (${detailType})`);
788
+ } catch (err) {
789
+ logger.error(`❌ S3 → EventBridge delivery failed: ${err.message}`);
790
+ }
791
+ }
792
+
793
+ getBucketNotificationConfiguration(bucketName) {
794
+ const bucket = this.buckets.get(bucketName);
795
+ if (!bucket) {
796
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
797
+ }
798
+ const nc = bucket.notificationConfiguration || {
799
+ LambdaFunctionConfigurations: [],
800
+ QueueConfigurations: [],
801
+ TopicConfigurations: [],
802
+ };
803
+ return { notificationConfiguration: nc };
804
+ }
805
+
806
+ putBucketNotificationConfiguration(bucketName, config) {
807
+ const bucket = this.buckets.get(bucketName);
808
+ if (!bucket) {
809
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
810
+ }
811
+ bucket.notificationConfiguration = this._normalizeNotificationConfig(config);
812
+ this.persistBuckets();
813
+ logger.debug(`📢 Notification config atualizada: ${bucketName}`);
814
+ return { success: true };
815
+ }
816
+
400
817
  // ─── Website Config ───────────────────────────────────────────────────────
401
818
 
402
819
  getWebsiteConfig(bucketName) {
@@ -464,6 +881,7 @@ class S3Simulator {
464
881
  region: bucket.region || "us-east-1",
465
882
  website: bucket.website || false,
466
883
  websiteConfiguration: bucket.websiteConfiguration || null,
884
+ notificationConfiguration: bucket.notificationConfiguration || null,
467
885
  objects: objectsObj,
468
886
  objectCount: bucket.objectCount,
469
887
  totalSize: bucket.totalSize,
@@ -563,6 +981,135 @@ class S3Simulator {
563
981
  <HostId>local-simulator</HostId>
564
982
  </Error>`;
565
983
  }
984
+
985
+ // ─── Presigned URLs ──────────────────────────────────────────────────────
986
+
987
+ /**
988
+ * Generate a presigned GET URL for downloading an object
989
+ * @param {string} bucketName - Bucket name
990
+ * @param {string} key - Object key
991
+ * @param {object} options - Options { expiresIn, credentials }
992
+ * @returns {object} { url, presignedUrl, expiration }
993
+ */
994
+ generatePresignedGetUrl(bucketName, key, options = {}) {
995
+ const bucket = this.getBucket(bucketName);
996
+ if (!bucket) {
997
+ return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
998
+ }
999
+
1000
+ try {
1001
+ const result = this.presignedUrlGenerator.generateGetUrl(bucketName, key, options);
1002
+ return { presignedUrl: result };
1003
+ } catch (err) {
1004
+ logger.error('S3: Error generating presigned GET URL:', err);
1005
+ return { error: { code: 'InternalError', message: err.message }, status: 500 };
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * Generate a presigned PUT URL for uploading an object
1011
+ * @param {string} bucketName - Bucket name
1012
+ * @param {string} key - Object key
1013
+ * @param {object} options - Options { expiresIn, credentials, contentType }
1014
+ * @returns {object} { presignedUrl, expiration }
1015
+ */
1016
+ generatePresignedPutUrl(bucketName, key, options = {}) {
1017
+ const bucket = this.getBucket(bucketName);
1018
+ if (!bucket) {
1019
+ return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
1020
+ }
1021
+
1022
+ try {
1023
+ const result = this.presignedUrlGenerator.generatePutUrl(bucketName, key, options);
1024
+ return { presignedUrl: result };
1025
+ } catch (err) {
1026
+ logger.error('S3: Error generating presigned PUT URL:', err);
1027
+ return { error: { code: 'InternalError', message: err.message }, status: 500 };
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * Generate a presigned DELETE URL for deleting an object
1033
+ * @param {string} bucketName - Bucket name
1034
+ * @param {string} key - Object key
1035
+ * @param {object} options - Options { expiresIn, credentials }
1036
+ * @returns {object} { presignedUrl, expiration }
1037
+ */
1038
+ generatePresignedDeleteUrl(bucketName, key, options = {}) {
1039
+ const bucket = this.getBucket(bucketName);
1040
+ if (!bucket) {
1041
+ return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
1042
+ }
1043
+
1044
+ try {
1045
+ const result = this.presignedUrlGenerator.generateDeleteUrl(bucketName, key, options);
1046
+ return { presignedUrl: result };
1047
+ } catch (err) {
1048
+ logger.error('S3: Error generating presigned DELETE URL:', err);
1049
+ return { error: { code: 'InternalError', message: err.message }, status: 500 };
1050
+ }
1051
+ }
1052
+
1053
+ /**
1054
+ * Generate a presigned HEAD URL for checking object metadata
1055
+ * @param {string} bucketName - Bucket name
1056
+ * @param {string} key - Object key
1057
+ * @param {object} options - Options { expiresIn, credentials }
1058
+ * @returns {object} { presignedUrl, expiration }
1059
+ */
1060
+ generatePresignedHeadUrl(bucketName, key, options = {}) {
1061
+ const bucket = this.getBucket(bucketName);
1062
+ if (!bucket) {
1063
+ return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
1064
+ }
1065
+
1066
+ try {
1067
+ const result = this.presignedUrlGenerator.generateHeadUrl(bucketName, key, options);
1068
+ return { presignedUrl: result };
1069
+ } catch (err) {
1070
+ logger.error('S3: Error generating presigned HEAD URL:', err);
1071
+ return { error: { code: 'InternalError', message: err.message }, status: 500 };
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Generate a presigned POST policy for form uploads
1077
+ * @param {string} bucketName - Bucket name
1078
+ * @param {string} key - Object key
1079
+ * @param {object} options - Options { expiresIn, conditions, contentType }
1080
+ * @returns {object} { policy, signature, amzAlgorithm, amzDate, amzCredential }
1081
+ */
1082
+ generatePresignedPostPolicy(bucketName, key, options = {}) {
1083
+ const bucket = this.getBucket(bucketName);
1084
+ if (!bucket) {
1085
+ return { error: { code: 'NoSuchBucket', message: `The specified bucket does not exist: ${bucketName}` }, status: 404 };
1086
+ }
1087
+
1088
+ try {
1089
+ const result = this.presignedUrlGenerator.generatePostPolicy(bucketName, key, options);
1090
+ return result;
1091
+ } catch (err) {
1092
+ logger.error('S3: Error generating presigned POST policy:', err);
1093
+ return { error: { code: 'InternalError', message: err.message }, status: 500 };
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * Validate a presigned URL
1099
+ * @param {string} url - Presigned URL to validate
1100
+ * @param {string} method - HTTP method (GET, PUT, DELETE, HEAD)
1101
+ * @param {object} credentials - Optional credentials for validation
1102
+ * @returns {object} { valid: boolean, message: string, expiresAt: Date }
1103
+ */
1104
+ validatePresignedUrl(url, method = 'GET', credentials = null) {
1105
+ try {
1106
+ const result = this.presignedUrlGenerator.validatePresignedUrl(url, method, credentials);
1107
+ return result;
1108
+ } catch (err) {
1109
+ logger.error('S3: Error validating presigned URL:', err);
1110
+ return { valid: false, message: err.message };
1111
+ }
1112
+ }
566
1113
  }
567
1114
 
568
1115
  module.exports = S3Simulator;
@@ -41,6 +41,37 @@ class SNSService {
41
41
  if (sqs) this.simulator.setSqsService(sqs);
42
42
  const ct = server.getService('cloudtrail');
43
43
  if (ct?.simulator) this.simulator.audit.setTrail(ct.simulator);
44
+
45
+ // IAM enforcement middleware integration
46
+ const iamService = server.getService('iam');
47
+ if (iamService && iamService.getSimulator()) {
48
+ const { createIAMEnforcementMiddleware } = require('../iam/middleware');
49
+ const iamSimulator = iamService.getSimulator();
50
+
51
+ const middleware = createIAMEnforcementMiddleware({
52
+ iamSimulator,
53
+ servicePrefix: 'sns',
54
+ extractAction: (req) => req.body?.Action || req.query?.Action || 'Unknown',
55
+ extractResource: (req) => {
56
+ const topicArn = req.body?.TopicArn || req.query?.TopicArn || '';
57
+ if (topicArn) {
58
+ return topicArn; // Already an ARN
59
+ }
60
+ const topicName = req.body?.Name || req.body?.TopicName || 'unknown';
61
+ return `arn:aws:sns:us-east-1:123456789012:${topicName}`;
62
+ },
63
+ formatError: (res, code, message) => {
64
+ res.status(403).set('Content-Type', 'text/xml').send(
65
+ `<ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/"><Error><Type>Sender</Type><Code>${code}</Code><Message>${message}</Message></Error></ErrorResponse>`
66
+ );
67
+ }
68
+ });
69
+
70
+ // Set middleware on the app so it's applied before routes
71
+ if (this.app) {
72
+ this.app.iamMiddleware = middleware;
73
+ }
74
+ }
44
75
  }
45
76
 
46
77
  async start() {