mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 07:10:41 -08:00
fix(terraform): do not re-expand dynamic blocks (#6151)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/aquasecurity/trivy/internal/testutil"
|
||||
"github.com/aquasecurity/trivy/pkg/iac/scanners/options"
|
||||
"github.com/aquasecurity/trivy/pkg/iac/terraform"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@@ -904,6 +905,91 @@ data "http" "example" {
|
||||
}
|
||||
|
||||
func TestForEach(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
expectedBucketName string
|
||||
expectedNameLabel string
|
||||
}{
|
||||
{
|
||||
name: "arg is set and ref to each.key",
|
||||
src: `locals {
|
||||
buckets = ["bucket1"]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = toset(local.buckets)
|
||||
bucket = each.key
|
||||
}`,
|
||||
expectedBucketName: "bucket1",
|
||||
expectedNameLabel: `this["bucket1"]`,
|
||||
},
|
||||
{
|
||||
name: "arg is set and ref to each.value",
|
||||
src: `locals {
|
||||
buckets = ["bucket1"]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = toset(local.buckets)
|
||||
bucket = each.value
|
||||
}`,
|
||||
expectedBucketName: "bucket1",
|
||||
expectedNameLabel: `this["bucket1"]`,
|
||||
},
|
||||
{
|
||||
name: "arg is map and ref to each.key",
|
||||
src: `locals {
|
||||
buckets = {
|
||||
bucket1key = "bucket1value"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = local.buckets
|
||||
bucket = each.key
|
||||
}`,
|
||||
expectedBucketName: "bucket1key",
|
||||
expectedNameLabel: `this["bucket1key"]`,
|
||||
},
|
||||
{
|
||||
name: "arg is map and ref to each.value",
|
||||
src: `locals {
|
||||
buckets = {
|
||||
bucket1key = "bucket1value"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = local.buckets
|
||||
bucket = each.value
|
||||
}`,
|
||||
expectedBucketName: "bucket1value",
|
||||
expectedNameLabel: `this["bucket1key"]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": tt.src,
|
||||
})
|
||||
require.Len(t, modules, 1)
|
||||
|
||||
buckets := modules.GetResourcesByType("aws_s3_bucket")
|
||||
assert.Len(t, buckets, 1)
|
||||
|
||||
bucket := buckets[0]
|
||||
bucketName := bucket.GetAttribute("bucket").Value().AsString()
|
||||
assert.Equal(t, tt.expectedBucketName, bucketName)
|
||||
|
||||
assert.Equal(t, tt.expectedNameLabel, bucket.NameLabel())
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestForEachCountExpanded(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -919,6 +1005,18 @@ func TestForEach(t *testing.T) {
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = local.buckets
|
||||
bucket = each.key
|
||||
}`,
|
||||
expectedCount: 2,
|
||||
},
|
||||
{
|
||||
name: "arg is empty list",
|
||||
source: `locals {
|
||||
buckets = []
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = local.buckets
|
||||
bucket = each.value
|
||||
}`,
|
||||
expectedCount: 0,
|
||||
},
|
||||
@@ -929,8 +1027,34 @@ resource "aws_s3_bucket" "this" {
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = loca.buckets
|
||||
for_each = local.buckets
|
||||
bucket = each.key
|
||||
}`,
|
||||
expectedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "argument set with the same values",
|
||||
source: `locals {
|
||||
buckets = ["true", "true"]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = toset(local.buckets)
|
||||
bucket = each.key
|
||||
}`,
|
||||
expectedCount: 1,
|
||||
},
|
||||
{
|
||||
name: "arg is non-valid set",
|
||||
source: `locals {
|
||||
buckets = [{
|
||||
bucket1key = "bucket1value"
|
||||
}]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = toset(local.buckets)
|
||||
bucket = each.value
|
||||
}`,
|
||||
expectedCount: 0,
|
||||
},
|
||||
@@ -961,18 +1085,25 @@ resource "aws_s3_bucket" "this" {
|
||||
}`,
|
||||
expectedCount: 2,
|
||||
},
|
||||
{
|
||||
name: "arg is empty map",
|
||||
source: `locals {
|
||||
buckets = {}
|
||||
}
|
||||
resource "aws_s3_bucket" "this" {
|
||||
for_each = local.buckets
|
||||
bucket = each.value
|
||||
}
|
||||
`,
|
||||
expectedCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fs := testutil.CreateFS(t, map[string]string{
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": tt.source,
|
||||
})
|
||||
parser := New(fs, "", OptionStopOnHCLError(true))
|
||||
require.NoError(t, parser.ParseFS(context.TODO(), "."))
|
||||
|
||||
modules, _, err := parser.EvaluateAll(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, modules, 1)
|
||||
|
||||
bucketBlocks := modules.GetResourcesByType("aws_s3_bucket")
|
||||
@@ -1139,3 +1270,165 @@ func TestForEachWithObjectsOfDifferentTypes(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, modules, 1)
|
||||
}
|
||||
|
||||
func TestDynamicBlocks(t *testing.T) {
|
||||
t.Run("arg is list of int", func(t *testing.T) {
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "aws_security_group" "sg-webserver" {
|
||||
vpc_id = "1111"
|
||||
dynamic "ingress" {
|
||||
for_each = [80, 443]
|
||||
content {
|
||||
from_port = ingress.value
|
||||
to_port = ingress.value
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
require.Len(t, modules, 1)
|
||||
|
||||
secGroups := modules.GetResourcesByType("aws_security_group")
|
||||
assert.Len(t, secGroups, 1)
|
||||
ingressBlocks := secGroups[0].GetBlocks("ingress")
|
||||
assert.Len(t, ingressBlocks, 2)
|
||||
|
||||
var inboundPorts []int
|
||||
for _, ingress := range ingressBlocks {
|
||||
fromPort := ingress.GetAttribute("from_port").AsIntValueOrDefault(-1, ingress).Value()
|
||||
inboundPorts = append(inboundPorts, fromPort)
|
||||
}
|
||||
|
||||
assert.True(t, compareSets([]int{80, 443}, inboundPorts))
|
||||
})
|
||||
|
||||
t.Run("empty for-each", func(t *testing.T) {
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "aws_lambda_function" "analyzer" {
|
||||
dynamic "vpc_config" {
|
||||
for_each = []
|
||||
content {}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
require.Len(t, modules, 1)
|
||||
|
||||
functions := modules.GetResourcesByType("aws_lambda_function")
|
||||
assert.Len(t, functions, 1)
|
||||
vpcConfigs := functions[0].GetBlocks("vpc_config")
|
||||
assert.Empty(t, vpcConfigs)
|
||||
})
|
||||
|
||||
t.Run("arg is list of bool", func(t *testing.T) {
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "aws_lambda_function" "analyzer" {
|
||||
dynamic "vpc_config" {
|
||||
for_each = [true]
|
||||
content {}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
require.Len(t, modules, 1)
|
||||
|
||||
functions := modules.GetResourcesByType("aws_lambda_function")
|
||||
assert.Len(t, functions, 1)
|
||||
vpcConfigs := functions[0].GetBlocks("vpc_config")
|
||||
assert.Len(t, vpcConfigs, 1)
|
||||
})
|
||||
|
||||
t.Run("arg is list of objects", func(t *testing.T) {
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": `locals {
|
||||
cluster_network_policy = [{
|
||||
enabled = true
|
||||
}]
|
||||
}
|
||||
|
||||
resource "google_container_cluster" "primary" {
|
||||
name = "test"
|
||||
|
||||
dynamic "network_policy" {
|
||||
for_each = local.cluster_network_policy
|
||||
|
||||
content {
|
||||
enabled = network_policy.value.enabled
|
||||
}
|
||||
}
|
||||
}`,
|
||||
})
|
||||
require.Len(t, modules, 1)
|
||||
|
||||
clusters := modules.GetResourcesByType("google_container_cluster")
|
||||
assert.Len(t, clusters, 1)
|
||||
|
||||
networkPolicies := clusters[0].GetBlocks("network_policy")
|
||||
assert.Len(t, networkPolicies, 1)
|
||||
|
||||
enabled := networkPolicies[0].GetAttribute("enabled")
|
||||
assert.True(t, enabled.Value().True())
|
||||
})
|
||||
|
||||
t.Run("nested dynamic", func(t *testing.T) {
|
||||
modules := parse(t, map[string]string{
|
||||
"main.tf": `
|
||||
resource "test_block" "this" {
|
||||
name = "name"
|
||||
location = "loc"
|
||||
dynamic "env" {
|
||||
for_each = ["1", "2"]
|
||||
content {
|
||||
dynamic "value_source" {
|
||||
for_each = [true, true]
|
||||
content {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
})
|
||||
require.Len(t, modules, 1)
|
||||
|
||||
testResources := modules.GetResourcesByType("test_block")
|
||||
assert.Len(t, testResources, 1)
|
||||
envs := testResources[0].GetBlocks("env")
|
||||
assert.Len(t, envs, 2)
|
||||
|
||||
var sources []*terraform.Block
|
||||
for _, env := range envs {
|
||||
sources = append(sources, env.GetBlocks("value_source")...)
|
||||
}
|
||||
assert.Len(t, sources, 4)
|
||||
})
|
||||
}
|
||||
|
||||
func parse(t *testing.T, files map[string]string) terraform.Modules {
|
||||
fs := testutil.CreateFS(t, files)
|
||||
parser := New(fs, "", OptionStopOnHCLError(true))
|
||||
require.NoError(t, parser.ParseFS(context.TODO(), "."))
|
||||
|
||||
modules, _, err := parser.EvaluateAll(context.TODO())
|
||||
require.NoError(t, err)
|
||||
|
||||
return modules
|
||||
}
|
||||
|
||||
func compareSets(a []int, b []int) bool {
|
||||
m := make(map[int]bool)
|
||||
for _, el := range a {
|
||||
m[el] = true
|
||||
}
|
||||
|
||||
for _, el := range b {
|
||||
if !m[el] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user