diff --git a/docs/docs/coverage/iac/index.md b/docs/docs/coverage/iac/index.md index 963e9b11b8c5..40f0a88023dd 100644 --- a/docs/docs/coverage/iac/index.md +++ b/docs/docs/coverage/iac/index.md @@ -8,17 +8,17 @@ Trivy scans Infrastructure as Code (IaC) files for ## Supported configurations -| Config type | File patterns | -|-------------------------------------|-----------------------------------------------| -| [Kubernetes](kubernetes.md) | \*.yml, \*.yaml, \*.json | -| [Docker](docker.md) | Dockerfile, Containerfile | -| [Terraform](terraform.md) | \*.tf, \*.tf.json, \*.tfvars | -| [Terraform Plan](terraform.md) | tfplan, \*.tfplan, \*.tfplan.json, \*.tf.json | -| [CloudFormation](cloudformation.md) | \*.yml, \*.yaml, \*.json | -| [Azure ARM Template](azure-arm.md) | \*.json | -| [Helm](helm.md) | \*.yaml, \*.tpl, \*.tar.gz, etc. | -| [YAML][json-and-yaml] | \*.yaml, \*.yml | -| [JSON][json-and-yaml] | \*.json | +| Config type | File patterns | +|-------------------------------------|----------------------------------| +| [Kubernetes](kubernetes.md) | \*.yml, \*.yaml, \*.json | +| [Docker](docker.md) | Dockerfile, Containerfile | +| [Terraform](terraform.md) | \*.tf, \*.tf.json, \*.tfvars | +| [Terraform Plan](terraform.md) | tfplan, \*.tfplan, \*.json | +| [CloudFormation](cloudformation.md) | \*.yml, \*.yaml, \*.json | +| [Azure ARM Template](azure-arm.md) | \*.json | +| [Helm](helm.md) | \*.yaml, \*.tpl, \*.tar.gz, etc. | +| [YAML][json-and-yaml] | \*.yaml, \*.yml | +| [JSON][json-and-yaml] | \*.json | [misconf]: ../../scanner/misconfiguration/index.md [secret]: ../../scanner/secret.md diff --git a/pkg/iac/detection/detect.go b/pkg/iac/detection/detect.go index fcfab15f83b2..fcaad6a36179 100644 --- a/pkg/iac/detection/detect.go +++ b/pkg/iac/detection/detect.go @@ -88,12 +88,17 @@ func init() { contents := make(map[string]any) err := json.NewDecoder(r).Decode(&contents) - if err == nil { - if _, ok := contents["terraform_version"]; ok { - _, stillOk := contents["format_version"] - return stillOk + if err != nil { + return false + } + + for _, k := range []string{"terraform_version", "format_version"} { + if _, ok := contents[k]; !ok { + return false } } + + return true } return false } @@ -150,8 +155,7 @@ func init() { return false } - return (sniff.Parameters != nil && len(sniff.Parameters) > 0) || - (sniff.Resources != nil && len(sniff.Resources) > 0) + return len(sniff.Parameters) > 0 || len(sniff.Resources) > 0 } matchers[FileTypeDockerfile] = func(name string, _ io.ReadSeeker) bool { diff --git a/pkg/iac/scanners/terraformplan/tfjson/scanner.go b/pkg/iac/scanners/terraformplan/tfjson/scanner.go index 875644d1dd2a..8b16ecb43116 100644 --- a/pkg/iac/scanners/terraformplan/tfjson/scanner.go +++ b/pkg/iac/scanners/terraformplan/tfjson/scanner.go @@ -6,8 +6,6 @@ import ( "io" "io/fs" - "github.com/bmatcuk/doublestar/v4" - "github.com/aquasecurity/trivy/pkg/iac/framework" "github.com/aquasecurity/trivy/pkg/iac/scan" "github.com/aquasecurity/trivy/pkg/iac/scanners/options" @@ -17,11 +15,6 @@ import ( "github.com/aquasecurity/trivy/pkg/log" ) -var tfPlanExts = []string{ - "**/*tfplan.json", - "**/*tf.json", -} - type Scanner struct { parser *parser.Parser logger *log.Logger @@ -93,25 +86,32 @@ func (s *Scanner) Name() string { return "Terraform Plan JSON" } -func (s *Scanner) ScanFS(ctx context.Context, inputFS fs.FS, dir string) (scan.Results, error) { - var filesFound []string +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) { + + var results scan.Results - for _, ext := range tfPlanExts { - files, err := doublestar.Glob(inputFS, ext, doublestar.WithFilesOnly()) + walkFn := func(path string, d fs.DirEntry, err error) error { if err != nil { - return nil, fmt.Errorf("unable to scan for terraform plan files: %w", err) + return err } - filesFound = append(filesFound, files...) - } - var results scan.Results - for _, f := range filesFound { - res, err := s.ScanFile(f, inputFS) + if d.IsDir() { + return nil + } + + res, err := s.ScanFile(path, fsys) if err != nil { - return nil, err + return fmt.Errorf("failed to scan %s: %w", path, err) } + results = append(results, res...) + return nil } + + if err := fs.WalkDir(fsys, dir, walkFn); err != nil { + return nil, err + } + return results, nil } @@ -148,7 +148,7 @@ func (s *Scanner) Scan(reader io.Reader) (scan.Results, error) { planFS, err := planFile.ToFS() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert plan to FS: %w", err) } scanner := terraformScanner.New(s.options...) diff --git a/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go b/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go index 678b32e00f48..289147f482dd 100644 --- a/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go +++ b/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go @@ -12,20 +12,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/scanners/options" ) -func Test_TerraformScanner(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - inputFile string - inputRego string - options []options.ScannerOption - }{ - { - name: "old rego metadata", - inputFile: "test/testdata/plan.json", - inputRego: ` -package defsec.abcdefg +const defaultCheck = `package defsec.abcdefg __rego_metadata__ := { "id": "TEST123", @@ -48,48 +35,40 @@ deny[cause] { bucket := input.aws.s3.buckets[_] bucket.name.value == "tfsec-plan-testing" cause := bucket.name -} -`, +}` + +func Test_TerraformScanner(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + inputFile string + check string + options []options.ScannerOption + }{ + { + name: "old rego metadata", + inputFile: "test/testdata/plan.json", + check: defaultCheck, options: []options.ScannerOption{ options.ScannerWithPolicyDirs("rules"), options.ScannerWithRegoOnly(true), - options.ScannerWithEmbeddedPolicies(false)}, + }, }, { name: "with user namespace", inputFile: "test/testdata/plan.json", - inputRego: ` -# METADATA -# title: Bad buckets are bad -# description: Bad buckets are bad because they are not good. -# scope: package -# schemas: -# - input: schema["input"] -# custom: -# avd_id: AVD-TEST-0123 -# severity: CRITICAL -# short_code: very-bad-misconfig -# recommended_action: "Fix the s3 bucket" - -package user.foobar.ABC001 - -deny[cause] { - bucket := input.aws.s3.buckets[_] - bucket.name.value == "tfsec-plan-testing" - cause := bucket.name -} -`, + check: defaultCheck, options: []options.ScannerOption{ options.ScannerWithPolicyDirs("rules"), options.ScannerWithRegoOnly(true), - options.ScannerWithEmbeddedPolicies(false), options.ScannerWithPolicyNamespaces("user"), }, }, { name: "with templated plan json", inputFile: "test/testdata/plan_with_template.json", - inputRego: ` + check: ` # METADATA # title: Bad buckets are bad # description: Bad buckets are bad because they are not good. @@ -113,19 +92,27 @@ deny[cause] { options: []options.ScannerOption{ options.ScannerWithPolicyDirs("rules"), options.ScannerWithRegoOnly(true), - options.ScannerWithEmbeddedPolicies(false), + options.ScannerWithPolicyNamespaces("user"), + }, + }, + { + name: "plan with arbitrary name", + inputFile: "test/testdata/arbitrary_name.json", + check: defaultCheck, + options: []options.ScannerOption{ + options.ScannerWithPolicyDirs("rules"), + options.ScannerWithRegoOnly(true), options.ScannerWithPolicyNamespaces("user"), }, }, } for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { b, _ := os.ReadFile(tc.inputFile) fs := testutil.CreateFS(t, map[string]string{ "/code/main.tfplan.json": string(b), - "/rules/test.rego": tc.inputRego, + "/rules/test.rego": tc.check, }) so := append(tc.options, options.ScannerWithPolicyFilesystem(fs)) diff --git a/pkg/iac/scanners/terraformplan/tfjson/test/testdata/arbitrary_name.json b/pkg/iac/scanners/terraformplan/tfjson/test/testdata/arbitrary_name.json new file mode 100644 index 000000000000..e0109b0325a8 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/test/testdata/arbitrary_name.json @@ -0,0 +1 @@ +{"format_version":"1.2","terraform_version":"1.8.1","planned_values":{"root_module":{"resources":[{"address":"aws_s3_bucket.this","mode":"managed","type":"aws_s3_bucket","name":"this","provider_name":"registry.opentofu.org/hashicorp/aws","schema_version":0,"values":{"bucket":"tfsec-plan-testing","force_destroy":false,"tags":null,"timeouts":null},"sensitive_values":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]}}]}},"resource_drift":[{"address":"aws_security_group.this","mode":"managed","type":"aws_security_group","name":"this","provider_name":"registry.opentofu.org/hashicorp/aws","change":{"actions":["delete"],"before":{"arn":"arn:aws:ec2:us-west-1:145167242158:security-group/sg-0f3ac859864629452","description":"Managed by Terraform","egress":[{"cidr_blocks":[],"description":"","from_port":0,"ipv6_cidr_blocks":[],"prefix_list_ids":[],"protocol":"-1","security_groups":[],"self":false,"to_port":0}],"id":"sg-0f3ac859864629452","ingress":[],"name":"test_sg","name_prefix":"","owner_id":"145167242158","revoke_rules_on_delete":false,"tags":null,"tags_all":{},"timeouts":null,"vpc_id":"vpc-06165c55f5a70fdfa"},"after":null,"after_unknown":{},"before_sensitive":{"egress":[{"cidr_blocks":[],"ipv6_cidr_blocks":[],"prefix_list_ids":[],"security_groups":[]}],"ingress":[],"tags_all":{}},"after_sensitive":false}}],"resource_changes":[{"address":"aws_s3_bucket.this","mode":"managed","type":"aws_s3_bucket","name":"this","provider_name":"registry.opentofu.org/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"bucket":"tfsec-plan-testing","force_destroy":false,"tags":null,"timeouts":null},"after_unknown":{"acceleration_status":true,"acl":true,"arn":true,"bucket_domain_name":true,"bucket_prefix":true,"bucket_regional_domain_name":true,"cors_rule":true,"grant":true,"hosted_zone_id":true,"id":true,"lifecycle_rule":true,"logging":true,"object_lock_configuration":true,"object_lock_enabled":true,"policy":true,"region":true,"replication_configuration":true,"request_payer":true,"server_side_encryption_configuration":true,"tags_all":true,"versioning":true,"website":true,"website_domain":true,"website_endpoint":true},"before_sensitive":false,"after_sensitive":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]}}}],"configuration":{"provider_config":{"aws":{"name":"aws","full_name":"registry.opentofu.org/hashicorp/aws"}},"root_module":{"resources":[{"address":"aws_s3_bucket.this","mode":"managed","type":"aws_s3_bucket","name":"this","provider_config_key":"aws","expressions":{"bucket":{"constant_value":"tfsec-plan-testing"}},"schema_version":0}]}},"timestamp":"2024-08-28T04:27:38Z","errored":false}