Skip to content

Commit f4bf20d

Browse files
committed
feat(terraform): add partial evaluation for policy templates
Signed-off-by: nikpivkin <[email protected]>
1 parent ef5f8de commit f4bf20d

File tree

3 files changed

+178
-0
lines changed

3 files changed

+178
-0
lines changed

pkg/iac/adapters/terraform/aws/iam/convert.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package iam
33
import (
44
"strings"
55

6+
"github.com/hashicorp/hcl/v2/hclsyntax"
7+
68
"github.com/aquasecurity/iamgo"
79
"github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam"
810
"github.com/aquasecurity/trivy/pkg/iac/scan"
@@ -15,6 +17,13 @@ type wrappedDocument struct {
1517
}
1618

1719
func ParsePolicyFromAttr(attr *terraform.Attribute, owner *terraform.Block, modules terraform.Modules) (*iam.Document, error) {
20+
attr.RewriteExpr(func(e hclsyntax.Expression) hclsyntax.Expression {
21+
if te, ok := e.(*hclsyntax.TemplateExpr); ok {
22+
return &terraform.PartialTemplateExpr{TemplateExpr: te}
23+
}
24+
return e
25+
})
26+
1827
if !attr.IsString() {
1928
return &iam.Document{
2029
Metadata: owner.GetMetadata(),

pkg/iac/adapters/terraform/aws/iam/policies_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,69 @@ data "aws_iam_policy_document" "policy" {
402402
},
403403
},
404404
},
405+
{
406+
name: "policy is template with unknown part",
407+
terraform: `resource "aws_iam_policy" "test" {
408+
name = "test"
409+
410+
policy = <<EOF
411+
{
412+
"Version": "2012-10-17",
413+
"Statement": [
414+
{
415+
"Effect": "Allow",
416+
"Action": [
417+
"s3:put*",
418+
"s3:get*",
419+
"s3:list*"
420+
],
421+
"Resource": "*"
422+
},
423+
{
424+
"Effect": "Allow",
425+
"Action": [
426+
"kinesis:DescribeStream",
427+
"kinesis:GetRecords"
428+
],
429+
"Resource": [
430+
"${aws_kinesis_stream.stepfunction_ecs_kinesis_stream.arn}"
431+
]
432+
}
433+
]
434+
}
435+
EOF
436+
}`,
437+
expected: []iam.Policy{
438+
{
439+
Name: iacTypes.StringTest("test"),
440+
Document: func() iam.Document {
441+
builder := iamgo.NewPolicyBuilder().
442+
WithStatement(
443+
iamgo.NewStatementBuilder().
444+
WithActions([]string{"s3:put*", "s3:get*", "s3:list*"}).
445+
WithResources([]string{"*"}).
446+
WithEffect("Allow").
447+
Build(),
448+
).
449+
WithStatement(
450+
iamgo.NewStatementBuilder().
451+
WithActions([]string{"kinesis:DescribeStream", "kinesis:GetRecords"}).
452+
WithResources([]string{"__UNRESOLVED__"}).
453+
WithEffect("Allow").
454+
Build(),
455+
).
456+
WithVersion("2012-10-17")
457+
458+
return iam.Document{
459+
Parsed: builder.Build(),
460+
Metadata: iacTypes.NewTestMetadata(),
461+
IsOffset: false,
462+
HasRefs: true,
463+
}
464+
}(),
465+
},
466+
},
467+
},
405468
}
406469

407470
for _, test := range tests {

pkg/iac/terraform/attribute.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/aquasecurity/trivy/pkg/iac/terraform/context"
1818
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
19+
"github.com/aquasecurity/trivy/pkg/log"
1920
)
2021

2122
type Attribute struct {
@@ -828,3 +829,108 @@ func safeOp[T any](a *Attribute, fn func(cty.Value) T) T {
828829

829830
return fn(val)
830831
}
832+
833+
// RewriteExpr applies the given function `transform` to the expression of the attribute,
834+
// recursively traversing and transforming it.
835+
func (a *Attribute) RewriteExpr(transform func(hclsyntax.Expression) hclsyntax.Expression) {
836+
a.hclAttribute.Expr = RewriteExpr(a.hclAttribute.Expr.(hclsyntax.Expression), transform)
837+
}
838+
839+
// nolint: gocyclo
840+
// RewriteExpr recursively rewrites an HCL expression tree in-place,
841+
// applying the provided transformation function `transform` to each node.
842+
func RewriteExpr(
843+
expr hclsyntax.Expression,
844+
transform func(hclsyntax.Expression) hclsyntax.Expression,
845+
) hclsyntax.Expression {
846+
if expr == nil {
847+
return nil
848+
}
849+
switch e := expr.(type) {
850+
case *hclsyntax.LiteralValueExpr:
851+
case *hclsyntax.TemplateExpr:
852+
for i, p := range e.Parts {
853+
e.Parts[i] = RewriteExpr(p, transform)
854+
}
855+
case *hclsyntax.TemplateWrapExpr:
856+
e.Wrapped = RewriteExpr(e.Wrapped, transform)
857+
case *hclsyntax.BinaryOpExpr:
858+
e.LHS = RewriteExpr(e.LHS, transform)
859+
e.RHS = RewriteExpr(e.RHS, transform)
860+
case *hclsyntax.UnaryOpExpr:
861+
e.Val = RewriteExpr(e.Val, transform)
862+
case *hclsyntax.TupleConsExpr:
863+
for i, elem := range e.Exprs {
864+
e.Exprs[i] = RewriteExpr(elem, transform)
865+
}
866+
case *hclsyntax.ParenthesesExpr:
867+
e.Expression = RewriteExpr(e.Expression, transform)
868+
case *hclsyntax.ObjectConsExpr:
869+
for i, item := range e.Items {
870+
e.Items[i].KeyExpr = RewriteExpr(item.KeyExpr, transform)
871+
e.Items[i].ValueExpr = RewriteExpr(item.ValueExpr, transform)
872+
}
873+
case *hclsyntax.ObjectConsKeyExpr:
874+
e.Wrapped = RewriteExpr(e.Wrapped, transform)
875+
case *hclsyntax.ScopeTraversalExpr:
876+
case *hclsyntax.RelativeTraversalExpr:
877+
e.Source = RewriteExpr(e.Source, transform)
878+
case *hclsyntax.ConditionalExpr:
879+
e.Condition = RewriteExpr(e.Condition, transform)
880+
e.TrueResult = RewriteExpr(e.TrueResult, transform)
881+
e.FalseResult = RewriteExpr(e.FalseResult, transform)
882+
case *hclsyntax.FunctionCallExpr:
883+
for i, arg := range e.Args {
884+
e.Args[i] = RewriteExpr(arg, transform)
885+
}
886+
case *hclsyntax.IndexExpr:
887+
e.Collection = RewriteExpr(e.Collection, transform)
888+
e.Key = RewriteExpr(e.Key, transform)
889+
case *hclsyntax.ForExpr:
890+
e.CollExpr = RewriteExpr(e.CollExpr, transform)
891+
e.KeyExpr = RewriteExpr(e.KeyExpr, transform)
892+
e.ValExpr = RewriteExpr(e.ValExpr, transform)
893+
e.CondExpr = RewriteExpr(e.CondExpr, transform)
894+
case *hclsyntax.SplatExpr:
895+
e.Source = RewriteExpr(e.Source, transform)
896+
case *hclsyntax.AnonSymbolExpr:
897+
default:
898+
log.Debug(
899+
"RewriteExpr encountered an unhandled expression type",
900+
log.Prefix(log.PrefixMisconfiguration),
901+
log.String("expr_type", fmt.Sprintf("%T", expr)),
902+
)
903+
}
904+
return transform(expr)
905+
}
906+
907+
// UnknownValuePrefix is a placeholder string used to represent parts of a
908+
// template expression that cannot be fully evaluated due to unknown values.
909+
const UnknownValuePrefix = "__UNRESOLVED__"
910+
911+
// PartialTemplateExpr is a wrapper around hclsyntax.TemplateExpr that
912+
// replaces unknown or unevaluated parts with placeholder strings during evaluation.
913+
type PartialTemplateExpr struct {
914+
*hclsyntax.TemplateExpr
915+
}
916+
917+
func (e *PartialTemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
918+
parts := make([]hclsyntax.Expression, len(e.Parts))
919+
for i, part := range e.Parts {
920+
partVal, diags := part.Value(ctx)
921+
if diags.HasErrors() || !partVal.IsKnown() {
922+
parts[i] = &hclsyntax.LiteralValueExpr{
923+
Val: cty.StringVal(UnknownValuePrefix),
924+
SrcRange: part.Range(),
925+
}
926+
} else {
927+
parts[i] = part
928+
}
929+
}
930+
newTemplate := &hclsyntax.TemplateExpr{
931+
Parts: parts,
932+
SrcRange: e.SrcRange,
933+
}
934+
935+
return newTemplate.Value(ctx)
936+
}

0 commit comments

Comments
 (0)