diff --git a/internal/testutil/util.go b/internal/testutil/util.go index 50286f9880a2..c24f17788f0a 100644 --- a/internal/testutil/util.go +++ b/internal/testutil/util.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/liamg/memoryfs" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,6 +37,16 @@ func AssertRuleNotFound(t *testing.T, ruleID string, results scan.Results, messa assert.False(t, found, append([]any{message}, args...)...) } +func AssertRuleNotFailed(t *testing.T, ruleID string, results scan.Results, message string, args ...any) { + failedExists := ruleIDInResults(ruleID, results.GetFailed()) + assert.False(t, failedExists, append([]any{message}, args...)...) + passedResults := lo.Filter(results, func(res scan.Result, _ int) bool { + return res.Status() == scan.StatusPassed || res.Status() == scan.StatusIgnored + }) + passedExists := ruleIDInResults(ruleID, passedResults) + assert.True(t, passedExists, append([]any{message}, args...)...) +} + func ruleIDInResults(ruleID string, results scan.Results) bool { for _, res := range results { if res.Rule().LongID() == ruleID { diff --git a/pkg/iac/scanners/terraform/count_test.go b/pkg/iac/scanners/terraform/count_test.go index 89aadbbf82c8..fbedaeed5a97 100644 --- a/pkg/iac/scanners/terraform/count_test.go +++ b/pkg/iac/scanners/terraform/count_test.go @@ -1,57 +1,54 @@ package terraform import ( + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/aquasecurity/trivy/internal/testutil" - "github.com/aquasecurity/trivy/pkg/iac/providers" - "github.com/aquasecurity/trivy/pkg/iac/rules" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/severity" - "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/rego" ) func Test_ResourcesWithCount(t *testing.T) { var tests = []struct { - name string - source string - expectedResults int + name string + source string + expected int }{ { name: "unspecified count defaults to 1", source: ` - resource "bad" "this" {} + resource "aws_s3_bucket" "test" {} `, - expectedResults: 1, + expected: 1, }, { name: "count is literal 1", source: ` - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = 1 } `, - expectedResults: 1, + expected: 1, }, { name: "count is literal 99", source: ` - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = 99 } `, - expectedResults: 99, + expected: 99, }, { name: "count is literal 0", source: ` - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = 0 } `, - expectedResults: 0, + expected: 0, }, { name: "count is 0 from variable", @@ -59,11 +56,11 @@ func Test_ResourcesWithCount(t *testing.T) { variable "count" { default = 0 } - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = var.count } `, - expectedResults: 0, + expected: 0, }, { name: "count is 1 from variable", @@ -71,22 +68,22 @@ func Test_ResourcesWithCount(t *testing.T) { variable "count" { default = 1 } - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = var.count } `, - expectedResults: 1, + expected: 1, }, { name: "count is 1 from variable without default", source: ` variable "count" { } - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = var.count } `, - expectedResults: 1, + expected: 1, }, { name: "count is 0 from conditional", @@ -94,11 +91,11 @@ func Test_ResourcesWithCount(t *testing.T) { variable "enabled" { default = false } - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = var.enabled ? 1 : 0 } `, - expectedResults: 0, + expected: 0, }, { name: "count is 1 from conditional", @@ -106,11 +103,11 @@ func Test_ResourcesWithCount(t *testing.T) { variable "enabled" { default = true } - resource "bad" "this" { + resource "aws_s3_bucket" "test" { count = var.enabled ? 1 : 0 } `, - expectedResults: 1, + expected: 1, }, { name: "issue 962", @@ -120,18 +117,18 @@ func Test_ResourcesWithCount(t *testing.T) { ok = true } - resource "bad" "bad" { - secure = something.else[0].ok + resource "aws_s3_bucket" "test" { + bucket = something.else[0].ok ? "test" : "" } `, - expectedResults: 0, + expected: 0, }, { name: "Test use of count.index", source: ` -resource "bad" "thing" { +resource "aws_s3_bucket" "test" { count = 1 - secure = var.things[count.index]["ok"] + bucket = var.things[count.index]["ok"] ? "test" : "" } variable "things" { @@ -145,49 +142,23 @@ variable "things" { ] } `, - expectedResults: 0, + expected: 0, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - r1 := scan.Rule{ - Provider: providers.AWSProvider, - Service: "service", - ShortCode: "abc123", - Severity: severity.High, - CustomChecks: scan.CustomChecks{ - Terraform: &scan.TerraformCustomCheck{ - RequiredLabels: []string{"bad"}, - Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { - if resourceBlock.GetAttribute("secure").IsTrue() { - return - } - results.Add( - "example problem", - resourceBlock, - ) - return - }, - }, - }, - } - reg := rules.Register(r1) - defer rules.Deregister(reg) - results := scanHCL(t, test.source) - var include string - var exclude string - if test.expectedResults > 0 { - include = r1.LongID() + results := scanHCL(t, test.source, + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + + assert.Len(t, results.GetFailed(), test.expected) + + if test.expected > 0 { + testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "false negative found") } else { - exclude = r1.LongID() - } - assert.Len(t, results.GetFailed(), test.expectedResults) - if include != "" { - testutil.AssertRuleFound(t, include, results, "false negative found") - } - if exclude != "" { - testutil.AssertRuleNotFound(t, exclude, results, "false positive found") + testutil.AssertRuleNotFound(t, "aws-s3-non-empty-bucket", results, "false positive found") } }) } diff --git a/pkg/iac/scanners/terraform/deterministic_test.go b/pkg/iac/scanners/terraform/deterministic_test.go index ccf2b7123e0c..c0e45fa8d8d8 100644 --- a/pkg/iac/scanners/terraform/deterministic_test.go +++ b/pkg/iac/scanners/terraform/deterministic_test.go @@ -1,27 +1,20 @@ package terraform import ( - "context" + "strings" "testing" "github.com/stretchr/testify/require" "github.com/aquasecurity/trivy/internal/testutil" - "github.com/aquasecurity/trivy/pkg/iac/rules" - "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/executor" - "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser" + "github.com/aquasecurity/trivy/pkg/iac/rego" ) func Test_DeterministicResults(t *testing.T) { - - reg := rules.Register(badRule) - defer rules.Deregister(reg) - - fs := testutil.CreateFS(t, map[string]string{ + fsys := testutil.CreateFS(t, map[string]string{ "first.tf": ` -resource "problem" "uhoh" { - bad = true - for_each = other.thing +resource "aws_s3_bucket" "test" { + for_each = other.thing } `, "second.tf": ` @@ -40,12 +33,11 @@ locals { }) for i := 0; i < 100; i++ { - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), ".") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) + results, err := scanFS(fsys, ".", + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) require.NoError(t, err) - results, _ := executor.New().Execute(modules) require.Len(t, results.GetFailed(), 2) } } diff --git a/pkg/iac/scanners/terraform/ignore_test.go b/pkg/iac/scanners/terraform/ignore_test.go index a42abc323d31..336fbad550c3 100644 --- a/pkg/iac/scanners/terraform/ignore_test.go +++ b/pkg/iac/scanners/terraform/ignore_test.go @@ -1,7 +1,6 @@ package terraform import ( - "context" "fmt" "strings" "testing" @@ -10,688 +9,278 @@ import ( "github.com/stretchr/testify/require" "github.com/aquasecurity/trivy/internal/testutil" - "github.com/aquasecurity/trivy/pkg/iac/providers" "github.com/aquasecurity/trivy/pkg/iac/rego" - "github.com/aquasecurity/trivy/pkg/iac/rules" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/scanners/options" - "github.com/aquasecurity/trivy/pkg/iac/severity" - "github.com/aquasecurity/trivy/pkg/iac/terraform" ) -var exampleRule = scan.Rule{ - Provider: providers.AWSProvider, - Service: "service", - ShortCode: "abc123", - AVDID: "AWS-ABC-123", - Aliases: []string{"aws-other-abc123"}, - Severity: severity.High, - CustomChecks: scan.CustomChecks{ - Terraform: &scan.TerraformCustomCheck{ - RequiredLabels: []string{"bad"}, - Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { - if attr, _ := resourceBlock.GetNestedAttribute("secure_settings.enabled"); attr.IsNotNil() { - if attr.IsFalse() { - results.Add("example problem", attr) - } - } else { - attr := resourceBlock.GetAttribute("secure") - if attr.IsNil() { - results.Add("example problem", resourceBlock) - } - if attr.IsFalse() { - results.Add("example problem", attr) - } - } - return - }, - }, - }, -} - func Test_IgnoreAll(t *testing.T) { var testCases = []struct { name string - inputOptions string + source string assertLength int }{ { - name: "IgnoreAll", - inputOptions: ` -resource "bad" "my-rule" { - secure = false // tfsec:ignore:* -} -`, + name: "inline rule ignore all checks", + source: `resource "aws_s3_bucket" "test" { + bucket = "" // %s:ignore:* +}`, assertLength: 0, }, { - name: "IgnoreLineAboveTheBlock", - inputOptions: ` -// tfsec:ignore:* -resource "bad" "my-rule" { - secure = false -} -`, + name: "rule above block ignore all checks", + source: `// %s:ignore:* +resource "aws_s3_bucket" "test" {}`, assertLength: 0, }, { - name: "IgnoreLineAboveTheBlockMatchingParamBool", - inputOptions: ` -// tfsec:ignore:*[secure=false] -resource "bad" "my-rule" { - secure = false -} -`, + name: "rule above block with boolean parameter", + source: `// %s:ignore:*[object_lock_enabled=false] +resource "aws_s3_bucket" "test" { + object_lock_enabled = false +}`, assertLength: 0, }, { - name: "IgnoreLineAboveTheBlockNotMatchingParamBool", - inputOptions: ` -// tfsec:ignore:*[secure=true] -resource "bad" "my-rule" { - secure = false -} -`, + name: "rule above block with non-matching boolean parameter", + source: `// %s:ignore:*[object_lock_enabled=false] +resource "aws_s3_bucket" "test" { + object_lock_enabled = true +}`, assertLength: 1, }, { - name: "IgnoreLineAboveTheBlockMatchingParamString", - inputOptions: ` -// tfsec:ignore:*[name=myrule] -resource "bad" "my-rule" { - name = "myrule" - secure = false -} -`, + name: "rule above block with string parameter", + source: `// %s:ignore:*[acl=private] +resource "aws_s3_bucket" "test" { + acl = "private" +}`, assertLength: 0, }, { - name: "IgnoreLineAboveTheBlockNotMatchingParamString", - inputOptions: ` -// tfsec:ignore:*[name=myrule2] -resource "bad" "my-rule" { - name = "myrule" - secure = false -} -`, + name: "rule above block with non-matching string parameter", + source: `// %s:ignore:*[acl=private] +resource "aws_s3_bucket" "test" { + acl = "public" +}`, assertLength: 1, }, { - name: "IgnoreLineAboveTheBlockMatchingParamInt", - inputOptions: ` -// tfsec:ignore:*[port=123] -resource "bad" "my-rule" { - secure = false - port = 123 -} -`, + name: "rule above block with int parameter", + source: `// %s:ignore:*[some_int=123] +resource "aws_s3_bucket" "test" { + some_int = 123 +}`, assertLength: 0, }, { - name: "IgnoreLineAboveTheBlockNotMatchingParamInt", - inputOptions: ` -// tfsec:ignore:*[port=456] -resource "bad" "my-rule" { - secure = false - port = 123 -} -`, + name: "rule above block with non-matching int parameter", + source: `// %s:ignore:*[some_int=456] +resource "aws_s3_bucket" "test" { + some_int = 123 +}`, assertLength: 1, }, { - name: "IgnoreLineStackedAboveTheBlock", - inputOptions: ` -// tfsec:ignore:* -// tfsec:ignore:a -// tfsec:ignore:b -// tfsec:ignore:c -// tfsec:ignore:d -resource "bad" "my-rule" { - secure = false -} + name: "stacked rules above block", + source: `// %s:ignore:* +// %s:ignore:a +// %s:ignore:b +// %s:ignore:c +// %s:ignore:d +resource "aws_s3_bucket" "test" {} `, assertLength: 0, }, { - name: "IgnoreLineStackedAboveTheBlockWithoutMatch", - inputOptions: ` -#tfsec:ignore:* + name: "stacked rules above block without a match", + source: `#%s:ignore:* -#tfsec:ignore:x -#tfsec:ignore:a -#tfsec:ignore:b -#tfsec:ignore:c -#tfsec:ignore:d -resource "bad" "my-rule" { - secure = false -} +#%s:ignore:x +#%s:ignore:a +#%s:ignore:b +#%s:ignore:c +#%s:ignore:d +resource "aws_s3_bucket" "test" {} `, assertLength: 1, }, { - name: "IgnoreLineStackedAboveTheBlockWithHashesWithoutSpaces", - inputOptions: ` -#tfsec:ignore:* -#tfsec:ignore:a -#tfsec:ignore:b -#tfsec:ignore:c -#tfsec:ignore:d -resource "bad" "my-rule" { - secure = false -} + name: "stacked rules above block without spaces between '#' comments", + source: `#%s:ignore:* +#%s:ignore:a +#%s:ignore:b +#%s:ignore:c +#%s:ignore:d +resource "aws_s3_bucket" "test" {} `, assertLength: 0, }, { - name: "IgnoreLineStackedAboveTheBlockWithoutSpaces", - inputOptions: ` -//tfsec:ignore:* -//tfsec:ignore:a -//tfsec:ignore:b -//tfsec:ignore:c -//tfsec:ignore:d -resource "bad" "my-rule" { - secure = false -} + name: "stacked rules above block without spaces between '//' comments", + source: `//%s:ignore:* +//%s:ignore:a +//%s:ignore:b +//%s:ignore:c +//%s:ignore:d +resource "aws_s3_bucket" "test" {} `, assertLength: 0, }, { - name: "IgnoreLineAboveTheLine", - inputOptions: ` -resource "bad" "my-rule" { - # tfsec:ignore:aws-service-abc123 - secure = false -} -`, - assertLength: 0, - }, - { - name: "IgnoreWithExpDateIfDateBreachedThenDontIgnore", - inputOptions: ` -resource "bad" "my-rule" { - secure = false # tfsec:ignore:aws-service-abc123:exp:2000-01-02 -} -`, - assertLength: 1, - }, - { - name: "IgnoreWithExpDateIfDateNotBreachedThenIgnoreIgnore", - inputOptions: ` -resource "bad" "my-rule" { - secure = false # tfsec:ignore:aws-service-abc123:exp:2221-01-02 -} -`, + name: "rule above the finding", + source: `resource "aws_s3_bucket" "test" { + # %s:ignore:aws-s3-non-empty-bucket + bucket = "" +}`, assertLength: 0, }, { - name: "IgnoreWithExpDateIfDateInvalidThenDropTheIgnore", - inputOptions: ` -resource "bad" "my-rule" { - secure = false # tfsec:ignore:aws-service-abc123:exp:2221-13-02 -} -`, + name: "rule with breached expiration date", + source: `resource "aws_s3_bucket" "test" { + bucket = "" # %s:ignore:aws-s3-non-empty-bucket:exp:2000-01-02 +}`, assertLength: 1, }, { - name: "IgnoreAboveResourceBlockWithExpDateIfDateNotBreachedThenIgnoreIgnore", - inputOptions: ` -#tfsec:ignore:aws-service-abc123:exp:2221-01-02 -resource "bad" "my-rule" { -} -`, - assertLength: 0, - }, - { - name: "IgnoreAboveResourceBlockWithExpDateAndMultipleIgnoresIfDateNotBreachedThenIgnoreIgnore", - inputOptions: ` -# tfsec:ignore:aws-service-abc123:exp:2221-01-02 -resource "bad" "my-rule" { - -} -`, - assertLength: 0, - }, - { - name: "IgnoreForImpliedIAMResource", - inputOptions: ` -terraform { - required_version = "~> 1.1.6" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 3.48" - } - } -} - -# Retrieve an IAM group defined outside of this Terraform config. - -# tfsec:ignore:aws-iam-enforce-mfa -data "aws_iam_group" "externally_defined_group" { - group_name = "group-name" # tfsec:ignore:aws-iam-enforce-mfa -} - -# Create an IAM policy and attach it to the group. - -# tfsec:ignore:aws-iam-enforce-mfa -resource "aws_iam_policy" "test_policy" { - name = "test-policy" # tfsec:ignore:aws-iam-enforce-mfa - policy = data.aws_iam_policy_document.test_policy.json # tfsec:ignore:aws-iam-enforce-mfa -} - -# tfsec:ignore:aws-iam-enforce-mfa -resource "aws_iam_group_policy_attachment" "test_policy_attachment" { - group = data.aws_iam_group.externally_defined_group.group_name # tfsec:ignore:aws-iam-enforce-mfa - policy_arn = aws_iam_policy.test_policy.arn # tfsec:ignore:aws-iam-enforce-mfa -} - -# tfsec:ignore:aws-iam-enforce-mfa -data "aws_iam_policy_document" "test_policy" { - statement { - sid = "PublishToCloudWatch" # tfsec:ignore:aws-iam-enforce-mfa - actions = [ - "cloudwatch:PutMetricData", # tfsec:ignore:aws-iam-enforce-mfa - ] - resources = ["*"] # tfsec:ignore:aws-iam-enforce-mfa - } -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreAll", - inputOptions: ` -resource "bad" "my-rule" { - secure = false // trivy:ignore:* -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreLineAboveTheBlock", - inputOptions: ` -// trivy:ignore:* -resource "bad" "my-rule" { - secure = false -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreLineAboveTheBlockMatchingParamBool", - inputOptions: ` -// trivy:ignore:*[secure=false] -resource "bad" "my-rule" { - secure = false -} -`, + name: "rule with unbreached expiration date", + source: `resource "aws_s3_bucket" "test" { + bucket = "" # %s:ignore:aws-s3-non-empty-bucket:exp:2221-01-02 +}`, assertLength: 0, }, { - name: "TrivyIgnoreLineAboveTheBlockNotMatchingParamBool", - inputOptions: ` -// trivy:ignore:*[secure=true] -resource "bad" "my-rule" { - secure = false -} -`, + name: "rule with invalid expiration date", + source: `resource "aws_s3_bucket" "test" { + bucket = "" # %s:ignore:aws-s3-non-empty-bucket:exp:2221-13-02 +}`, assertLength: 1, }, { - name: "TrivyIgnoreLineAboveTheBlockMatchingParamString", - inputOptions: ` -// trivy:ignore:*[name=myrule] -resource "bad" "my-rule" { - name = "myrule" - secure = false -} -`, + name: "rule above block with unbreached expiration date", + source: `#%s:ignore:aws-s3-non-empty-bucket:exp:2221-01-02 +resource "aws_s3_bucket" "test" {}`, assertLength: 0, }, { - name: "TrivyIgnoreLineAboveTheBlockNotMatchingParamString", - inputOptions: ` -// trivy:ignore:*[name=myrule2] -resource "bad" "my-rule" { - name = "myrule" - secure = false -} -`, - assertLength: 1, - }, - { - name: "TrivyIgnoreLineAboveTheBlockMatchingParamInt", - inputOptions: ` -// trivy:ignore:*[port=123] -resource "bad" "my-rule" { - secure = false - port = 123 -} -`, + name: "trivy inline rule ignore all checks", + source: `resource "aws_s3_bucket" "test" { + bucket = "" // %s:ignore:* +}`, assertLength: 0, }, - { - name: "TrivyIgnoreLineAboveTheBlockNotMatchingParamInt", - inputOptions: ` -// trivy:ignore:*[port=456] -resource "bad" "my-rule" { - secure = false - port = 123 -} -`, - assertLength: 1, - }, { name: "ignore by nested attribute", - inputOptions: ` -// trivy:ignore:*[secure_settings.enabled=false] -resource "bad" "my-rule" { - secure_settings { + source: `// %s:ignore:*[versioning.enabled=false] +resource "aws_s3_bucket" "test" { + versioning { enabled = false } -} -`, +}`, assertLength: 0, }, { name: "ignore by nested attribute of another type", - inputOptions: ` -// trivy:ignore:*[secure_settings.enabled=1] -resource "bad" "my-rule" { - secure_settings { + source: `// %s:ignore:*[versioning.enabled=1] +resource "aws_s3_bucket" "test" { + versioning { enabled = false } -} -`, +}`, assertLength: 1, }, { name: "ignore by non-existent nested attribute", - inputOptions: ` -// trivy:ignore:*[secure_settings.rule=myrule] -resource "bad" "my-rule" { - secure_settings { + source: `// %s:ignore:*[versioning.target=foo] +resource "aws_s3_bucket" "test" { + versioning { enabled = false } -} -`, +}`, assertLength: 1, }, { name: "ignore resource with `for_each` meta-argument", - inputOptions: ` -// trivy:ignore:*[secure=false] -resource "bad" "my-rule" { - for_each = toset(["false", "true", "false"]) - secure = each.key -} -`, - assertLength: 0, + source: `// %s:ignore:*[acl=public] +resource "aws_s3_bucket" "test" { + for_each = toset(["private", "public"]) + acl = each.value +}`, + assertLength: 1, }, { name: "ignore by dynamic block value", - inputOptions: ` -// trivy:ignore:*[secure_settings.enabled=false] -resource "bad" "my-rule" { - dynamic "secure_settings" { - for_each = ["false", "true"] - content { - enabled = secure_settings.value - } - } -} -`, - 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"] + source: `// %s:ignore:*[versioning.enabled=false] +resource "aws_s3_bucket" "test" { + dynamic "versioning" { + for_each = [{}] content { - enabled = secure_settings.value + enabled = false } } -} -`, +}`, assertLength: 0, }, { - name: "TrivyIgnoreLineStackedAboveTheBlock", - inputOptions: ` -// trivy:ignore:* -// trivy:ignore:a -// trivy:ignore:b -// trivy:ignore:c -// trivy:ignore:d -resource "bad" "my-rule" { - secure = false + name: "ignore by each.value", + source: `locals { + acls = toset(["private", "public"]) } -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreLineStackedAboveTheBlockWithoutMatch", - inputOptions: ` -#trivy:ignore:* -#trivy:ignore:x -#trivy:ignore:a -#trivy:ignore:b -#trivy:ignore:c -#trivy:ignore:d -resource "bad" "my-rule" { - secure = false -} -`, - assertLength: 1, - }, - { - name: "TrivyIgnoreLineStackedAboveTheBlockWithHashesWithoutSpaces", - inputOptions: ` -#trivy:ignore:* -#trivy:ignore:a -#trivy:ignore:b -#trivy:ignore:c -#trivy:ignore:d -resource "bad" "my-rule" { - secure = false -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreLineStackedAboveTheBlockWithoutSpaces", - inputOptions: ` -//trivy:ignore:* -//trivy:ignore:a -//trivy:ignore:b -//trivy:ignore:c -//trivy:ignore:d -resource "bad" "my-rule" { - secure = false -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreLineAboveTheLine", - inputOptions: ` -resource "bad" "my-rule" { - # trivy:ignore:aws-service-abc123 - secure = false -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreWithExpDateIfDateBreachedThenDontIgnore", - inputOptions: ` -resource "bad" "my-rule" { - secure = false # trivy:ignore:aws-service-abc123:exp:2000-01-02 -} -`, - assertLength: 1, - }, - { - name: "TrivyIgnoreWithExpDateIfDateNotBreachedThenIgnoreIgnore", - inputOptions: ` -resource "bad" "my-rule" { - secure = false # trivy:ignore:aws-service-abc123:exp:2221-01-02 -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreWithExpDateIfDateInvalidThenDropTheIgnore", - inputOptions: ` -resource "bad" "my-rule" { - secure = false # trivy:ignore:aws-service-abc123:exp:2221-13-02 -} -`, +// %s:ignore:*[each.value=private] +resource "aws_s3_bucket" "test" { + for_each = local.acls + + acl = each.value +}`, assertLength: 1, }, { - name: "TrivyIgnoreAboveResourceBlockWithExpDateIfDateNotBreachedThenIgnoreIgnore", - inputOptions: ` -#trivy:ignore:aws-service-abc123:exp:2221-01-02 -resource "bad" "my-rule" { -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreAboveResourceBlockWithExpDateAndMultipleIgnoresIfDateNotBreachedThenIgnoreIgnore", - inputOptions: ` -# trivy:ignore:aws-service-abc123:exp:2221-01-02 -resource "bad" "my-rule" { - -} -`, - assertLength: 0, - }, - { - name: "TrivyIgnoreForImpliedIAMResource", - inputOptions: ` -terraform { - required_version = "~> 1.1.6" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 3.48" + name: "ignore by nested each.value", + source: `locals { + acls = { + private = { + permission = "private" + } + public = { + permission = "public" } } } -# Retrieve an IAM group defined outside of this Terraform config. - -# trivy:ignore:aws-iam-enforce-mfa -data "aws_iam_group" "externally_defined_group" { - group_name = "group-name" # trivy:ignore:aws-iam-enforce-mfa -} - -# Create an IAM policy and attach it to the group. - -# trivy:ignore:aws-iam-enforce-mfa -resource "aws_iam_policy" "test_policy" { - name = "test-policy" # trivy:ignore:aws-iam-enforce-mfa - policy = data.aws_iam_policy_document.test_policy.json # trivy:ignore:aws-iam-enforce-mfa -} - -# trivy:ignore:aws-iam-enforce-mfa -resource "aws_iam_group_policy_attachment" "test_policy_attachment" { - group = data.aws_iam_group.externally_defined_group.group_name # trivy:ignore:aws-iam-enforce-mfa - policy_arn = aws_iam_policy.test_policy.arn # trivy:ignore:aws-iam-enforce-mfa -} +// %s:ignore:*[each.value.permission=private] +resource "aws_s3_bucket" "test" { + for_each = local.acls -# trivy:ignore:aws-iam-enforce-mfa -data "aws_iam_policy_document" "test_policy" { - statement { - sid = "PublishToCloudWatch" # trivy:ignore:aws-iam-enforce-mfa - actions = [ - "cloudwatch:PutMetricData", # trivy:ignore:aws-iam-enforce-mfa - ] - 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 -} -`, + acl = each.value.permission +}`, assertLength: 1, }, { name: "ignore resource with `count` meta-argument", - inputOptions: ` -// trivy:ignore:*[count.index=1] -resource "bad" "my-rule" { + source: `// %s:ignore:*[count.index=1] +resource "aws_s3_bucket" "test" { 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 + source: `// %s:ignore:*[ingress.99.port=9090] +// %s:ignore:*[ingress.-10.port=9090] +resource "aws_s3_bucket" "test" { 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 + source: `#%s:ignore:*[someattr.1.Environment=dev] +resource "aws_s3_bucket" "test" { someattr = [ { Environment = "prod" @@ -700,16 +289,13 @@ resource "bad" "my-rule" { 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 + source: `#%s:ignore:*[someattr.-2.Environment=dev] +resource "aws_s3_bucket" "test" { someattr = [ { Environment = "prod" @@ -718,41 +304,34 @@ resource "bad" "my-rule" { Environment = "dev" } ] -} -`, +}`, assertLength: 1, }, { name: "ignore by object value", - inputOptions: ` -#trivy:ignore:*[tags.Environment=dev] -resource "bad" "my-rule" { - secure = false + source: `#%s:ignore:*[tags.Environment=dev] +resource "aws_s3_bucket" "test" { 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 + source: `#%s:ignore:*[someblock.tags.Environment=dev] +resource "aws_s3_bucket" "test" { someblock { tags = { Environment = "dev" } } -} -`, +}`, assertLength: 0, }, { name: "ignore by list value in map", - inputOptions: ` + source: ` variable "testvar" { type = map(list(string)) default = { @@ -761,9 +340,8 @@ variable "testvar" { } } -#trivy:ignore:*[someblock.someattr.server1.1=dev] -resource "bad" "my-rule" { - secure = false +#%s:ignore:*[someblock.someattr.server1.1=dev] +resource "aws_s3_bucket" "test" { someblock { someattr = var.testvar } @@ -771,22 +349,127 @@ resource "bad" "my-rule" { `, assertLength: 0, }, + { + name: "ignore by alias", + source: `#%s:ignore:my-alias +resource "aws_s3_bucket" "test" {}`, + assertLength: 0, + }, + { + name: "ignore by alias with trivy prefix", + source: `#%s:ignore:my-alias +resource "aws_s3_bucket" "test" {}`, + assertLength: 0, + }, } - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - results := scanHCL(t, tc.inputOptions) - assert.Len(t, results.GetFailed(), tc.assertLength) + prefixes := []string{"tfsec", "trivy"} + for _, prefix := range prefixes { + t.Run(prefix, func(t *testing.T) { + results := scanHCL( + t, formatWithSingleValue(tc.source, prefix), + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + assert.Len(t, results.GetFailed(), tc.assertLength) + }) + } + }) + } +} + +func formatWithSingleValue(format string, value any) string { + count := strings.Count(format, "%s") + + args := make([]any, count) + for i := range args { + args[i] = value + } + + return fmt.Sprintf(format, args...) +} + +func Test_IgnoreByDynamicBlockValue(t *testing.T) { + + check := `# METADATA +# custom: +# avd_id: USER-TEST-0124 +# short_code: test +# provider: aws +# service: ec2 +package user.test124 + +import rego.v1 + +deny contains res if { + some group in input.aws.ec2.securitygroups + some rule in group.ingressrules + rule.toport.value < 1024 + res := result.new( + sprintf("Port below 1024 is not allowed, but got %s", [rule.toport.value]), + rule.toport, + ) +} +` + + tests := []struct { + name string + source string + expected int + }{ + { + name: "by dynamic value", + source: `// trivy:ignore:*[ingress.from_port=80] +resource "aws_security_group" "loadbalancer" { + name = "test" + + dynamic "ingress" { + for_each = [80, 443] + content { + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + } +} +`, + expected: 0, + }, + { + name: "access by index", + source: `// trivy:ignore:*[ingress.0.from_port=80] +resource "aws_security_group" "loadbalancer" { + name = "test" + + dynamic "ingress" { + for_each = [80, 443] + content { + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + } +} +`, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results := scanHCL(t, tt.source, + rego.WithPolicyReader(strings.NewReader(check)), + rego.WithPolicyNamespaces("user")) + require.Len(t, results.GetFailed(), tt.expected) }) } } func Test_IgnoreByWorkspace(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) tests := []struct { name string @@ -795,123 +478,79 @@ func Test_IgnoreByWorkspace(t *testing.T) { }{ { name: "with expiry and workspace", - src: `# tfsec:ignore:aws-service-abc123:exp:2221-01-02:ws:testworkspace -resource "bad" "my-rule" {}`, + src: `# tfsec:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:testworkspace +resource "aws_s3_bucket" "test" {}`, expectedFailed: 0, }, { name: "bad workspace", - src: `# tfsec:ignore:aws-service-abc123:exp:2221-01-02:ws:otherworkspace -resource "bad" "my-rule" {}`, + src: `# tfsec:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:otherworkspace +resource "aws_s3_bucket" "test" {}`, 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" {}`, + src: `# trivy:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:testworkspace +resource "aws_s3_bucket" "test" {}`, expectedFailed: 0, }, { name: "bad workspace, trivy prefix", - src: `# trivy:ignore:aws-service-abc123:exp:2221-01-02:ws:otherworkspace -resource "bad" "my-rule" {}`, + src: `# trivy:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:otherworkspace +resource "aws_s3_bucket" "test" {}`, expectedFailed: 1, }, { name: "workspace with wildcard", src: `# tfsec:ignore:*:ws:test* -resource "bad" "my-rule" {}`, +resource "aws_s3_bucket" "test" {}`, expectedFailed: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - results := scanHCLWithWorkspace(t, tt.src, "testworkspace") + results := scanHCL(t, tt.src, + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ScannerWithWorkspaceName("testworkspace"), + ) assert.Len(t, results.GetFailed(), tt.expectedFailed) }) } } -func Test_IgnoreInline(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCL(t, fmt.Sprintf(` - resource "bad" "sample" { - secure = false # tfsec:ignore:%s - } - `, exampleRule.LongID())) - assert.Empty(t, results.GetFailed()) -} - -func Test_IgnoreWithAliasCodeStillIgnored(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCLWithWorkspace(t, ` -# tfsec:ignore:aws-other-abc123 -resource "bad" "my-rule" { - -} -`, "testworkspace") - assert.Empty(t, results.GetFailed()) -} - -func Test_TrivyIgnoreWithAliasCodeStillIgnored(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCLWithWorkspace(t, ` -# trivy:ignore:aws-other-abc123 -resource "bad" "my-rule" { - -} -`, "testworkspace") - assert.Empty(t, results.GetFailed()) -} - -func Test_TrivyIgnoreInline(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - - results := scanHCL(t, fmt.Sprintf(` - resource "bad" "sample" { - secure = false # trivy:ignore:%s - } - `, exampleRule.LongID())) - assert.Empty(t, results.GetFailed()) -} - func Test_IgnoreInlineByAVDID(t *testing.T) { testCases := []struct { input string }{ { - input: ` - resource "bad" "sample" { - secure = false # tfsec:ignore:%s - } + input: `resource "aws_s3_bucket" "test" { + bucket = "" # tfsec:ignore:%s +} `, }, { - input: ` - resource "bad" "sample" { - secure = false # trivy:ignore:%s - } + input: `resource "aws_s3_bucket" "test" { + bucket = "" # trivy:ignore:%s +} `, }, } for _, tc := range testCases { - tc := tc - for _, id := range []string{exampleRule.AVDID, strings.ToLower(exampleRule.AVDID), exampleRule.ShortCode, exampleRule.LongID()} { - id := id - t.Run("", func(t *testing.T) { - reg := rules.Register(exampleRule) - defer rules.Deregister(reg) - results := scanHCL(t, fmt.Sprintf(tc.input, id)) - assert.Empty(t, results.GetFailed()) + ids := []string{ + "USER-TEST-0123", strings.ToLower("user-test-0123"), + "non-empty-bucket", "aws-s3-non-empty-bucket", + } + + for _, id := range ids { + t.Run(id, func(t *testing.T) { + results := scanHCL(t, fmt.Sprintf(tc.input, id), + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") }) } } @@ -935,37 +574,17 @@ func TestIgnoreRemoteTerraformResource(t *testing.T) { } `, ".terraform/modules/bucket/main.tf": ` -# trivy:ignore:test-0001 +# trivy:ignore:user-test-0123 resource "aws_s3_bucket" "test" { bucket = "" } `, }) - check := `# METADATA -# title: Test -# custom: -# id: test-0001 -# avdid: test-0001 - -package user.test0001 - -deny[res] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "" - res := result.new("Empty bucket name!", bucket) -}` - - localScanner := New( - rego.WithEmbeddedPolicies(false), - rego.WithEmbeddedLibraries(true), - options.ScannerWithRegoOnly(true), + results, err := scanFS(fsys, ".", + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), rego.WithPolicyNamespaces("user"), - rego.WithPolicyReader(strings.NewReader(check)), - ScannerWithDownloadsAllowed(false), - ScannerWithSkipCachedModules(true), ) - results, err := localScanner.ScanFS(context.TODO(), fsys, ".") require.NoError(t, err) - assert.Empty(t, results.GetFailed()) + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") } diff --git a/pkg/iac/scanners/terraform/json_test.go b/pkg/iac/scanners/terraform/json_test.go index 835425265f17..aad2d9ac8c32 100644 --- a/pkg/iac/scanners/terraform/json_test.go +++ b/pkg/iac/scanners/terraform/json_test.go @@ -1,22 +1,19 @@ package terraform import ( + "strings" "testing" "github.com/aquasecurity/trivy/internal/testutil" - "github.com/aquasecurity/trivy/pkg/iac/providers" - "github.com/aquasecurity/trivy/pkg/iac/rules" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/severity" - "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/rego" ) func TestScanningJSON(t *testing.T) { var tests = []struct { - name string - source string - shouldFail bool + name string + source string + expected bool }{ { name: "check results are picked up in tf json configs", @@ -29,16 +26,14 @@ func TestScanningJSON(t *testing.T) { } }, "resource": { - "bad": { - "thing": { - "type": "ingress", - "cidr_blocks": ["0.0.0.0/0"], - "description": "testing" + "aws_s3_bucket": { + "test": { + "bucket": "" } } } }`, - shouldFail: true, + expected: true, }, { name: "check attributes are checked in tf json configs", @@ -51,52 +46,27 @@ func TestScanningJSON(t *testing.T) { } }, "resource": { - "bad": { - "or_not": { - "secure": true + "aws_s3_bucket": { + "test": { + "bucket": "test" } } } }`, - shouldFail: false, + expected: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - r1 := scan.Rule{ - Provider: providers.AWSProvider, - Service: "service", - ShortCode: "abc123", - Severity: severity.High, - CustomChecks: scan.CustomChecks{ - Terraform: &scan.TerraformCustomCheck{ - RequiredLabels: []string{"bad"}, - Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { - if resourceBlock.GetAttribute("secure").IsTrue() { - return - } - results.Add("something", resourceBlock) - return - }, - }, - }, - } - reg := rules.Register(r1) - defer rules.Deregister(reg) - - results := scanJSON(t, test.source) - var include, exclude string - if test.shouldFail { - include = r1.LongID() + results := scanJSON(t, test.source, + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + if test.expected { + testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "false negative found") } else { - exclude = r1.LongID() - } - if include != "" { - testutil.AssertRuleFound(t, include, results, "false negative found") - } - if exclude != "" { - testutil.AssertRuleNotFound(t, exclude, results, "false positive found") + testutil.AssertRuleNotFound(t, "aws-s3-non-empty-bucket", results, "false positive found") } }) } diff --git a/pkg/iac/scanners/terraform/module_test.go b/pkg/iac/scanners/terraform/module_test.go index ec4291fd59e4..a70ec36582e1 100644 --- a/pkg/iac/scanners/terraform/module_test.go +++ b/pkg/iac/scanners/terraform/module_test.go @@ -1,52 +1,26 @@ package terraform import ( - "context" + "strings" "testing" "github.com/stretchr/testify/require" - "github.com/aquasecurity/trivy-checks/checks/cloud/aws/iam" "github.com/aquasecurity/trivy/internal/testutil" - "github.com/aquasecurity/trivy/pkg/iac/providers" - "github.com/aquasecurity/trivy/pkg/iac/rules" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/executor" - "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser" - "github.com/aquasecurity/trivy/pkg/iac/severity" - "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/rego" ) -var badRule = scan.Rule{ - Provider: providers.AWSProvider, - Service: "service", - ShortCode: "abc", - Summary: "A stupid example check for a test.", - Impact: "You will look stupid", - Resolution: "Don't do stupid stuff", - Explanation: "Bad should not be set.", - Severity: severity.High, - CustomChecks: scan.CustomChecks{ - Terraform: &scan.TerraformCustomCheck{ - RequiredTypes: []string{"resource"}, - RequiredLabels: []string{"problem"}, - Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { - if attr := resourceBlock.GetAttribute("bad"); attr.IsTrue() { - results.Add("bad", attr) - } - return - }, - }, - }, -} - -// IMPORTANT: if this test is failing, you probably need to set the version of go-cty in go.mod to the same version that hcl uses. -func Test_GoCtyCompatibilityIssue(t *testing.T) { - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "/project/main.tf": ` +func Test_Modules(t *testing.T) { + tests := []struct { + name string + files map[string]string + expected bool + }{ + { + // IMPORTANT: if this test is failing, you probably need to set the version of go-cty in go.mod to the same version that hcl uses. + name: "go-cty compatibility issue", + files: map[string]string{ + "/project/main.tf": ` data "aws_vpc" "default" { default = true } @@ -54,10 +28,8 @@ data "aws_vpc" "default" { module "test" { source = "../modules/problem/" cidr_block = data.aws_vpc.default.cidr_block -} -`, - "/modules/problem/main.tf": ` -variable "cidr_block" {} +}`, + "/modules/problem/main.tf": `variable "cidr_block" {} variable "open" { default = false @@ -76,589 +48,333 @@ resource "aws_security_group" "this" { } } -resource "problem" "uhoh" { - bad = true -} -`, - }) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") -} - -func Test_ProblemInModuleInSiblingDir(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "/project/main.tf": ` +resource "aws_s3_bucket" "test" {}`, + }, + expected: true, + }, + { + name: "misconfig in sibling directory module", + files: map[string]string{ + "/project/main.tf": ` module "something" { source = "../modules/problem" } `, - "modules/problem/main.tf": ` -resource "problem" "uhoh" { - bad = true -} -`}, - ) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") - -} - -func Test_ProblemInModuleIgnored(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "/project/main.tf": ` -#tfsec:ignore:aws-service-abc + "modules/problem/main.tf": ` +resource "aws_s3_bucket" "test" {}`, + }, + expected: true, + }, + { + name: "ignore misconfig in module", + files: map[string]string{ + "/project/main.tf": ` +#tfsec:ignore:aws-s3-non-empty-bucket module "something" { source = "../modules/problem" } `, - "modules/problem/main.tf": ` -resource "problem" "uhoh" { - bad = true -} -`}, - ) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleNotFound(t, badRule.LongID(), results, "") - -} - -func Test_ProblemInModuleInSubdirectory(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` + "modules/problem/main.tf": ` +resource "aws_s3_bucket" "test" {} +`, + }, + expected: false, + }, + { + name: "misconfig in subdirectory module", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "./modules/problem" } `, - "project/modules/problem/main.tf": ` -resource "problem" "uhoh" { - bad = true -} -`}) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") - -} - -func Test_ProblemInModuleInParentDir(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` + "project/modules/problem/main.tf": ` +resource "aws_s3_bucket" "test" {} +`, + }, + expected: true, + }, + { + name: "misconfig in parent directory module", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "../problem" } `, - "problem/main.tf": ` -resource "problem" "uhoh" { - bad = true -} -`}) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") - -} - -func Test_ProblemInModuleReuse(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` + "problem/main.tf": ` +resource "aws_s3_bucket" "test" {} +`}, + expected: true, + }, + { + name: "misconfig in reused module", + files: map[string]string{ + "project/main.tf": ` module "something_good" { source = "../modules/problem" - bad = false + bucket = "test" } module "something_bad" { source = "../modules/problem" - bad = true + bucket = "" } `, - "modules/problem/main.tf": ` -variable "bad" { - default = false -} -resource "problem" "uhoh" { - bad = var.bad -} -`}) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") + "modules/problem/main.tf": ` +variable "bucket" {} +resource "aws_s3_bucket" "test" { + bucket = var.bucket } - -func Test_ProblemInNestedModule(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` +`}, + expected: true, + }, + { + name: "misconfig in nested module", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "../modules/a" } `, - "modules/a/main.tf": ` + "modules/a/main.tf": ` module "something" { source = "../../modules/b" } `, - "modules/b/main.tf": ` + "modules/b/main.tf": ` module "something" { source = "../c" } `, - "modules/c/main.tf": ` -resource "problem" "uhoh" { - bad = true -} -`, - }) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") - -} - -func Test_ProblemInReusedNestedModule(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` + "modules/c/main.tf": `resource "aws_s3_bucket" "test" {}`, + }, + expected: true, + }, + { + name: "misconfig in reused nested module", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "../modules/a" - bad = false + bucket = "test" } module "something-bad" { source = "../modules/a" - bad = true + bucket = "" } `, - "modules/a/main.tf": ` -variable "bad" { - default = false -} + "modules/a/main.tf": ` +variable "bucket" {} + module "something" { source = "../../modules/b" - bad = var.bad + bucket = var.bucket } `, - "modules/b/main.tf": ` -variable "bad" { - default = false -} + "modules/b/main.tf": ` +variable "bucket" {} + module "something" { source = "../c" - bad = var.bad -} -`, - "modules/c/main.tf": ` -variable "bad" { - default = false -} -resource "problem" "uhoh" { - bad = var.bad + bucket = var.bad } `, - }) + "modules/c/main.tf": ` +variable "bucket" {} - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") +resource "aws_s3_bucket" "test" { + bucket = var.bucket } - -func Test_ProblemInInitialisedModule(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` +`, + }, + expected: true, + }, + { + name: "misconfig in terraform cached module", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "../modules/somewhere" - bad = false + bucket = "test" } `, - "modules/somewhere/main.tf": ` + "modules/somewhere/main.tf": ` module "something_nested" { count = 1 source = "github.com/some/module.git" - bad = true + bucket = "" } -variable "bad" { - default = false -} +variable "bucket" { + default = "" +}`, + "project/.terraform/modules/something.something_nested/main.tf": ` +variable "bucket" {} -`, - "project/.terraform/modules/something.something_nested/main.tf": ` -variable "bad" { - default = false -} -resource "problem" "uhoh" { - bad = var.bad +resource "aws_s3_bucket" "test" { + bucket = var.bucket } `, - "project/.terraform/modules/modules.json": ` + "project/.terraform/modules/modules.json": ` {"Modules":[ {"Key":"something","Source":"../modules/somewhere","Version":"2.35.0","Dir":"../modules/somewhere"}, {"Key":"something.something_nested","Source":"git::https://github.com/some/module.git","Version":"2.35.0","Dir":".terraform/modules/something.something_nested"} ]} `, - }) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") -} - -func Test_ProblemInReusedInitialisedModule(t *testing.T) { - - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` + }, + expected: true, + }, + { + name: "misconfig in reused terraform cached module", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "/nowhere" - bad = false + bucket = "" } + module "something2" { source = "/nowhere" - bad = true + bucket = "" } `, - "project/.terraform/modules/a/main.tf": ` -variable "bad" { - default = false -} -resource "problem" "uhoh" { - bad = var.bad + "project/.terraform/modules/a/main.tf": ` +variable "bucket" {} + +resource "aws_s3_bucket" "test" { + bucket = var.bucket } `, - "project/.terraform/modules/modules.json": ` + "project/.terraform/modules/modules.json": ` {"Modules":[{"Key":"something","Source":"/nowhere","Version":"2.35.0","Dir":".terraform/modules/a"},{"Key":"something2","Source":"/nowhere","Version":"2.35.0","Dir":".terraform/modules/a"}]} `, - }) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") - -} - -func Test_ProblemInDuplicateModuleNameAndPath(t *testing.T) { - registered := rules.Register(badRule) - defer rules.Deregister(registered) - - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` + }, + expected: true, + }, + { + name: "misconfig in nested modules with duplicate module names and paths", + files: map[string]string{ + "project/main.tf": ` module "something" { source = "../modules/a" - bad = 0 + s3_bucket_count = 0 } module "something-bad" { source = "../modules/a" - bad = 1 + s3_bucket_count = 1 } `, - "modules/a/main.tf": ` -variable "bad" { + "modules/a/main.tf": ` +variable "s3_bucket_count" { default = 0 } module "something" { source = "../b" - bad = var.bad + s3_bucket_count = var.s3_bucket_count } `, - "modules/b/main.tf": ` -variable "bad" { + "modules/b/main.tf": ` +variable "s3_bucket_count" { default = 0 } module "something" { source = "../c" - bad = var.bad + s3_bucket_count = var.s3_bucket_count } `, - "modules/c/main.tf": ` -variable "bad" { + "modules/c/main.tf": ` +variable "s3_bucket_count" { default = 0 } -resource "problem" "uhoh" { - count = var.bad - bad = true -} -`, - }) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleFound(t, badRule.LongID(), results, "") +resource "aws_s3_bucket" "test" { + count = var.s3_bucket_count } - -func Test_Dynamic_Variables(t *testing.T) { - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` +`, + }, + expected: true, + }, + { + name: "misconfigured attribute referencing to dynamic variable", + files: map[string]string{ + "project/main.tf": ` resource "something" "this" { - dynamic "blah" { for_each = ["a"] - content { - ok = true + bucket = "" } } } - -resource "bad" "thing" { - secure = something.this.blah[0].ok -} -`}) - - r1 := scan.Rule{ - Provider: providers.AWSProvider, - Service: "service", - ShortCode: "abc123", - Severity: severity.High, - CustomChecks: scan.CustomChecks{ - Terraform: &scan.TerraformCustomCheck{ - RequiredLabels: []string{"bad"}, - Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { - if resourceBlock.GetAttribute("secure").IsTrue() { - return - } - results.Add("example problem", resourceBlock) - return - }, - }, - }, - } - reg := rules.Register(r1) - defer rules.Deregister(reg) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - testutil.AssertRuleFound(t, r1.LongID(), results, "") +resource "aws_s3_bucket" "test" { + secure = something.this.blah[0].bucket } - -func Test_Dynamic_Variables_FalsePositive(t *testing.T) { - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` +`}, + expected: true, + }, + { + name: "attribute referencing to dynamic variable without index", + files: map[string]string{ + "project/main.tf": ` resource "something" "else" { - x = 1 dynamic "blah" { - for_each = toset(["true"]) - + for_each = toset(["test"]) content { - ok = blah.value + bucket = blah.value } } } - -resource "bad" "thing" { - secure = something.else.blah.ok -} -`}) - r1 := scan.Rule{ - Provider: providers.AWSProvider, - Service: "service", - ShortCode: "abc123", - Severity: severity.High, - CustomChecks: scan.CustomChecks{ - Terraform: &scan.TerraformCustomCheck{ - RequiredLabels: []string{"bad"}, - Check: func(resourceBlock *terraform.Block, _ *terraform.Module) (results scan.Results) { - if resourceBlock.GetAttribute("secure").IsTrue() { - return - } - results.Add("example problem", resourceBlock) - return - }, - }, +resource "aws_s3_bucket" "test" { + bucket = something.else.blah.bucket +}`}, + expected: false, }, - } - reg := rules.Register(r1) - defer rules.Deregister(reg) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleNotFound(t, r1.LongID(), results, "") -} - -func Test_ReferencesPassedToNestedModule(t *testing.T) { + { + name: "references passed to nested module", + files: map[string]string{ + "project/main.tf": ` - fs := testutil.CreateFS(t, map[string]string{ - "project/main.tf": ` - -resource "aws_iam_group" "developers" { - name = "developers" +resource "some_resource" "this" { + name = "test" } module "something" { source = "../modules/a" - group = aws_iam_group.developers.name + bucket = some_resource.this.name } `, - "modules/a/main.tf": ` -variable "group" { + "modules/a/main.tf": ` +variable "bucket" { type = string } -resource "aws_iam_group_policy" "mfa" { - group = var.group - policy = data.aws_iam_policy_document.policy.json +resource "aws_s3_bucket" "test" { + bucket = var.bucket } +`}, + expected: false, + }, + } -data "aws_iam_policy_document" "policy" { - statement { - sid = "main" - effect = "Allow" - - actions = ["s3:*"] - resources = ["*"] - condition { - test = "Bool" - variable = "aws:MultiFactorAuthPresent" - values = ["true"] - } - } -} -`}) - - p := parser.New(fs, "", parser.OptionStopOnHCLError(true)) - err := p.ParseFS(context.TODO(), "project") - require.NoError(t, err) - modules, _, err := p.EvaluateAll(context.TODO()) - require.NoError(t, err) - - results, err := executor.New().Execute(modules) - require.NoError(t, err) - - testutil.AssertRuleNotFound(t, iam.CheckEnforceGroupMFA.LongID(), results, "") - + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(t, tt.files) + results, err := scanFS(fsys, "project", + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + require.NoError(t, err) + if tt.expected { + testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "") + } else { + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") + } + }) + } } diff --git a/pkg/iac/scanners/terraform/options.go b/pkg/iac/scanners/terraform/options.go index 5a665a05638f..7547a19a8382 100644 --- a/pkg/iac/scanners/terraform/options.go +++ b/pkg/iac/scanners/terraform/options.go @@ -100,3 +100,11 @@ func ScannerWithSkipDirs(dirs []string) options.ScannerOption { } } } + +func ScannerWithStopOnHCLError(stop bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionStopOnHCLError(stop)) + } + } +} diff --git a/pkg/iac/scanners/terraform/scanner_integration_test.go b/pkg/iac/scanners/terraform/scanner_integration_test.go index 96c681e97553..bac3169992b2 100644 --- a/pkg/iac/scanners/terraform/scanner_integration_test.go +++ b/pkg/iac/scanners/terraform/scanner_integration_test.go @@ -2,6 +2,7 @@ package terraform import ( "context" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -24,29 +25,11 @@ module "s3_bucket" { bucket = "my-s3-bucket" } `, - "/rules/bucket_name.rego": ` -# METADATA -# schemas: -# - input: schema.input -# custom: -# avd_id: AVD-AWS-0001 -# input: -# selector: -# - type: cloud -# subtypes: -# - service: s3 -# provider: aws -package defsec.test.aws1 -deny[res] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "" - res := result.new("The name of the bucket must not be empty", bucket) -}`, }) scanner := New( - rego.WithPolicyFilesystem(fs), - rego.WithPolicyDirs("rules"), + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), rego.WithEmbeddedPolicies(false), rego.WithEmbeddedLibraries(false), options.ScannerWithRegoOnly(true), @@ -81,29 +64,11 @@ module "s3_bucket" { bucket = var.bucket } `, - "rules/bucket_name.rego": ` -# METADATA -# schemas: -# - input: schema.input -# custom: -# avd_id: AVD-AWS-0001 -# input: -# selector: -# - type: cloud -# subtypes: -# - service: s3 -# provider: aws -package defsec.test.aws1 -deny[res] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "" - res := result.new("The name of the bucket must not be empty", bucket) -}`, }) scanner := New( - rego.WithPolicyFilesystem(fs), - rego.WithPolicyDirs("rules"), + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), rego.WithEmbeddedPolicies(false), rego.WithEmbeddedLibraries(false), options.ScannerWithRegoOnly(true), diff --git a/pkg/iac/scanners/terraform/scanner_test.go b/pkg/iac/scanners/terraform/scanner_test.go index a7020b4ddd06..6cdcc06d77f3 100644 --- a/pkg/iac/scanners/terraform/scanner_test.go +++ b/pkg/iac/scanners/terraform/scanner_test.go @@ -20,76 +20,25 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/types" ) -const emptyBucketRule = ` -# METADATA -# schemas: -# - input: schema.input -# custom: -# avd_id: AVD-AWS-0001 -# input: -# selector: -# - type: cloud -# subtypes: -# - service: s3 -# provider: aws -package defsec.test.aws1 -deny[res] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "" - res := result.new("The name of the bucket must not be empty", bucket) -} -` - func Test_OptionWithPolicyDirs(t *testing.T) { - fs := testutil.CreateFS(t, map[string]string{ - "/code/main.tf": ` -resource "aws_s3_bucket" "my-bucket" { - bucket = "evil" -} -`, - "/rules/test.rego": ` -package defsec.abcdefg - -__rego_metadata__ := { - "id": "TEST123", - "avd_id": "AVD-TEST-0123", - "title": "Buckets should not be evil", - "short_code": "no-evil-buckets", - "severity": "CRITICAL", - "type": "DefSec Security Check", - "description": "You should not allow buckets to be evil", - "recommended_actions": "Use a good bucket instead", - "url": "https://google.com/search?q=is+my+bucket+evil", -} - -__rego_input__ := { - "combine": false, - "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], -} - -deny[cause] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "evil" - cause := bucket.name -} -`, + fsys := testutil.CreateFS(t, map[string]string{ + "/code/main.tf": `resource "aws_s3_bucket" "my-bucket" {}`, + "/rules/test.rego": emptyBucketCheck, }) - scanner := New( - rego.WithPolicyFilesystem(fs), + results, err := scanFS(fsys, "code", + rego.WithPolicyFilesystem(fsys), rego.WithPolicyDirs("rules"), - options.ScannerWithRegoOnly(true), + rego.WithPolicyNamespaces("user"), ) - - results, err := scanner.ScanFS(context.TODO(), fs, "code") require.NoError(t, err) require.Len(t, results.GetFailed(), 1) failure := results.GetFailed()[0] - assert.Equal(t, "AVD-TEST-0123", failure.Rule().AVDID) + assert.Equal(t, "USER-TEST-0123", failure.Rule().AVDID) actualCode, err := failure.GetCode() require.NoError(t, err) @@ -98,28 +47,11 @@ deny[cause] { } assert.Equal(t, []scan.Line{ { - Number: 2, - Content: "resource \"aws_s3_bucket\" \"my-bucket\" {", - IsCause: false, - FirstCause: false, - LastCause: false, - Annotation: "", - }, - { - Number: 3, - Content: "\tbucket = \"evil\"", + Number: 1, + Content: "resource \"aws_s3_bucket\" \"my-bucket\" {}", IsCause: true, FirstCause: true, LastCause: true, - Annotation: "", - }, - { - Number: 4, - Content: "}", - IsCause: false, - FirstCause: false, - LastCause: false, - Annotation: "", }, }, actualCode.Lines) @@ -236,99 +168,41 @@ cause := bucket.name func Test_OptionWithRegoOnly(t *testing.T) { - fs := testutil.CreateFS(t, map[string]string{ + fsys := testutil.CreateFS(t, map[string]string{ "/code/main.tf": ` -resource "aws_s3_bucket" "my-bucket" { - bucket = "evil" -} -`, - "/rules/test.rego": ` -package defsec.abcdefg - -__rego_metadata__ := { - "id": "TEST123", - "avd_id": "AVD-TEST-0123", - "title": "Buckets should not be evil", - "short_code": "no-evil-buckets", - "severity": "CRITICAL", - "type": "DefSec Security Check", - "description": "You should not allow buckets to be evil", - "recommended_actions": "Use a good bucket instead", - "url": "https://google.com/search?q=is+my+bucket+evil", -} - -__rego_input__ := { - "combine": false, - "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], -} - -deny[cause] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "evil" - cause := bucket.name -} +resource "aws_s3_bucket" "my-bucket" {} `, + "/rules/test.rego": emptyBucketCheck, }) - scanner := New( + results, err := scanFS(fsys, "code", rego.WithPolicyDirs("rules"), - options.ScannerWithRegoOnly(true), + rego.WithPolicyNamespaces("user"), ) - - results, err := scanner.ScanFS(context.TODO(), fs, "code") require.NoError(t, err) require.Len(t, results.GetFailed(), 1) - assert.Equal(t, "AVD-TEST-0123", results[0].Rule().AVDID) + assert.Equal(t, "USER-TEST-0123", results[0].Rule().AVDID) } func Test_OptionWithRegoOnly_CodeHighlighting(t *testing.T) { - fs := testutil.CreateFS(t, map[string]string{ + fsys := testutil.CreateFS(t, map[string]string{ "/code/main.tf": ` -resource "aws_s3_bucket" "my-bucket" { - bucket = "evil" -} -`, - "/rules/test.rego": ` -package defsec.abcdefg - -__rego_metadata__ := { - "id": "TEST123", - "avd_id": "AVD-TEST-0123", - "title": "Buckets should not be evil", - "short_code": "no-evil-buckets", - "severity": "CRITICAL", - "type": "DefSec Security Check", - "description": "You should not allow buckets to be evil", - "recommended_actions": "Use a good bucket instead", - "url": "https://google.com/search?q=is+my+bucket+evil", -} - -__rego_input__ := { - "combine": false, - "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], -} - -deny[res] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "evil" - res := result.new("oh no", bucket.name) -} +resource "aws_s3_bucket" "my-bucket" {} `, + "/rules/test.rego": emptyBucketCheck, }) - scanner := New( + results, err := scanFS(fsys, "code", rego.WithPolicyDirs("rules"), - options.ScannerWithRegoOnly(true), - rego.WithEmbeddedLibraries(true), + rego.WithPolicyNamespaces("user"), ) - results, err := scanner.ScanFS(context.TODO(), fs, "code") require.NoError(t, err) require.Len(t, results.GetFailed(), 1) - assert.Equal(t, "AVD-TEST-0123", results[0].Rule().AVDID) + assert.Equal(t, "USER-TEST-0123", results[0].Rule().AVDID) assert.NotNil(t, results[0].Metadata().Range().GetFS()) } @@ -709,7 +583,7 @@ resource "aws_s3_bucket" "main" { bucket = var.bucket_name } `, - "rules/bucket_name.rego": emptyBucketRule, + "rules/bucket_name.rego": emptyBucketCheck, }) configsFS := testutil.CreateFS(t, map[string]string{ @@ -719,6 +593,7 @@ bucket_name = "test" }) scanner := New( + rego.WithPolicyNamespaces("user"), rego.WithPolicyDirs("rules"), rego.WithPolicyFilesystem(fs), options.ScannerWithRegoOnly(true), @@ -746,13 +621,14 @@ resource "aws_s3_bucket" "main" { bucket = var.bucket_name } `, - "rules/bucket_name.rego": emptyBucketRule, + "rules/bucket_name.rego": emptyBucketCheck, "main.tfvars": ` bucket_name = "test" `, }) scanner := New( + rego.WithPolicyNamespaces("user"), rego.WithPolicyDirs("rules"), rego.WithPolicyFilesystem(fs), options.ScannerWithRegoOnly(true), @@ -805,25 +681,7 @@ resource "aws_security_group" "main" { description = var.security_group_description } `, - "/rules/bucket_name.rego": ` -# METADATA -# schemas: -# - input: schema.input -# custom: -# avd_id: AVD-AWS-0001 -# input: -# selector: -# - type: cloud -# subtypes: -# - service: s3 -# provider: aws -package defsec.test.aws1 -deny[res] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "" - res := result.new("The name of the bucket must not be empty", bucket) -} -`, + "/rules/bucket_name.rego": emptyBucketCheck, "/rules/sec_group_description.rego": ` # METADATA # schemas: @@ -846,6 +704,7 @@ deny[res] { }) scanner := New( + rego.WithPolicyNamespaces("user"), rego.WithPolicyFilesystem(fs), rego.WithPolicyDirs("rules"), rego.WithEmbeddedPolicies(false), diff --git a/pkg/iac/scanners/terraform/setup_test.go b/pkg/iac/scanners/terraform/setup_test.go index 5b98c438f9c1..3b2e39c8c7e7 100644 --- a/pkg/iac/scanners/terraform/setup_test.go +++ b/pkg/iac/scanners/terraform/setup_test.go @@ -2,6 +2,7 @@ package terraform import ( "context" + "io/fs" "testing" "github.com/stretchr/testify/require" @@ -14,6 +15,33 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/terraform" ) +var emptyBucketCheck = `# METADATA +# schemas: +# - input: schema.cloud +# custom: +# avd_id: USER-TEST-0123 +# short_code: non-empty-bucket +# provider: aws +# service: s3 +# aliases: +# - my-alias +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package user.test123 + +import rego.v1 + +deny contains res if { + some bucket in input.aws.s3.buckets + bucket.name.value == "" + res := result.new("The bucket name cannot be empty.", bucket.name) +} +` + func createModulesFromSource(t *testing.T, source, ext string) terraform.Modules { fs := testutil.CreateFS(t, map[string]string{ "source" + ext: source, @@ -30,30 +58,40 @@ func createModulesFromSource(t *testing.T, source, ext string) terraform.Modules return modules } -func scanHCLWithWorkspace(t *testing.T, source, workspace string) scan.Results { - return scanHCL(t, source, ScannerWithWorkspaceName(workspace)) +func scanFS(fsys fs.FS, target string, opts ...options.ScannerOption) (scan.Results, error) { + s := New(append( + []options.ScannerOption{ + rego.WithEmbeddedLibraries(true), + rego.WithRegoErrorLimits(0), + options.ScannerWithRegoOnly(true), + ScannerWithAllDirectories(true), + ScannerWithSkipCachedModules(true), + ScannerWithStopOnHCLError(true), + }, + opts..., + )..., + ) + + return s.ScanFS(context.TODO(), fsys, target) } func scanHCL(t *testing.T, source string, opts ...options.ScannerOption) scan.Results { - fs := testutil.CreateFS(t, map[string]string{ + fsys := testutil.CreateFS(t, map[string]string{ "main.tf": source, }) - - localScanner := New(append(opts, rego.WithEmbeddedPolicies(false))...) - results, err := localScanner.ScanFS(context.TODO(), fs, ".") + results, err := scanFS(fsys, ".", opts...) require.NoError(t, err) return results } -func scanJSON(t *testing.T, source string) scan.Results { +func scanJSON(t *testing.T, source string, opts ...options.ScannerOption) scan.Results { - fs := testutil.CreateFS(t, map[string]string{ + fsys := testutil.CreateFS(t, map[string]string{ "main.tf.json": source, }) - s := New(rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true)) - results, err := s.ScanFS(context.TODO(), fs, ".") + results, err := scanFS(fsys, ".", opts...) require.NoError(t, err) return results } diff --git a/pkg/iac/scanners/terraform/wildcard_test.go b/pkg/iac/scanners/terraform/wildcard_test.go index 5de281e69be0..4e5ec3bacd9f 100644 --- a/pkg/iac/scanners/terraform/wildcard_test.go +++ b/pkg/iac/scanners/terraform/wildcard_test.go @@ -1,9 +1,12 @@ package terraform import ( + "context" "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/aquasecurity/trivy/internal/testutil" "github.com/aquasecurity/trivy/pkg/iac/rules" "github.com/aquasecurity/trivy/pkg/iac/scan" @@ -49,7 +52,7 @@ func Test_WildcardMatchingOnRequiredLabels(t *testing.T) { code := fmt.Sprintf("wild%d", i) - t.Run(code, func(t *testing.T) { + t.Run(test.pattern, func(t *testing.T) { rule := scan.Rule{ Service: "service", @@ -71,7 +74,12 @@ func Test_WildcardMatchingOnRequiredLabels(t *testing.T) { reg := rules.Register(rule) defer rules.Deregister(reg) - results := scanHCL(t, test.input) + fsys := testutil.CreateFS(t, map[string]string{ + "main.tf": test.input, + }) + s := New() + results, err := s.ScanFS(context.TODO(), fsys, ".") + require.NoError(t, err) if test.expectedFailure { testutil.AssertRuleFound(t, fmt.Sprintf("custom-service-%s", code), results, "")