Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/iac/adapters/terraform/aws/iam/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package iam
import (
"strings"

"github.com/hashicorp/hcl/v2/hclsyntax"

"github.com/aquasecurity/iamgo"
"github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam"
"github.com/aquasecurity/trivy/pkg/iac/scan"
Expand All @@ -15,6 +17,13 @@ type wrappedDocument struct {
}

func ParsePolicyFromAttr(attr *terraform.Attribute, owner *terraform.Block, modules terraform.Modules) (*iam.Document, error) {
attr.RewriteExpr(func(e hclsyntax.Expression) hclsyntax.Expression {
if te, ok := e.(*hclsyntax.TemplateExpr); ok {
return &terraform.PartialTemplateExpr{TemplateExpr: te}
}
return e
})
Comment on lines +20 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we be re-writing each expression on every call? Is there any way to somehow selectively do this only for the cases where we could potentially encounter an unknown value? My only concern asking this is about performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewriting will only occur for expressions in policy blocks. Is structure wrapping an expensive operation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small benchmark that includes template expression parsing:

BenchmarkEvalExpr/baseline-8         	   18002	     66560 ns/op	   24299 B/op	     267 allocs/op
BenchmarkEvalExpr/rewrite-8          	   17904	     66774 ns/op	   25963 B/op	     273 allocs/op


if !attr.IsString() {
return &iam.Document{
Metadata: owner.GetMetadata(),
Expand Down
67 changes: 67 additions & 0 deletions pkg/iac/adapters/terraform/aws/iam/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,73 @@ data "aws_iam_policy_document" "policy" {
},
},
},
{
name: "policy is template with unknown part",
terraform: `variable "action" {
default = null
}

resource "aws_iam_policy" "test" {
name = "test"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"${var.action}",
"s3:get*",
"s3:list*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"kinesis:DescribeStream",
"kinesis:GetRecords"
],
"Resource": [
"${aws_kinesis_stream.stepfunction_ecs_kinesis_stream.arn}"
]
}
]
}
EOF
}`,
expected: []iam.Policy{
{
Name: iacTypes.StringTest("test"),
Document: func() iam.Document {
builder := iamgo.NewPolicyBuilder().
WithStatement(
iamgo.NewStatementBuilder().
WithActions([]string{"__UNRESOLVED__", "s3:get*", "s3:list*"}).
WithResources([]string{"*"}).
WithEffect("Allow").
Build(),
).
WithStatement(
iamgo.NewStatementBuilder().
WithActions([]string{"kinesis:DescribeStream", "kinesis:GetRecords"}).
WithResources([]string{"__UNRESOLVED__"}).
WithEffect("Allow").
Build(),
).
WithVersion("2012-10-17")

return iam.Document{
Parsed: builder.Build(),
Metadata: iacTypes.NewTestMetadata(),
IsOffset: false,
HasRefs: true,
}
}(),
},
},
},
}

for _, test := range tests {
Expand Down
112 changes: 112 additions & 0 deletions pkg/iac/terraform/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/gocty"

"github.com/aquasecurity/trivy/pkg/iac/terraform/context"
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
"github.com/aquasecurity/trivy/pkg/log"
)

type Attribute struct {
Expand Down Expand Up @@ -828,3 +830,113 @@ func safeOp[T any](a *Attribute, fn func(cty.Value) T) T {

return fn(val)
}

// RewriteExpr applies the given function `transform` to the expression of the attribute,
// recursively traversing and transforming it.
func (a *Attribute) RewriteExpr(transform func(hclsyntax.Expression) hclsyntax.Expression) {
a.hclAttribute.Expr = RewriteExpr(a.hclAttribute.Expr.(hclsyntax.Expression), transform)
}

// nolint: gocyclo
// RewriteExpr recursively rewrites an HCL expression tree in-place,
// applying the provided transformation function `transform` to each node.
func RewriteExpr(
expr hclsyntax.Expression,
transform func(hclsyntax.Expression) hclsyntax.Expression,
) hclsyntax.Expression {
if expr == nil {
return nil
}
switch e := expr.(type) {
case *hclsyntax.LiteralValueExpr:
case *hclsyntax.TemplateExpr:
for i, p := range e.Parts {
e.Parts[i] = RewriteExpr(p, transform)
}
case *hclsyntax.TemplateWrapExpr:
e.Wrapped = RewriteExpr(e.Wrapped, transform)
case *hclsyntax.BinaryOpExpr:
e.LHS = RewriteExpr(e.LHS, transform)
e.RHS = RewriteExpr(e.RHS, transform)
case *hclsyntax.UnaryOpExpr:
e.Val = RewriteExpr(e.Val, transform)
case *hclsyntax.TupleConsExpr:
for i, elem := range e.Exprs {
e.Exprs[i] = RewriteExpr(elem, transform)
}
case *hclsyntax.ParenthesesExpr:
e.Expression = RewriteExpr(e.Expression, transform)
case *hclsyntax.ObjectConsExpr:
for i, item := range e.Items {
e.Items[i].KeyExpr = RewriteExpr(item.KeyExpr, transform)
e.Items[i].ValueExpr = RewriteExpr(item.ValueExpr, transform)
}
case *hclsyntax.ObjectConsKeyExpr:
e.Wrapped = RewriteExpr(e.Wrapped, transform)
case *hclsyntax.ScopeTraversalExpr:
case *hclsyntax.RelativeTraversalExpr:
e.Source = RewriteExpr(e.Source, transform)
case *hclsyntax.ConditionalExpr:
e.Condition = RewriteExpr(e.Condition, transform)
e.TrueResult = RewriteExpr(e.TrueResult, transform)
e.FalseResult = RewriteExpr(e.FalseResult, transform)
case *hclsyntax.FunctionCallExpr:
for i, arg := range e.Args {
e.Args[i] = RewriteExpr(arg, transform)
}
case *hclsyntax.IndexExpr:
e.Collection = RewriteExpr(e.Collection, transform)
e.Key = RewriteExpr(e.Key, transform)
case *hclsyntax.ForExpr:
e.CollExpr = RewriteExpr(e.CollExpr, transform)
e.KeyExpr = RewriteExpr(e.KeyExpr, transform)
e.ValExpr = RewriteExpr(e.ValExpr, transform)
e.CondExpr = RewriteExpr(e.CondExpr, transform)
case *hclsyntax.SplatExpr:
e.Source = RewriteExpr(e.Source, transform)
case *hclsyntax.AnonSymbolExpr:
default:
log.Debug(
"RewriteExpr encountered an unhandled expression type",
log.Prefix(log.PrefixMisconfiguration),
log.String("expr_type", fmt.Sprintf("%T", expr)),
)
}
return transform(expr)
}

// UnknownValuePrefix is a placeholder string used to represent parts of a
// template expression that cannot be fully evaluated due to unknown values.
const UnknownValuePrefix = "__UNRESOLVED__"

// PartialTemplateExpr is a wrapper around hclsyntax.TemplateExpr that
// replaces unknown or unevaluated parts with placeholder strings during evaluation.
type PartialTemplateExpr struct {
*hclsyntax.TemplateExpr
}

func (e *PartialTemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
parts := make([]hclsyntax.Expression, len(e.Parts))
for i, part := range e.Parts {
partVal, diags := part.Value(ctx)
if diags.HasErrors() || partVal.IsNull() || !partVal.IsKnown() {
parts[i] = &hclsyntax.LiteralValueExpr{
Val: cty.StringVal(UnknownValuePrefix),
SrcRange: part.Range(),
}
} else if _, err := convert.Convert(partVal, cty.String); err != nil {
parts[i] = &hclsyntax.LiteralValueExpr{
Val: cty.StringVal(UnknownValuePrefix),
SrcRange: part.Range(),
}
} else {
parts[i] = part
}
}
newTemplate := &hclsyntax.TemplateExpr{
Parts: parts,
SrcRange: e.SrcRange,
}

return newTemplate.Value(ctx)
}