From dafbd42c3f1c26917908ab62a3351d8cbf25627c Mon Sep 17 00:00:00 2001 From: Nikita Pivkin Date: Fri, 24 Nov 2023 02:11:13 +0300 Subject: [PATCH] feat(terraform): add support for AWS provider block (#50) * feat(terraform): add support for AWS provider block * chore: bump defsec --- go.mod | 2 +- go.sum | 4 +- internal/adapters/terraform/aws/adapt.go | 4 + .../adapters/terraform/aws/provider/adapt.go | 166 ++++++++++++++++++ .../terraform/aws/provider/adapt_test.go | 129 ++++++++++++++ .../adapters/terraform/google/gke/adapt.go | 8 +- pkg/rego/schemas/cloud.json | 23 ++- pkg/scanners/terraform/scanner_test.go | 82 ++++++++- 8 files changed, 400 insertions(+), 18 deletions(-) create mode 100644 internal/adapters/terraform/aws/provider/adapt.go create mode 100644 internal/adapters/terraform/aws/provider/adapt_test.go diff --git a/go.mod b/go.mod index a489cea4..0193c845 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/BurntSushi/toml v1.3.2 github.com/Masterminds/semver v1.5.0 github.com/apparentlymart/go-cidr v1.1.0 - github.com/aquasecurity/defsec v0.93.2-0.20231117234854-a13ada52a90f + github.com/aquasecurity/defsec v0.93.2-0.20231120220217-6818261529c8 github.com/aquasecurity/trivy-policies v0.6.1-0.20231120231532-f6f2330bf842 github.com/aws/smithy-go v1.14.2 github.com/bmatcuk/doublestar/v4 v4.6.0 diff --git a/go.sum b/go.sum index 065dd77b..42cbca57 100644 --- a/go.sum +++ b/go.sum @@ -238,8 +238,8 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/aquasecurity/defsec v0.93.2-0.20231117234854-a13ada52a90f h1:cO9S78J2eBx9tEIZYwFoousuYWV4DtgQlGsZUusMyNY= -github.com/aquasecurity/defsec v0.93.2-0.20231117234854-a13ada52a90f/go.mod h1:J30VViSgmoW2Ic/6aqVJO2qvuADsmZ3MYuNxPcU6Vt0= +github.com/aquasecurity/defsec v0.93.2-0.20231120220217-6818261529c8 h1:w/Sm2fVtb0Rv1bcLLwsW9j37mNUya8MwzKMcjG9OW/Q= +github.com/aquasecurity/defsec v0.93.2-0.20231120220217-6818261529c8/go.mod h1:J30VViSgmoW2Ic/6aqVJO2qvuADsmZ3MYuNxPcU6Vt0= github.com/aquasecurity/trivy-policies v0.6.1-0.20231120231532-f6f2330bf842 h1:RnxM3eTcwPlA/WBwnmaEpeEk3WOCDcnz7yTIFxVL7us= github.com/aquasecurity/trivy-policies v0.6.1-0.20231120231532-f6f2330bf842/go.mod h1:BmEeSFgmBjo3avCli71736sy0veGcSUzGATupp1MCgA= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= diff --git a/internal/adapters/terraform/aws/adapt.go b/internal/adapters/terraform/aws/adapt.go index a9786350..72e1e1b4 100644 --- a/internal/adapters/terraform/aws/adapt.go +++ b/internal/adapters/terraform/aws/adapt.go @@ -28,6 +28,7 @@ import ( "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/mq" "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/msk" "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/neptune" + "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/provider" "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/rds" "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/redshift" "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/aws/s3" @@ -39,6 +40,9 @@ import ( func Adapt(modules terraform.Modules) aws.AWS { return aws.AWS{ + Meta: aws.Meta{ + TFProviders: provider.Adapt(modules), + }, APIGateway: apigateway.Adapt(modules), Athena: athena.Adapt(modules), Cloudfront: cloudfront.Adapt(modules), diff --git a/internal/adapters/terraform/aws/provider/adapt.go b/internal/adapters/terraform/aws/provider/adapt.go new file mode 100644 index 00000000..b34fc8e7 --- /dev/null +++ b/internal/adapters/terraform/aws/provider/adapt.go @@ -0,0 +1,166 @@ +package provider + +import ( + "github.com/aquasecurity/defsec/pkg/providers/aws" + "github.com/aquasecurity/defsec/pkg/terraform" + "github.com/aquasecurity/defsec/pkg/types" +) + +const ( + defaultMaxRetires = 25 + defaultSharedConfigFile = "~/.aws/config" + //#nosec G101 -- False positive + defaultSharedCredentialsFile = "~/.aws/credentials" +) + +func Adapt(modules terraform.Modules) []aws.TerraformProvider { + return adaptProviders(modules) +} + +func adaptProviders(modules terraform.Modules) []aws.TerraformProvider { + var providers []aws.TerraformProvider + for _, providerBlock := range modules.GetBlocks().OfType("provider") { + if providerBlock.Label() == "aws" { + providers = append(providers, adaptProvider(providerBlock)) + } + } + + return providers +} + +func adaptProvider(b *terraform.Block) aws.TerraformProvider { + return aws.TerraformProvider{ + Metadata: b.GetMetadata(), + Alias: getStringAttrValue("alias", b), + Version: getStringAttrValue("version", b), + AccessKey: getStringAttrValue("access_key", b), + AllowedAccountsIDs: b.GetAttribute("allowed_account_ids").AsStringValueSliceOrEmpty(), + AssumeRole: adaptAssumeRole(b), + AssumeRoleWithWebIdentity: adaptAssumeRoleWithWebIdentity(b), + CustomCABundle: getStringAttrValue("custom_ca_bundle", b), + DefaultTags: adaptDefaultTags(b), + EC2MetadataServiceEndpoint: getStringAttrValue("ec2_metadata_service_endpoint", b), + EC2MetadataServiceEndpointMode: getStringAttrValue("ec2_metadata_service_endpoint_mode", b), + Endpoints: adaptEndpoints(b), + ForbiddenAccountIDs: b.GetAttribute("forbidden_account_ids").AsStringValueSliceOrEmpty(), + HttpProxy: getStringAttrValue("http_proxy", b), + IgnoreTags: adaptIgnoreTags(b), + Insecure: b.GetAttribute("insecure").AsBoolValueOrDefault(false, b), + MaxRetries: b.GetAttribute("max_retries").AsIntValueOrDefault(defaultMaxRetires, b), + Profile: getStringAttrValue("profile", b), + Region: getStringAttrValue("region", b), + RetryMode: getStringAttrValue("retry_mode", b), + S3UsePathStyle: b.GetAttribute("s3_use_path_style").AsBoolValueOrDefault(false, b), + S3USEast1RegionalEndpoint: getStringAttrValue("s3_us_east_1_regional_endpoint", b), + SecretKey: getStringAttrValue("secret_key", b), + SharedConfigFiles: b.GetAttribute("shared_config_files").AsStringValuesOrDefault(b, defaultSharedConfigFile), + SharedCredentialsFiles: b.GetAttribute("shared_credentials_files").AsStringValuesOrDefault(b, defaultSharedCredentialsFile), + SkipCredentialsValidation: b.GetAttribute("skip_credentials_validation").AsBoolValueOrDefault(false, b), + SkipMetadataAPICheck: b.GetAttribute("skip_metadata_api_check").AsBoolValueOrDefault(false, b), + SkipRegionValidation: b.GetAttribute("skip_region_validation").AsBoolValueOrDefault(false, b), + SkipRequestingAccountID: b.GetAttribute("skip_requesting_account_id").AsBoolValueOrDefault(false, b), + STSRegion: getStringAttrValue("sts_region", b), + Token: getStringAttrValue("token", b), + UseDualstackEndpoint: b.GetAttribute("use_dualstack_endpoint").AsBoolValueOrDefault(false, b), + UseFIPSEndpoint: b.GetAttribute("use_fips_endpoint").AsBoolValueOrDefault(false, b), + } +} + +func adaptAssumeRole(p *terraform.Block) aws.AssumeRole { + assumeRoleBlock := p.GetBlock("assume_role") + + if assumeRoleBlock.IsNil() { + return aws.AssumeRole{ + Metadata: p.GetMetadata(), + Duration: types.StringDefault("", p.GetMetadata()), + ExternalID: types.StringDefault("", p.GetMetadata()), + Policy: types.StringDefault("", p.GetMetadata()), + RoleARN: types.StringDefault("", p.GetMetadata()), + SessionName: types.StringDefault("", p.GetMetadata()), + SourceIdentity: types.StringDefault("", p.GetMetadata()), + } + } + + return aws.AssumeRole{ + Metadata: assumeRoleBlock.GetMetadata(), + Duration: getStringAttrValue("duration", p), + ExternalID: getStringAttrValue("external_id", p), + Policy: getStringAttrValue("policy", p), + PolicyARNs: p.GetAttribute("policy_arns").AsStringValueSliceOrEmpty(), + RoleARN: getStringAttrValue("role_arn", p), + SessionName: getStringAttrValue("session_name", p), + SourceIdentity: getStringAttrValue("source_identity", p), + Tags: p.GetAttribute("tags").AsMapValue(), + TransitiveTagKeys: p.GetAttribute("transitive_tag_keys").AsStringValueSliceOrEmpty(), + } +} + +func adaptAssumeRoleWithWebIdentity(p *terraform.Block) aws.AssumeRoleWithWebIdentity { + block := p.GetBlock("assume_role_with_web_identity") + if block.IsNil() { + return aws.AssumeRoleWithWebIdentity{ + Metadata: p.GetMetadata(), + Duration: types.StringDefault("", p.GetMetadata()), + Policy: types.StringDefault("", p.GetMetadata()), + RoleARN: types.StringDefault("", p.GetMetadata()), + SessionName: types.StringDefault("", p.GetMetadata()), + WebIdentityToken: types.StringDefault("", p.GetMetadata()), + WebIdentityTokenFile: types.StringDefault("", p.GetMetadata()), + } + } + + return aws.AssumeRoleWithWebIdentity{ + Metadata: block.GetMetadata(), + Duration: getStringAttrValue("duration", p), + Policy: getStringAttrValue("policy", p), + PolicyARNs: p.GetAttribute("policy_arns").AsStringValueSliceOrEmpty(), + RoleARN: getStringAttrValue("role_arn", p), + SessionName: getStringAttrValue("session_name", p), + WebIdentityToken: getStringAttrValue("web_identity_token", p), + WebIdentityTokenFile: getStringAttrValue("web_identity_token_file", p), + } +} + +func adaptEndpoints(p *terraform.Block) types.MapValue { + block := p.GetBlock("endpoints") + if block.IsNil() { + return types.MapDefault(make(map[string]string), p.GetMetadata()) + } + + values := make(map[string]string) + + for name, attr := range block.Attributes() { + values[name] = attr.AsStringValueOrDefault("", block).Value() + } + + return types.Map(values, block.GetMetadata()) +} + +func adaptDefaultTags(p *terraform.Block) aws.DefaultTags { + attr, _ := p.GetNestedAttribute("default_tags.tags") + if attr.IsNil() { + return aws.DefaultTags{} + } + + return aws.DefaultTags{ + Metadata: attr.GetMetadata(), + Tags: attr.AsMapValue(), + } +} + +func adaptIgnoreTags(p *terraform.Block) aws.IgnoreTags { + block := p.GetBlock("ignore_tags") + if block.IsNil() { + return aws.IgnoreTags{} + } + + return aws.IgnoreTags{ + Metadata: block.GetMetadata(), + Keys: block.GetAttribute("keys").AsStringValueSliceOrEmpty(), + KeyPrefixes: block.GetAttribute("key_prefixes").AsStringValueSliceOrEmpty(), + } +} + +func getStringAttrValue(name string, parent *terraform.Block) types.StringValue { + return parent.GetAttribute(name).AsStringValueOrDefault("", parent) +} diff --git a/internal/adapters/terraform/aws/provider/adapt_test.go b/internal/adapters/terraform/aws/provider/adapt_test.go new file mode 100644 index 00000000..ec0d0469 --- /dev/null +++ b/internal/adapters/terraform/aws/provider/adapt_test.go @@ -0,0 +1,129 @@ +package provider + +import ( + "testing" + + "github.com/aquasecurity/defsec/pkg/providers/aws" + "github.com/aquasecurity/defsec/pkg/types" + + "github.com/aquasecurity/trivy-iac/internal/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy-iac/test/testutil" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected []aws.TerraformProvider + }{ + { + name: "happy", + source: ` +variable "s3_use_path_style" { + default = true +} + +provider "aws" { + version = "~> 5.0" + region = "us-east-1" + profile = "localstack" + + access_key = "fake" + secret_key = "fake" + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + s3_use_path_style = var.s3_use_path_style + + endpoints { + dynamodb = "http://localhost:4566" + s3 = "http://localhost:4566" + } + + default_tags { + tags = { + Environment = "Local" + Name = "LocalStack" + } + } +}`, + expected: []aws.TerraformProvider{ + { + Version: types.String("~> 5.0", types.NewTestMetadata()), + Region: types.String("us-east-1", types.NewTestMetadata()), + DefaultTags: aws.DefaultTags{ + Metadata: types.NewTestMetadata(), + Tags: types.Map(map[string]string{ + "Environment": "Local", + "Name": "LocalStack", + }, types.NewTestMetadata()), + }, + Endpoints: types.Map(map[string]string{ + "dynamodb": "http://localhost:4566", + "s3": "http://localhost:4566", + }, types.NewTestMetadata()), + Profile: types.String("localstack", types.NewTestMetadata()), + AccessKey: types.String("fake", types.NewTestMetadata()), + SecretKey: types.String("fake", types.NewTestMetadata()), + SkipCredentialsValidation: types.Bool(true, types.NewTestMetadata()), + SkipMetadataAPICheck: types.Bool(true, types.NewTestMetadata()), + SkipRequestingAccountID: types.Bool(true, types.NewTestMetadata()), + S3UsePathStyle: types.Bool(true, types.NewTestMetadata()), + MaxRetries: types.IntDefault(defaultMaxRetires, types.NewTestMetadata()), + SharedConfigFiles: types.StringValueList{ + types.StringDefault(defaultSharedConfigFile, types.NewTestMetadata()), + }, + SharedCredentialsFiles: types.StringValueList{ + types.StringDefault(defaultSharedCredentialsFile, types.NewTestMetadata()), + }, + }, + }, + }, + { + name: "multiply provider configurations", + source: ` + +provider "aws" { + region = "us-east-1" +} + +provider "aws" { + alias = "west" + region = "us-west-2" +} +`, + expected: []aws.TerraformProvider{ + { + Region: types.String("us-east-1", types.NewTestMetadata()), + Endpoints: types.Map(make(map[string]string), types.NewTestMetadata()), + MaxRetries: types.IntDefault(defaultMaxRetires, types.NewTestMetadata()), + SharedConfigFiles: types.StringValueList{ + types.StringDefault(defaultSharedConfigFile, types.NewTestMetadata()), + }, + SharedCredentialsFiles: types.StringValueList{ + types.StringDefault(defaultSharedCredentialsFile, types.NewTestMetadata()), + }, + }, + { + Alias: types.String("west", types.NewTestMetadata()), + Region: types.String("us-west-2", types.NewTestMetadata()), + Endpoints: types.Map(make(map[string]string), types.NewTestMetadata()), + MaxRetries: types.IntDefault(defaultMaxRetires, types.NewTestMetadata()), + SharedConfigFiles: types.StringValueList{ + types.StringDefault(defaultSharedConfigFile, types.NewTestMetadata()), + }, + SharedCredentialsFiles: types.StringValueList{ + types.StringDefault(defaultSharedCredentialsFile, types.NewTestMetadata()), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.source, ".tf") + testutil.AssertDefsecEqual(t, test.expected, Adapt(modules)) + }) + } +} diff --git a/internal/adapters/terraform/google/gke/adapt.go b/internal/adapters/terraform/google/gke/adapt.go index 8528f98b..6703671f 100644 --- a/internal/adapters/terraform/google/gke/adapt.go +++ b/internal/adapters/terraform/google/gke/adapt.go @@ -141,13 +141,7 @@ func (a *adapter) adaptCluster(resource *terraform.Block, module *terraform.Modu resourceLabelsAttr := resource.GetAttribute("resource_labels") if resourceLabelsAttr.IsNotNil() { - resourceLabels := make(map[string]string) - _ = resourceLabelsAttr.Each(func(key, val cty.Value) { - if key.Type() == cty.String && val.Type() == cty.String { - resourceLabels[key.AsString()] = val.AsString() - } - }) - cluster.ResourceLabels = defsecTypes.Map(resourceLabels, resourceLabelsAttr.GetMetadata()) + cluster.ResourceLabels = resourceLabelsAttr.AsMapValue() } cluster.RemoveDefaultNodePool = resource.GetAttribute("remove_default_node_pool").AsBoolValueOrDefault(false, resource) diff --git a/pkg/rego/schemas/cloud.json b/pkg/rego/schemas/cloud.json index 0518601a..d6ca8b87 100644 --- a/pkg/rego/schemas/cloud.json +++ b/pkg/rego/schemas/cloud.json @@ -138,6 +138,10 @@ "type": "object", "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.lambda.Lambda" }, + "meta": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.Meta" + }, "mq": { "type": "object", "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.mq.MQ" @@ -150,13 +154,6 @@ "type": "object", "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.neptune.Neptune" }, - "providers": { - "type": "array", - "items": { - "type": "object", - "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.TerraformProvider" - } - }, "rds": { "type": "object", "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.rds.RDS" @@ -302,6 +299,18 @@ } } }, + "github.com.aquasecurity.defsec.pkg.providers.aws.Meta": { + "type": "object", + "properties": { + "tfproviders": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.defsec.pkg.providers.aws.TerraformProvider" + } + } + } + }, "github.com.aquasecurity.defsec.pkg.providers.aws.TerraformProvider": { "type": "object", "properties": { diff --git a/pkg/scanners/terraform/scanner_test.go b/pkg/scanners/terraform/scanner_test.go index 314f0a9c..e7f9647e 100644 --- a/pkg/scanners/terraform/scanner_test.go +++ b/pkg/scanners/terraform/scanner_test.go @@ -1258,7 +1258,7 @@ deny[res] { policy.name.value == "bad-policy" res := result.new("Deny!", policy) } - `, +`, }) debugLog := bytes.NewBuffer([]byte{}) @@ -1278,3 +1278,83 @@ deny[res] { assert.Len(t, results, 1) assert.Len(t, results.GetFailed(), 1) } + +func Test_RegoRefToAwsProviderAttributes(t *testing.T) { + fs := testutil.CreateFS(t, map[string]string{ + "code/providers.tf": ` +provider "aws" { + region = "us-east-2" + default_tags { + tags = { + Environment = "Local" + Name = "LocalStack" + } + } +} +`, + "rules/region.rego": ` +# METADATA +# schemas: +# - input: schema.input +# custom: +# avd_id: AVD-AWS-0001 +# input: +# selector: +# - type: cloud +# subtypes: +# - service: meta +# provider: aws +package defsec.test.aws1 +deny[res] { + region := input.aws.meta.tfproviders[_].region + region.value != "us-east-1" + res := result.new("Only the 'us-east-1' region is allowed!", region) +} +`, + "rules/tags.rego": ` +# METADATA +# schemas: +# - input: schema.input +# custom: +# avd_id: AVD-AWS-0002 +# input: +# selector: +# - type: cloud +# subtypes: +# - service: meta +# provider: aws +package defsec.test.aws2 +deny[res] { + provider := input.aws.meta.tfproviders[_] + tags = provider.defaulttags.tags.value + not tags.Environment + res := result.new("provider should have the following default tags: 'Environment'", tags) +}`, + }) + + debugLog := bytes.NewBuffer([]byte{}) + scanner := New( + options.ScannerWithDebug(debugLog), + options.ScannerWithPolicyDirs("rules"), + options.ScannerWithPolicyFilesystem(fs), + options.ScannerWithRegoOnly(true), + options.ScannerWithEmbeddedLibraries(false), + options.ScannerWithEmbeddedPolicies(false), + ScannerWithAllDirectories(true), + ) + + results, err := scanner.ScanFS(context.TODO(), fs, "code") + require.NoError(t, err) + + require.Len(t, results, 2) + + require.Len(t, results.GetFailed(), 1) + assert.Equal(t, "AVD-AWS-0001", results.GetFailed()[0].Rule().AVDID) + + require.Len(t, results.GetPassed(), 1) + assert.Equal(t, "AVD-AWS-0002", results.GetPassed()[0].Rule().AVDID) + + if t.Failed() { + fmt.Printf("Debug logs:\n%s\n", debugLog.String()) + } +}