test: replace Go checks with Rego (#7867)

Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
Nikita Pivkin
2024-11-26 04:04:53 +06:00
committed by GitHub
parent e9a899a3cf
commit 5a93a7736b
11 changed files with 761 additions and 1538 deletions

View File

@@ -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 {

View File

@@ -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
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,
results := scanHCL(t, test.source,
rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)),
rego.WithPolicyNamespaces("user"),
)
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()
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")
}
})
}

View File

@@ -1,26 +1,19 @@
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
resource "aws_s3_bucket" "test" {
for_each = other.thing
}
`,
@@ -40,12 +33,11 @@ locals {
})
for i := 0; i < 100; i++ {
p := parser.New(fs, "", parser.OptionStopOnHCLError(true))
err := p.ParseFS(context.TODO(), ".")
results, err := scanFS(fsys, ".",
rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)),
rego.WithPolicyNamespaces("user"),
)
require.NoError(t, err)
modules, _, err := p.EvaluateAll(context.TODO())
require.NoError(t, err)
results, _ := executor.New().Execute(modules)
require.Len(t, results.GetFailed(), 2)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,11 @@
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) {
@@ -16,7 +13,7 @@ func TestScanningJSON(t *testing.T) {
var tests = []struct {
name string
source string
shouldFail bool
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")
}
})
}

View File

@@ -1,51 +1,25 @@
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
},
},
},
}
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.
func Test_GoCtyCompatibilityIssue(t *testing.T) {
registered := rules.Register(badRule)
defer rules.Deregister(registered)
fs := testutil.CreateFS(t, map[string]string{
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,192 +48,91 @@ 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{
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{
resource "aws_s3_bucket" "test" {}`,
},
expected: true,
},
{
name: "ignore misconfig in module",
files: map[string]string{
"/project/main.tf": `
#tfsec:ignore:aws-service-abc
#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{
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{
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{
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
variable "bucket" {}
resource "aws_s3_bucket" "test" {
bucket = var.bucket
}
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, "")
}
func Test_ProblemInNestedModule(t *testing.T) {
registered := rules.Register(badRule)
defer rules.Deregister(registered)
fs := testutil.CreateFS(t, map[string]string{
`},
expected: true,
},
{
name: "misconfig in nested module",
files: map[string]string{
"project/main.tf": `
module "something" {
source = "../modules/a"
@@ -277,113 +148,74 @@ 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{
"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
}
variable "bucket" {}
module "something" {
source = "../../modules/b"
bad = var.bad
bucket = var.bucket
}
`,
"modules/b/main.tf": `
variable "bad" {
default = false
}
variable "bucket" {}
module "something" {
source = "../c"
bad = var.bad
bucket = var.bad
}
`,
"modules/c/main.tf": `
variable "bad" {
default = false
}
resource "problem" "uhoh" {
bad = var.bad
variable "bucket" {}
resource "aws_s3_bucket" "test" {
bucket = var.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, "")
}
func Test_ProblemInInitialisedModule(t *testing.T) {
registered := rules.Register(badRule)
defer rules.Deregister(registered)
fs := testutil.CreateFS(t, map[string]string{
},
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": `
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 "bad" {
default = false
}
resource "problem" "uhoh" {
bad = var.bad
variable "bucket" {}
resource "aws_s3_bucket" "test" {
bucket = var.bucket
}
`,
"project/.terraform/modules/modules.json": `
@@ -392,273 +224,157 @@ resource "problem" "uhoh" {
{"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{
},
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
variable "bucket" {}
resource "aws_s3_bucket" "test" {
bucket = var.bucket
}
`,
"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{
},
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" {
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" {
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" {
variable "s3_bucket_count" {
default = 0
}
resource "problem" "uhoh" {
count = var.bad
bad = true
resource "aws_s3_bucket" "test" {
count = var.s3_bucket_count
}
`,
})
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_Dynamic_Variables(t *testing.T) {
fs := testutil.CreateFS(t, map[string]string{
},
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
resource "aws_s3_bucket" "test" {
secure = something.this.blah[0].bucket
}
`})
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
`},
expected: true,
},
},
},
}
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, "")
}
func Test_Dynamic_Variables_FalsePositive(t *testing.T) {
fs := testutil.CreateFS(t, map[string]string{
{
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) {
fs := testutil.CreateFS(t, map[string]string{
{
name: "references passed to nested module",
files: 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" {
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")
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)
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, "")
if tt.expected {
testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "")
} else {
testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "")
}
})
}
}

View File

@@ -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))
}
}
}

View File

@@ -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),

View File

@@ -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),

View File

@@ -2,6 +2,7 @@ package terraform
import (
"context"
"io/fs"
"testing"
"github.com/stretchr/testify/require"
@@ -14,6 +15,68 @@ 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)
}
`
var enforceGroupMfaCheck = `# METADATA
# schemas:
# - input: schema["cloud"]
# custom:
# id: USER-TEST-0124
# aliases:
# - aws-iam-enforce-mfa
# provider: aws
# service: iam
# short_code: enforce-group-mfa
# input:
# selector:
# - type: cloud
# subtypes:
# - service: iam
# provider: aws
package user.test124
import rego.v1
deny contains res if {
some group in input.aws.iam.groups
not is_group_mfa_enforced(group)
res := result.new("Multi-Factor authentication is not enforced for group", group)
}
is_group_mfa_enforced(group) if {
some policy in group.policies
value := json.unmarshal(policy.document.value)
some condition in value.Statement[_].Condition
some key, _ in condition
key == "aws:MultiFactorAuthPresent"
}
`
func createModulesFromSource(t *testing.T, source, ext string) terraform.Modules {
fs := testutil.CreateFS(t, map[string]string{
"source" + ext: source,
@@ -30,30 +93,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
}

View File

@@ -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, "")