@@ -2,6 +2,7 @@ package iam
22
33import (
44 "sort"
5+ "strings"
56 "testing"
67
78 "github.com/aquasecurity/iamgo"
@@ -377,3 +378,171 @@ data "aws_partition" "current" {}
377378 })
378379 }
379380}
381+
382+ // validateLambdaEcsKeys validates that attachment references contain both lambda and ecs-tasks keys
383+ func validateLambdaEcsKeys (t * testing.T , attachmentRefs []string ) {
384+ hasLambda := false
385+ hasEcs := false
386+ for _ , ref := range attachmentRefs {
387+ if strings .Contains (ref , "lambda" ) {
388+ hasLambda = true
389+ }
390+ if strings .Contains (ref , "ecs-tasks" ) {
391+ hasEcs = true
392+ }
393+ }
394+ if ! hasLambda || ! hasEcs {
395+ t .Errorf ("expected attachment refs to include both lambda and ecs-tasks keys, got %v" , attachmentRefs )
396+ }
397+ }
398+
399+ func Test_forEachReferences (t * testing.T ) {
400+ tests := []struct {
401+ name string
402+ terraform string
403+ expectedCount int
404+ }{
405+ {
406+ name : "computed local with for_each map" ,
407+ terraform : `
408+ locals {
409+ platform_role_principals = {
410+ lambda = "lambda.amazonaws.com"
411+ ecs-tasks = "ecs-tasks.amazonaws.com"
412+ }
413+ }
414+
415+ data "aws_iam_policy_document" "platform_role_assume_policy" {
416+ for_each = local.platform_role_principals
417+
418+ statement {
419+ actions = ["sts:AssumeRole"]
420+ principals {
421+ type = "Service"
422+ identifiers = [each.value]
423+ }
424+ }
425+ }
426+
427+ resource "aws_iam_role" "platform_role" {
428+ for_each = local.platform_role_principals
429+ name = "platform-${each.key}"
430+ assume_role_policy = data.aws_iam_policy_document.platform_role_assume_policy[each.key].json
431+ }
432+
433+ locals {
434+ platform_roles = {
435+ for role_key, role_res in aws_iam_role.platform_role :
436+ role_key => {
437+ role = role_res.name
438+ }
439+ }
440+ }
441+
442+ data "aws_iam_policy_document" "administrative_policy_doc" {
443+ statement {
444+ resources = ["*"]
445+ actions = ["Tag:GetResources", "Tag:TagResources", "Tag:UntagResources"]
446+ }
447+ }
448+
449+ resource "aws_iam_policy" "administrative_policy" {
450+ name = "administrative-policy"
451+ policy = data.aws_iam_policy_document.administrative_policy_doc.json
452+ }
453+
454+ resource "aws_iam_role_policy_attachment" "administrative_policy_attachment" {
455+ for_each = local.platform_roles
456+ role = each.value.role
457+ policy_arn = aws_iam_policy.administrative_policy.arn
458+ }` ,
459+ expectedCount : 2 ,
460+ },
461+ {
462+ name : "direct for_each reference" ,
463+ terraform : `
464+ locals {
465+ roles = {
466+ lambda = "lambda.amazonaws.com"
467+ ecs-tasks = "ecs-tasks.amazonaws.com"
468+ }
469+ }
470+
471+ resource "aws_iam_role" "platform_role" {
472+ for_each = local.roles
473+ name = "platform-${each.key}"
474+ }
475+
476+ resource "aws_iam_role_policy_attachment" "test" {
477+ for_each = aws_iam_role.platform_role
478+ role = each.value.name
479+ policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
480+ }` ,
481+ expectedCount : 2 ,
482+ },
483+ {
484+ name : "for_each with computed local reference" ,
485+ terraform : `
486+ locals {
487+ role_principals = {
488+ lambda = "lambda.amazonaws.com"
489+ ecs-tasks = "ecs-tasks.amazonaws.com"
490+ }
491+ }
492+
493+ resource "aws_iam_role" "platform_role" {
494+ for_each = local.role_principals
495+ name = "platform-${each.key}"
496+ }
497+
498+ locals {
499+ platform_roles = {
500+ for role_key, role_res in aws_iam_role.platform_role :
501+ role_key => {
502+ role = role_res.name
503+ }
504+ }
505+ }
506+
507+ resource "aws_iam_role_policy_attachment" "test" {
508+ for_each = local.platform_roles
509+ role = each.value.role
510+ policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
511+ }` ,
512+ expectedCount : 2 ,
513+ },
514+ }
515+
516+ for _ , test := range tests {
517+ t .Run (test .name , func (t * testing.T ) {
518+ modules := tftestutil .CreateModulesFromSource (t , test .terraform , ".tf" )
519+ attachments := modules .GetResourcesByType ("aws_iam_role_policy_attachment" )
520+
521+ // Debug output for troubleshooting
522+ t .Logf ("Total resources found: %d" , len (attachments ))
523+ for i , attachment := range attachments {
524+ t .Logf ("Attachment %d: %s" , i , attachment .Reference ().String ())
525+ t .Logf (" - FullName: %s" , attachment .FullName ())
526+ t .Logf (" - TypeLabel: %s" , attachment .TypeLabel ())
527+ t .Logf (" - NameLabel: %s" , attachment .NameLabel ())
528+ if key := attachment .Reference ().RawKey (); ! key .IsNull () && key .IsKnown () {
529+ t .Logf (" - Key: %s (%s)" , key .GoString (), key .Type ().GoString ())
530+ }
531+ }
532+
533+ var attachmentRefs []string
534+ for _ , a := range attachments {
535+ attachmentRefs = append (attachmentRefs , a .Reference ().String ())
536+ }
537+
538+ sort .Strings (attachmentRefs )
539+
540+ if len (attachments ) != test .expectedCount {
541+ t .Fatalf ("expected %d policy attachments, got %d: %v" , test .expectedCount , len (attachments ), attachmentRefs )
542+ }
543+
544+ // Validate that both lambda and ecs-tasks keys are present
545+ validateLambdaEcsKeys (t , attachmentRefs )
546+ })
547+ }
548+ }
0 commit comments