Skip to content

Commit

Permalink
feat(misconf): support for ignore by nested attributes (#7205)
Browse files Browse the repository at this point in the history
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
  • Loading branch information
nikpivkin authored Aug 28, 2024
1 parent 0799770 commit 44e4686
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 15 deletions.
7 changes: 2 additions & 5 deletions docs/docs/scanner/misconfiguration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ If you want to ignore multiple resources on different attributes, you can specif
#trivy:ignore:aws-ec2-no-public-ingress-sgr[from_port=5432]
```
You can also ignore a resource on multiple attributes:
You can also ignore a resource on multiple attributes in the same rule:
```tf
locals {
rules = {
Expand Down Expand Up @@ -563,10 +563,7 @@ resource "aws_security_group_rule" "example" {
}
```
Checks can also be ignored by nested attributes, but certain restrictions apply:
- You cannot access an individual block using indexes, for example when working with dynamic blocks.
- 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.
Checks can also be ignored by nested attributes:
```tf
#trivy:ignore:*[logging_config.prefix=myprefix]
Expand Down
17 changes: 7 additions & 10 deletions pkg/iac/scanners/terraform/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,26 +135,23 @@ func ignoreByParams(params map[string]string, modules terraform.Modules, m *type
if block == nil {
return true
}
for key, val := range params {
attr, _ := block.GetNestedAttribute(key)
if attr.IsNil() || !attr.Value().IsKnown() {
return false
}
switch attr.Type() {
for key, param := range params {
val := block.GetValueByPath(key)
switch val.Type() {
case cty.String:
if !attr.Equals(val) {
if val.AsString() != param {
return false
}
case cty.Number:
bf := attr.Value().AsBigFloat()
bf := val.AsBigFloat()
f64, _ := bf.Float64()
comparableInt := fmt.Sprintf("%d", int(f64))
comparableFloat := fmt.Sprintf("%f", f64)
if val != comparableInt && val != comparableFloat {
if param != comparableInt && param != comparableFloat {
return false
}
case cty.Bool:
if fmt.Sprintf("%t", attr.IsTrue()) != val {
if fmt.Sprintf("%t", val.True()) != param {
return false
}
default:
Expand Down
163 changes: 163 additions & 0 deletions pkg/iac/scanners/terraform/ignore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,21 @@ resource "bad" "my-rule" {
}
}
}
`,
assertLength: 0,
},
{
name: "ignore by indexed dynamic block value",
inputOptions: `
// trivy:ignore:*[secure_settings.0.enabled=false]
resource "bad" "my-rule" {
dynamic "secure_settings" {
for_each = ["false", "true"]
content {
enabled = secure_settings.value
}
}
}
`,
assertLength: 0,
},
Expand Down Expand Up @@ -604,6 +619,154 @@ data "aws_iam_policy_document" "test_policy" {
resources = ["*"] # trivy:ignore:aws-iam-enforce-mfa
}
}
`,
assertLength: 0,
},
{
name: "ignore by each.value",
inputOptions: `
// trivy:ignore:*[each.value=false]
resource "bad" "my-rule" {
for_each = toset(["false", "true", "false"])
secure = each.value
}
`,
assertLength: 0,
},
{
name: "ignore by nested each.value",
inputOptions: `
locals {
vms = [
{
ip_address = "10.0.0.1"
name = "vm-1"
},
{
ip_address = "10.0.0.2"
name = "vm-2"
}
]
}
// trivy:ignore:*[each.value.name=vm-2]
resource "bad" "my-rule" {
secure = false
for_each = { for vm in local.vms : vm.name => vm }
ip_address = each.value.ip_address
}
`,
assertLength: 1,
},
{
name: "ignore resource with `count` meta-argument",
inputOptions: `
// trivy:ignore:*[count.index=1]
resource "bad" "my-rule" {
count = 2
secure = false
}
`,
assertLength: 1,
},
{
name: "invalid index when accessing blocks",
inputOptions: `
// trivy:ignore:*[ingress.99.port=9090]
// trivy:ignore:*[ingress.-10.port=9090]
resource "bad" "my-rule" {
secure = false
dynamic "ingress" {
for_each = [8080, 9090]
content {
port = ingress.value
}
}
}
`,
assertLength: 1,
},
{
name: "ignore by list value",
inputOptions: `
#trivy:ignore:*[someattr.1.Environment=dev]
resource "bad" "my-rule" {
secure = false
someattr = [
{
Environment = "prod"
},
{
Environment = "dev"
}
]
}
`,
assertLength: 0,
},
{
name: "ignore by list value with invalid index",
inputOptions: `
#trivy:ignore:*[someattr.-2.Environment=dev]
resource "bad" "my-rule" {
secure = false
someattr = [
{
Environment = "prod"
},
{
Environment = "dev"
}
]
}
`,
assertLength: 1,
},
{
name: "ignore by object value",
inputOptions: `
#trivy:ignore:*[tags.Environment=dev]
resource "bad" "my-rule" {
secure = false
tags = {
Environment = "dev"
}
}
`,
assertLength: 0,
},
{
name: "ignore by object value in block",
inputOptions: `
#trivy:ignore:*[someblock.tags.Environment=dev]
resource "bad" "my-rule" {
secure = false
someblock {
tags = {
Environment = "dev"
}
}
}
`,
assertLength: 0,
},
{
name: "ignore by list value in map",
inputOptions: `
variable "testvar" {
type = map(list(string))
default = {
server1 = ["web", "dev"]
server2 = ["prod"]
}
}
#trivy:ignore:*[someblock.someattr.server1.1=dev]
resource "bad" "my-rule" {
secure = false
someblock {
someattr = var.testvar
}
}
`,
assertLength: 0,
},
Expand Down
115 changes: 115 additions & 0 deletions pkg/iac/terraform/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package terraform
import (
"fmt"
"io/fs"
"strconv"
"strings"

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

// GetValueByPath returns the value of the attribute located at the given path.
// Supports special paths like "count.index," "each.key," and "each.value."
// The path may contain indices, keys and dots (used as separators).
func (b *Block) GetValueByPath(path string) cty.Value {

if path == "count.index" || path == "each.key" || path == "each.value" {
return b.Context().GetByDot(path)
}

if restPath, ok := strings.CutPrefix(path, "each.value."); ok {
if restPath == "" {
return cty.NilVal
}

val := b.Context().GetByDot("each.value")
res, err := getValueByPath(val, strings.Split(restPath, "."))
if err != nil {
return cty.NilVal
}
return res
}

attr, restPath := b.getAttributeByPath(path)

if attr == nil {
return cty.NilVal
}

if !attr.IsIterable() || len(restPath) == 0 {
return attr.Value()
}

res, err := getValueByPath(attr.Value(), restPath)
if err != nil {
return cty.NilVal
}
return res
}

func (b *Block) getAttributeByPath(path string) (*Attribute, []string) {
steps := strings.Split(path, ".")

if len(steps) == 1 {
return b.GetAttribute(steps[0]), nil
}

var (
attribute *Attribute
stepIndex int
)

for currentBlock := b; currentBlock != nil && stepIndex < len(steps); {
blocks := currentBlock.GetBlocks(steps[stepIndex])
var nextBlock *Block
if !hasIndex(steps, stepIndex+1) && len(blocks) > 0 {
// if index is not provided then return the first block for backwards compatibility
nextBlock = blocks[0]
} else if len(blocks) > 1 && stepIndex < len(steps)-2 {
// handling the case when there are multiple blocks with the same name,
// e.g. when using a `dynamic` block
indexVal, err := strconv.Atoi(steps[stepIndex+1])
if err == nil && indexVal >= 0 && indexVal < len(blocks) {
nextBlock = blocks[indexVal]
stepIndex++
}
}

if nextBlock == nil {
attribute = currentBlock.GetAttribute(steps[stepIndex])
}

currentBlock = nextBlock
stepIndex++
}

return attribute, steps[stepIndex:]
}

func hasIndex(steps []string, idx int) bool {
if idx < 0 || idx >= len(steps) {
return false
}
_, err := strconv.Atoi(steps[idx])
return err == nil
}

func getValueByPath(val cty.Value, path []string) (cty.Value, error) {
var err error
for _, step := range path {
switch valType := val.Type(); {
case valType.IsMapType():
val, err = cty.IndexStringPath(step).Apply(val)
case valType.IsObjectType():
val, err = cty.GetAttrPath(step).Apply(val)
case valType.IsListType() || valType.IsTupleType():
var idx int
idx, err = strconv.Atoi(step)
if err != nil {
return cty.NilVal, fmt.Errorf("index %q is not a number", step)
}
val, err = cty.IndexIntPath(idx).Apply(val)
default:
return cty.NilVal, fmt.Errorf(
"unexpected value type %s for path step %q",
valType.FriendlyName(), step,
)
}
if err != nil {
return cty.NilVal, err
}
}
return val, nil
}

func (b *Block) GetNestedAttribute(name string) (*Attribute, *Block) {

parts := strings.Split(name, ".")
Expand Down

0 comments on commit 44e4686

Please sign in to comment.