From 8dd0fcd61b37690f800f9aac6b5c95aec2bb6a65 Mon Sep 17 00:00:00 2001 From: Nikita Pivkin Date: Wed, 3 Apr 2024 03:43:29 +0300 Subject: [PATCH] feat(misconf): add support for wildcard ignores (#6414) --- docs/docs/scanner/misconfiguration/index.md | 13 ++- pkg/iac/ignore/rule.go | 37 +++++++- pkg/iac/ignore/rule_test.go | 61 ++++++++++++ .../scanners/terraform/executor/executor.go | 38 ++++---- pkg/iac/scanners/terraform/ignore_test.go | 94 ++++++++++--------- 5 files changed, 178 insertions(+), 65 deletions(-) diff --git a/docs/docs/scanner/misconfiguration/index.md b/docs/docs/scanner/misconfiguration/index.md index f76dc9392363..b243d3e8dc17 100644 --- a/docs/docs/scanner/misconfiguration/index.md +++ b/docs/docs/scanner/misconfiguration/index.md @@ -547,4 +547,15 @@ module "s3_bucket" { bucket = each.value } ``` -[custom]: custom/index.md \ No newline at end of file + +#### Support for Wildcards + +You can use wildcards in the `ws` (workspace) and `ignore` sections of the ignore rules. + +```tf +# trivy:ignore:aws-s3-*:ws:dev-* +``` + +This example ignores all checks starting with `aws-s3-` for workspaces matching the pattern `dev-*`. + +[custom]: custom/index.md diff --git a/pkg/iac/ignore/rule.go b/pkg/iac/ignore/rule.go index d81f17576915..61057ce75f87 100644 --- a/pkg/iac/ignore/rule.go +++ b/pkg/iac/ignore/rule.go @@ -1,7 +1,9 @@ package ignore import ( + "regexp" "slices" + "strings" "time" "github.com/samber/lo" @@ -10,7 +12,7 @@ import ( ) // Ignorer represents a function that checks if the rule should be ignored. -type Ignorer func(resultMeta types.Metadata, param any) bool +type Ignorer func(resultMeta types.Metadata, ignoredParam any) bool type Rules []Rule @@ -88,7 +90,16 @@ func defaultIgnorers(ids []string) map[string]Ignorer { return map[string]Ignorer{ "id": func(_ types.Metadata, param any) bool { id, ok := param.(string) - return ok && (id == "*" || len(ids) == 0 || slices.Contains(ids, id)) + if !ok { + return false + } + if id == "*" || len(ids) == 0 { + return true + } + + return slices.ContainsFunc(ids, func(s string) bool { + return MatchPattern(s, id) + }) }, "exp": func(_ types.Metadata, param any) bool { expiry, ok := param.(time.Time) @@ -96,3 +107,25 @@ func defaultIgnorers(ids []string) map[string]Ignorer { }, } } + +// MatchPattern checks if the pattern string matches the input pattern. +// The wildcard '*' in the pattern matches any sequence of characters. +func MatchPattern(input, pattern string) bool { + matched, err := regexp.MatchString(regexpFromPattern(pattern), input) + return err == nil && matched +} + +func regexpFromPattern(pattern string) string { + parts := strings.Split(pattern, "*") + if len(parts) == 1 { + return "^" + pattern + "$" + } + var sb strings.Builder + for i, literal := range parts { + if i > 0 { + sb.WriteString(".*") + } + sb.WriteString(regexp.QuoteMeta(literal)) + } + return "^" + sb.String() + "$" +} diff --git a/pkg/iac/ignore/rule_test.go b/pkg/iac/ignore/rule_test.go index 6b35e52efe43..da89ca2a3595 100644 --- a/pkg/iac/ignore/rule_test.go +++ b/pkg/iac/ignore/rule_test.go @@ -172,6 +172,24 @@ func TestRules_Ignore(t *testing.T) { }, shouldIgnore: false, }, + { + name: "with valid wildcard", + src: `#trivy:ignore:rule-*`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "with non-valid wildcard", + src: `#trivy:ignore:rule-1-*d`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1-abc"}, + }, + shouldIgnore: false, + }, } for _, tt := range tests { @@ -220,6 +238,27 @@ func TestRules_IgnoreWithCustomIgnorer(t *testing.T) { }, shouldIgnore: true, }, + { + name: "with wildcard", + src: `#trivy:ignore:rule-1:ws:dev-*`, + parser: &ignore.StringMatchParser{ + SectionKey: "ws", + }, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + ignorers: map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } + return ignore.MatchPattern("dev-stage1", ws) + }, + }, + }, + shouldIgnore: true, + }, { name: "bad", src: `#trivy:ignore:rule-1:ws:prod`, @@ -251,3 +290,25 @@ func TestRules_IgnoreWithCustomIgnorer(t *testing.T) { }) } } + +func TestMatchPattern(t *testing.T) { + tests := []struct { + input string + pattern string + expected bool + }{ + {"foo-test-bar", "*-test-*", true}, + {"foo-test-bar", "*-example-*", false}, + {"test", "*test", true}, + {"example", "test", false}, + {"example-test", "*-test*", true}, + {"example-test", "*example-*", true}, + } + + for _, tc := range tests { + t.Run(tc.input+":"+tc.pattern, func(t *testing.T) { + got := ignore.MatchPattern(tc.input, tc.pattern) + assert.Equal(t, tc.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/terraform/executor/executor.go b/pkg/iac/scanners/terraform/executor/executor.go index efc140b89b46..96c4939d756b 100644 --- a/pkg/iac/scanners/terraform/executor/executor.go +++ b/pkg/iac/scanners/terraform/executor/executor.go @@ -99,22 +99,8 @@ func (e *Executor) Execute(modules terraform.Modules) (scan.Results, error) { } ignorers := map[string]ignore.Ignorer{ - "ws": func(_ types.Metadata, param any) bool { - ws, ok := param.(string) - if !ok { - return false - } - - return ws == e.workspaceName - }, - "ignore": func(resultMeta types.Metadata, param any) bool { - params, ok := param.(map[string]string) - if !ok { - return false - } - - return ignoreByParams(params, modules, &resultMeta) - }, + "ws": workspaceIgnorer(e.workspaceName), + "ignore": attributeIgnorer(modules), } results.Ignore(ignores, ignorers) @@ -229,3 +215,23 @@ func ignoreByParams(params map[string]string, modules terraform.Modules, m *type } return true } + +func workspaceIgnorer(ws string) ignore.Ignorer { + return func(_ types.Metadata, param any) bool { + ignoredWorkspace, ok := param.(string) + if !ok { + return false + } + return ignore.MatchPattern(ws, ignoredWorkspace) + } +} + +func attributeIgnorer(modules terraform.Modules) ignore.Ignorer { + return func(resultMeta types.Metadata, param any) bool { + params, ok := param.(map[string]string) + if !ok { + return false + } + return ignoreByParams(params, modules, &resultMeta) + } +} diff --git a/pkg/iac/scanners/terraform/ignore_test.go b/pkg/iac/scanners/terraform/ignore_test.go index 0e8c0c8bfdd5..ddddd7a6e04e 100644 --- a/pkg/iac/scanners/terraform/ignore_test.go +++ b/pkg/iac/scanners/terraform/ignore_test.go @@ -599,7 +599,10 @@ data "aws_iam_policy_document" "test_policy" { resources = ["*"] # trivy:ignore:aws-iam-enforce-mfa } } -`, assertLength: 0}} +`, + assertLength: 0, + }, + } reg := rules.Register(exampleRule) defer rules.Deregister(reg) @@ -612,16 +615,53 @@ data "aws_iam_policy_document" "test_policy" { } } -func Test_IgnoreIgnoreWithExpiryAndWorkspaceAndWorkspaceSupplied(t *testing.T) { +func Test_IgnoreByWorkspace(t *testing.T) { reg := rules.Register(exampleRule) defer rules.Deregister(reg) - results := scanHCLWithWorkspace(t, ` -# tfsec:ignore:aws-service-abc123:exp:2221-01-02:ws:testworkspace -resource "bad" "my-rule" { -} -`, "testworkspace") - assert.Len(t, results.GetFailed(), 0) + tests := []struct { + name string + src string + expectedFailed int + }{ + { + name: "with expiry and workspace", + src: `# tfsec:ignore:aws-service-abc123:exp:2221-01-02:ws:testworkspace +resource "bad" "my-rule" {}`, + expectedFailed: 0, + }, + { + name: "bad workspace", + src: `# tfsec:ignore:aws-service-abc123:exp:2221-01-02:ws:otherworkspace +resource "bad" "my-rule" {}`, + expectedFailed: 1, + }, + { + name: "with expiry and workspace, trivy prefix", + src: `# trivy:ignore:aws-service-abc123:exp:2221-01-02:ws:testworkspace +resource "bad" "my-rule" {}`, + expectedFailed: 0, + }, + { + name: "bad workspace, trivy prefix", + src: `# trivy:ignore:aws-service-abc123:exp:2221-01-02:ws:otherworkspace +resource "bad" "my-rule" {}`, + expectedFailed: 1, + }, + { + name: "workspace with wildcard", + src: `# tfsec:ignore:*:ws:test* +resource "bad" "my-rule" {}`, + expectedFailed: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results := scanHCLWithWorkspace(t, tt.src, "testworkspace") + assert.Len(t, results.GetFailed(), tt.expectedFailed) + }) + } } func Test_IgnoreInline(t *testing.T) { @@ -636,19 +676,6 @@ func Test_IgnoreInline(t *testing.T) { assert.Len(t, results.GetFailed(), 0) } -func Test_IgnoreIgnoreWithExpiryAndWorkspaceButWrongWorkspaceSupplied(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCLWithWorkspace(t, ` -# tfsec:ignore:aws-service-abc123:exp:2221-01-02:ws:otherworkspace -resource "bad" "my-rule" { - -} -`, "testworkspace") - assert.Len(t, results.GetFailed(), 1) -} - func Test_IgnoreWithAliasCodeStillIgnored(t *testing.T) { reg := rules.Register(exampleRule) defer rules.Deregister(reg) @@ -662,31 +689,6 @@ resource "bad" "my-rule" { assert.Len(t, results.GetFailed(), 0) } -func Test_TrivyIgnoreIgnoreWithExpiryAndWorkspaceAndWorkspaceSupplied(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCLWithWorkspace(t, ` -# trivy:ignore:aws-service-abc123:exp:2221-01-02:ws:testworkspace -resource "bad" "my-rule" { -} -`, "testworkspace") - assert.Len(t, results.GetFailed(), 0) -} - -func Test_TrivyIgnoreIgnoreWithExpiryAndWorkspaceButWrongWorkspaceSupplied(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCLWithWorkspace(t, ` -# trivy:ignore:aws-service-abc123:exp:2221-01-02:ws:otherworkspace -resource "bad" "my-rule" { - -} -`, "testworkspace") - assert.Len(t, results.GetFailed(), 1) -} - func Test_TrivyIgnoreWithAliasCodeStillIgnored(t *testing.T) { reg := rules.Register(exampleRule) defer rules.Deregister(reg)