Skip to content

Commit 44e4686

Browse files
authored
feat(misconf): support for ignore by nested attributes (#7205)
Signed-off-by: nikpivkin <[email protected]>
1 parent 0799770 commit 44e4686

File tree

4 files changed

+287
-15
lines changed

4 files changed

+287
-15
lines changed

docs/docs/scanner/misconfiguration/index.md

+2-5
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ If you want to ignore multiple resources on different attributes, you can specif
534534
#trivy:ignore:aws-ec2-no-public-ingress-sgr[from_port=5432]
535535
```
536536
537-
You can also ignore a resource on multiple attributes:
537+
You can also ignore a resource on multiple attributes in the same rule:
538538
```tf
539539
locals {
540540
rules = {
@@ -563,10 +563,7 @@ resource "aws_security_group_rule" "example" {
563563
}
564564
```
565565
566-
Checks can also be ignored by nested attributes, but certain restrictions apply:
567-
568-
- You cannot access an individual block using indexes, for example when working with dynamic blocks.
569-
- Special variables like [each](https://developer.hashicorp.com/terraform/language/meta-arguments/for_each#the-each-object) and [count](https://developer.hashicorp.com/terraform/language/meta-arguments/count#the-count-object) cannot be accessed.
566+
Checks can also be ignored by nested attributes:
570567
571568
```tf
572569
#trivy:ignore:*[logging_config.prefix=myprefix]

pkg/iac/scanners/terraform/executor/executor.go

+7-10
Original file line numberDiff line numberDiff line change
@@ -135,26 +135,23 @@ func ignoreByParams(params map[string]string, modules terraform.Modules, m *type
135135
if block == nil {
136136
return true
137137
}
138-
for key, val := range params {
139-
attr, _ := block.GetNestedAttribute(key)
140-
if attr.IsNil() || !attr.Value().IsKnown() {
141-
return false
142-
}
143-
switch attr.Type() {
138+
for key, param := range params {
139+
val := block.GetValueByPath(key)
140+
switch val.Type() {
144141
case cty.String:
145-
if !attr.Equals(val) {
142+
if val.AsString() != param {
146143
return false
147144
}
148145
case cty.Number:
149-
bf := attr.Value().AsBigFloat()
146+
bf := val.AsBigFloat()
150147
f64, _ := bf.Float64()
151148
comparableInt := fmt.Sprintf("%d", int(f64))
152149
comparableFloat := fmt.Sprintf("%f", f64)
153-
if val != comparableInt && val != comparableFloat {
150+
if param != comparableInt && param != comparableFloat {
154151
return false
155152
}
156153
case cty.Bool:
157-
if fmt.Sprintf("%t", attr.IsTrue()) != val {
154+
if fmt.Sprintf("%t", val.True()) != param {
158155
return false
159156
}
160157
default:

pkg/iac/scanners/terraform/ignore_test.go

+163
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,21 @@ resource "bad" "my-rule" {
442442
}
443443
}
444444
}
445+
`,
446+
assertLength: 0,
447+
},
448+
{
449+
name: "ignore by indexed dynamic block value",
450+
inputOptions: `
451+
// trivy:ignore:*[secure_settings.0.enabled=false]
452+
resource "bad" "my-rule" {
453+
dynamic "secure_settings" {
454+
for_each = ["false", "true"]
455+
content {
456+
enabled = secure_settings.value
457+
}
458+
}
459+
}
445460
`,
446461
assertLength: 0,
447462
},
@@ -604,6 +619,154 @@ data "aws_iam_policy_document" "test_policy" {
604619
resources = ["*"] # trivy:ignore:aws-iam-enforce-mfa
605620
}
606621
}
622+
`,
623+
assertLength: 0,
624+
},
625+
{
626+
name: "ignore by each.value",
627+
inputOptions: `
628+
// trivy:ignore:*[each.value=false]
629+
resource "bad" "my-rule" {
630+
for_each = toset(["false", "true", "false"])
631+
secure = each.value
632+
}
633+
`,
634+
assertLength: 0,
635+
},
636+
{
637+
name: "ignore by nested each.value",
638+
inputOptions: `
639+
locals {
640+
vms = [
641+
{
642+
ip_address = "10.0.0.1"
643+
name = "vm-1"
644+
},
645+
{
646+
ip_address = "10.0.0.2"
647+
name = "vm-2"
648+
}
649+
]
650+
}
651+
// trivy:ignore:*[each.value.name=vm-2]
652+
resource "bad" "my-rule" {
653+
secure = false
654+
for_each = { for vm in local.vms : vm.name => vm }
655+
ip_address = each.value.ip_address
656+
}
657+
`,
658+
assertLength: 1,
659+
},
660+
{
661+
name: "ignore resource with `count` meta-argument",
662+
inputOptions: `
663+
// trivy:ignore:*[count.index=1]
664+
resource "bad" "my-rule" {
665+
count = 2
666+
secure = false
667+
}
668+
`,
669+
assertLength: 1,
670+
},
671+
{
672+
name: "invalid index when accessing blocks",
673+
inputOptions: `
674+
// trivy:ignore:*[ingress.99.port=9090]
675+
// trivy:ignore:*[ingress.-10.port=9090]
676+
resource "bad" "my-rule" {
677+
secure = false
678+
dynamic "ingress" {
679+
for_each = [8080, 9090]
680+
content {
681+
port = ingress.value
682+
}
683+
}
684+
}
685+
`,
686+
assertLength: 1,
687+
},
688+
{
689+
name: "ignore by list value",
690+
inputOptions: `
691+
#trivy:ignore:*[someattr.1.Environment=dev]
692+
resource "bad" "my-rule" {
693+
secure = false
694+
someattr = [
695+
{
696+
Environment = "prod"
697+
},
698+
{
699+
Environment = "dev"
700+
}
701+
]
702+
}
703+
`,
704+
assertLength: 0,
705+
},
706+
{
707+
name: "ignore by list value with invalid index",
708+
inputOptions: `
709+
#trivy:ignore:*[someattr.-2.Environment=dev]
710+
resource "bad" "my-rule" {
711+
secure = false
712+
someattr = [
713+
{
714+
Environment = "prod"
715+
},
716+
{
717+
Environment = "dev"
718+
}
719+
]
720+
}
721+
`,
722+
assertLength: 1,
723+
},
724+
{
725+
name: "ignore by object value",
726+
inputOptions: `
727+
#trivy:ignore:*[tags.Environment=dev]
728+
resource "bad" "my-rule" {
729+
secure = false
730+
tags = {
731+
Environment = "dev"
732+
}
733+
}
734+
`,
735+
assertLength: 0,
736+
},
737+
{
738+
name: "ignore by object value in block",
739+
inputOptions: `
740+
#trivy:ignore:*[someblock.tags.Environment=dev]
741+
resource "bad" "my-rule" {
742+
secure = false
743+
someblock {
744+
tags = {
745+
Environment = "dev"
746+
}
747+
}
748+
}
749+
`,
750+
assertLength: 0,
751+
},
752+
{
753+
name: "ignore by list value in map",
754+
inputOptions: `
755+
variable "testvar" {
756+
type = map(list(string))
757+
default = {
758+
server1 = ["web", "dev"]
759+
server2 = ["prod"]
760+
}
761+
}
762+
763+
#trivy:ignore:*[someblock.someattr.server1.1=dev]
764+
resource "bad" "my-rule" {
765+
secure = false
766+
someblock {
767+
someattr = var.testvar
768+
}
769+
}
607770
`,
608771
assertLength: 0,
609772
},

pkg/iac/terraform/block.go

+115
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package terraform
33
import (
44
"fmt"
55
"io/fs"
6+
"strconv"
67
"strings"
78

89
"github.com/google/uuid"
@@ -298,6 +299,120 @@ func (b *Block) GetAttribute(name string) *Attribute {
298299
return nil
299300
}
300301

302+
// GetValueByPath returns the value of the attribute located at the given path.
303+
// Supports special paths like "count.index," "each.key," and "each.value."
304+
// The path may contain indices, keys and dots (used as separators).
305+
func (b *Block) GetValueByPath(path string) cty.Value {
306+
307+
if path == "count.index" || path == "each.key" || path == "each.value" {
308+
return b.Context().GetByDot(path)
309+
}
310+
311+
if restPath, ok := strings.CutPrefix(path, "each.value."); ok {
312+
if restPath == "" {
313+
return cty.NilVal
314+
}
315+
316+
val := b.Context().GetByDot("each.value")
317+
res, err := getValueByPath(val, strings.Split(restPath, "."))
318+
if err != nil {
319+
return cty.NilVal
320+
}
321+
return res
322+
}
323+
324+
attr, restPath := b.getAttributeByPath(path)
325+
326+
if attr == nil {
327+
return cty.NilVal
328+
}
329+
330+
if !attr.IsIterable() || len(restPath) == 0 {
331+
return attr.Value()
332+
}
333+
334+
res, err := getValueByPath(attr.Value(), restPath)
335+
if err != nil {
336+
return cty.NilVal
337+
}
338+
return res
339+
}
340+
341+
func (b *Block) getAttributeByPath(path string) (*Attribute, []string) {
342+
steps := strings.Split(path, ".")
343+
344+
if len(steps) == 1 {
345+
return b.GetAttribute(steps[0]), nil
346+
}
347+
348+
var (
349+
attribute *Attribute
350+
stepIndex int
351+
)
352+
353+
for currentBlock := b; currentBlock != nil && stepIndex < len(steps); {
354+
blocks := currentBlock.GetBlocks(steps[stepIndex])
355+
var nextBlock *Block
356+
if !hasIndex(steps, stepIndex+1) && len(blocks) > 0 {
357+
// if index is not provided then return the first block for backwards compatibility
358+
nextBlock = blocks[0]
359+
} else if len(blocks) > 1 && stepIndex < len(steps)-2 {
360+
// handling the case when there are multiple blocks with the same name,
361+
// e.g. when using a `dynamic` block
362+
indexVal, err := strconv.Atoi(steps[stepIndex+1])
363+
if err == nil && indexVal >= 0 && indexVal < len(blocks) {
364+
nextBlock = blocks[indexVal]
365+
stepIndex++
366+
}
367+
}
368+
369+
if nextBlock == nil {
370+
attribute = currentBlock.GetAttribute(steps[stepIndex])
371+
}
372+
373+
currentBlock = nextBlock
374+
stepIndex++
375+
}
376+
377+
return attribute, steps[stepIndex:]
378+
}
379+
380+
func hasIndex(steps []string, idx int) bool {
381+
if idx < 0 || idx >= len(steps) {
382+
return false
383+
}
384+
_, err := strconv.Atoi(steps[idx])
385+
return err == nil
386+
}
387+
388+
func getValueByPath(val cty.Value, path []string) (cty.Value, error) {
389+
var err error
390+
for _, step := range path {
391+
switch valType := val.Type(); {
392+
case valType.IsMapType():
393+
val, err = cty.IndexStringPath(step).Apply(val)
394+
case valType.IsObjectType():
395+
val, err = cty.GetAttrPath(step).Apply(val)
396+
case valType.IsListType() || valType.IsTupleType():
397+
var idx int
398+
idx, err = strconv.Atoi(step)
399+
if err != nil {
400+
return cty.NilVal, fmt.Errorf("index %q is not a number", step)
401+
}
402+
val, err = cty.IndexIntPath(idx).Apply(val)
403+
default:
404+
return cty.NilVal, fmt.Errorf(
405+
"unexpected value type %s for path step %q",
406+
valType.FriendlyName(), step,
407+
)
408+
}
409+
if err != nil {
410+
return cty.NilVal, err
411+
}
412+
}
413+
return val, nil
414+
}
415+
301416
func (b *Block) GetNestedAttribute(name string) (*Attribute, *Block) {
302417

303418
parts := strings.Split(name, ".")

0 commit comments

Comments
 (0)