Repository: aquasecurity/tfsec
Pull request #1355 (2022-01-17 18:36:24)

Add adapter for Digital Ocean Kubernetes
`go mod vendor` is a bit weird to me. I tried to use `defsec == 0.3.20`, but that caused the AWS IAM adapters to break.

I'm not really sure about tests for this, and if it should be in defsec, or here. But I guess that's second to fetching the right dependencies. Keeping all of your dependencies in the repository, versus just having a file listing them (e.g., in Python) is one aspect of GoLang that's a bit peculiar to me, but I see positives of it.

liamg: @atombrella Awesome, thanks! You should be able to rebase this on master now and defsec should work :crossed_fingers: 

Patch:
diff --git a/docs/getting-started/configuration/custom-checks.md b/docs/getting-started/configuration/custom-checks.md
index 20cd931be..344d02b6a 100644
--- a/docs/getting-started/configuration/custom-checks.md
+++ b/docs/getting-started/configuration/custom-checks.md
@@ -98,14 +98,15 @@ The check contains up of the following attributes;
 
 The `MatchSpec` is the what will define the check itself - this is fairly basic and is made up of the following attributes
 
-| Attribute          | Description                                                                                        |
-| :----------------- | :------------------------------------------------------------------------------------------------- |
-| name               | The name of the attribute or block to run the check on                                             |
-| action             | The check type - see below for more information                                                    |
-| value              | In cases where a value is required, the value to look for                                          |
-| ignoreUndefined    | If the attribute is undefined, ignore and pass the check                                           |
-| subMatch           | A sub MatchSpec block for nested checking - think looking for `enabled` value in a `logging` block |
-| predicateMatchSpec | An array of MatchSpec blocks to be logically aggregated by either `and` or `or` actions            |
+| Attribute          | Description                                                                                                                             |
+| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
+| name               | The name of the attribute or block to run the check on                                                                                  |
+| action             | The check type - see below for more information                                                                                         |
+| value              | In cases where a value is required, the value to look for, text matching `TFSEC_VAR_{VAR_NAME}` will be replace with the variable value |
+| ignoreUndefined    | If the attribute is undefined, ignore and pass the check                                                                                |
+| subMatch           | A sub MatchSpec block for nested checking - think looking for `enabled` value in a `logging` block                                      |
+| predicateMatchSpec | An array of MatchSpec blocks to be logically aggregated by either `and` or `or` actions                                                 |
+| assignVariable     | The name of the "variable" to store the value of the `name` attribute in, has to by uppercase and start with `TFSEC_VAR_`               |
 
 #### Check Actions
 There are a number of `CheckActions` available which should allow you to quickly put together most checks.
diff --git a/go.mod b/go.mod
index d28ff2eee..6164f793a 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.17
 
 require (
 	github.com/apparentlymart/go-cidr v1.1.0
-	github.com/aquasecurity/defsec v0.3.21
+	github.com/aquasecurity/defsec v0.3.22
 	github.com/bmatcuk/doublestar v1.3.4
 	github.com/google/go-cmp v0.5.6
 	github.com/google/uuid v1.3.0
diff --git a/go.sum b/go.sum
index 659cd9b2e..d0f551e9c 100644
--- a/go.sum
+++ b/go.sum
@@ -76,6 +76,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/aquasecurity/defsec v0.3.21 h1:teUw4IzI7hl94yoO3cK83N6LCPe9hxRo8deC58mQhYc=
 github.com/aquasecurity/defsec v0.3.21/go.mod h1:k69jht4X6WIpU4Q9py0o9hYHs4lnGTsBb64ncWCuQvE=
+github.com/aquasecurity/defsec v0.3.22 h1:0Fe17NpscVydytsqHuSLvSz18WFtB6Pt4nqhcwMEz3E=
+github.com/aquasecurity/defsec v0.3.22/go.mod h1:k69jht4X6WIpU4Q9py0o9hYHs4lnGTsBb64ncWCuQvE=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
diff --git a/internal/app/tfsec/adapter/azure/keyvault/adapt.go b/internal/app/tfsec/adapter/azure/keyvault/adapt.go
index b666960e7..3a0c35145 100644
--- a/internal/app/tfsec/adapter/azure/keyvault/adapt.go
+++ b/internal/app/tfsec/adapter/azure/keyvault/adapt.go
@@ -1,10 +1,102 @@
 package keyvault
 
 import (
+	"time"
+
 	"github.com/aquasecurity/defsec/provider/azure/keyvault"
+	"github.com/aquasecurity/defsec/types"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 )
 
 func Adapt(modules []block.Module) keyvault.KeyVault {
-	return keyvault.KeyVault{}
+	return keyvault.KeyVault{
+		Vaults: adaptVaults(modules),
+	}
+}
+
+func adaptVaults(modules []block.Module) []keyvault.Vault {
+	var vaults []keyvault.Vault
+	for _, module := range modules {
+		for _, resource := range module.GetResourcesByType("azurerm_key_vault") {
+			vaults = append(vaults, adaptVault(resource, module))
+
+		}
+	}
+	return vaults
+}
+
+func adaptVault(resource block.Block, module block.Module) keyvault.Vault {
+	var keys []keyvault.Key
+	var secrets []keyvault.Secret
+
+	defaultActionVal := types.StringDefault("", *resource.GetMetadata())
+
+	secretBlocks := module.GetReferencingResources(resource, "azurerm_key_vault_secret", "key_vault_id")
+	for _, secretBlock := range secretBlocks {
+		secrets = append(secrets, adaptSecret(secretBlock))
+	}
+
+	keyBlocks := module.GetReferencingResources(resource, "azurerm_key_vault_key", "key_vault_id")
+	for _, keyBlock := range keyBlocks {
+		keys = append(keys, adaptKey(keyBlock))
+	}
+
+	purgeProtectionAttr := resource.GetAttribute("purge_protection_enabled")
+	purgeProtectionVal := purgeProtectionAttr.AsBoolValueOrDefault(false, resource)
+
+	softDeleteRetentionDaysAttr := resource.GetAttribute("soft_delete_retention_days")
+	softDeleteRetentionDaysVal := softDeleteRetentionDaysAttr.AsIntValueOrDefault(0, resource)
+
+	if resource.HasChild("network_acls") {
+		defaultActionAttr := resource.GetBlock("network_acls").GetAttribute("default_action")
+		defaultActionVal = defaultActionAttr.AsStringValueOrDefault("", resource.GetBlock("network_acls"))
+	}
+
+	return keyvault.Vault{
+		Secrets:                 secrets,
+		Keys:                    keys,
+		EnablePurgeProtection:   purgeProtectionVal,
+		SoftDeleteRetentionDays: softDeleteRetentionDaysVal,
+		NetworkACLs: keyvault.NetworkACLs{
+			DefaultAction: defaultActionVal,
+		},
+	}
+}
+
+func adaptSecret(resource block.Block) keyvault.Secret {
+	contentTypeAttr := resource.GetAttribute("content_type")
+	contentTypeVal := contentTypeAttr.AsStringValueOrDefault("", resource)
+
+	expiryDateAttr := resource.GetAttribute("expiration_date")
+	expiryDateVal := types.TimeDefault(time.Time{}, resource.GetMetadata())
+
+	if expiryDateAttr.IsNotNil() {
+		expiryDateString := expiryDateAttr.Value().AsString()
+		layout := "2006-01-02T15:04:00Z"
+		if expiryDate, err := time.Parse(layout, expiryDateString); err == nil {
+			expiryDateVal = types.Time(expiryDate, expiryDateAttr.GetMetadata())
+		}
+	}
+
+	return keyvault.Secret{
+		ContentType: contentTypeVal,
+		ExpiryDate:  expiryDateVal,
+	}
+}
+
+func adaptKey(resource block.Block) keyvault.Key {
+	expiryDateAttr := resource.GetAttribute("expiration_date")
+	expiryDateVal := types.TimeDefault(time.Time{}, resource.GetMetadata())
+
+	if expiryDateAttr.IsNotNil() {
+		expiryDateString := expiryDateAttr.Value().AsString()
+		layout := "2006-01-02T15:04:00Z"
+		if expiryDate, err := time.Parse(layout, expiryDateString); err == nil {
+			expiryDateVal = types.Time(expiryDate, expiryDateAttr.GetMetadata())
+		}
+	}
+
+	return keyvault.Key{
+		ExpiryDate: expiryDateVal,
+	}
 }
diff --git a/internal/app/tfsec/adapter/digitalocean/compute/adapt.go b/internal/app/tfsec/adapter/digitalocean/compute/adapt.go
index 99ebf68ce..bdc1474f3 100644
--- a/internal/app/tfsec/adapter/digitalocean/compute/adapt.go
+++ b/internal/app/tfsec/adapter/digitalocean/compute/adapt.go
@@ -8,9 +8,10 @@ import (
 
 func Adapt(modules []block.Module) compute.Compute {
 	return compute.Compute{
-		Droplets:      adaptDroplets(modules),
-		Firewalls:     adaptFirewalls(modules),
-		LoadBalancers: adaptLoadBalancers(modules),
+		Droplets:           adaptDroplets(modules),
+		Firewalls:          adaptFirewalls(modules),
+		LoadBalancers:      adaptLoadBalancers(modules),
+		KubernetesClusters: adaptKubernetesClusters(modules),
 	}
 }
 
@@ -93,3 +94,13 @@ func adaptLoadBalancers(module block.Modules) (loadBalancers []compute.LoadBalan
 
 	return loadBalancers
 }
+
+func adaptKubernetesClusters(module block.Modules) (kubernetesClusters []compute.KubernetesCluster) {
+	for _, block := range module.GetResourcesByType("digitalocean_kubernetes_cluster") {
+		kubernetesClusters = append(kubernetesClusters, compute.KubernetesCluster{
+			AutoUpgrade:  block.GetAttribute("auto_upgrade").AsBoolValueOrDefault(false, block),
+			SurgeUpgrade: block.GetAttribute("surge_upgrade").AsBoolValueOrDefault(false, block),
+		})
+	}
+	return kubernetesClusters
+}
diff --git a/internal/app/tfsec/custom/custom_check.go b/internal/app/tfsec/custom/custom_check.go
index a573001ef..16bfdd5ce 100644
--- a/internal/app/tfsec/custom/custom_check.go
+++ b/internal/app/tfsec/custom/custom_check.go
@@ -112,6 +112,7 @@ type MatchSpec struct {
 	SubMatch           *MatchSpec  `json:"subMatch,omitempty" yaml:"subMatch,omitempty"`
 	IgnoreUndefined    bool        `json:"ignoreUndefined,omitempty" yaml:"ignoreUndefined,omitempty"`
 	IgnoreUnmatched    bool        `json:"ignoreUnmatched,omitempty" yaml:"ignoreUnmatched,omitempty"`
+	AssignVariable     string      `json:"assignVariable,omitempty" yaml:"assignVariable,omitempty"`
 }
 
 // Check specifies the check definition represented in json/yaml
diff --git a/internal/app/tfsec/custom/custom_context.go b/internal/app/tfsec/custom/custom_context.go
new file mode 100644
index 000000000..10e98834c
--- /dev/null
+++ b/internal/app/tfsec/custom/custom_context.go
@@ -0,0 +1,31 @@
+package custom
+
+import "github.com/aquasecurity/tfsec/internal/app/tfsec/block"
+
+type customCheckVariables map[string]string
+
+type customContext struct {
+	module    block.Module
+	variables customCheckVariables
+}
+
+func NewEmptyCustomContext() *customContext {
+	return &customContext{
+		module:    nil,
+		variables: make(customCheckVariables),
+	}
+}
+
+func NewCustomContext(module block.Module) *customContext {
+	return &customContext{
+		module:    module,
+		variables: make(customCheckVariables),
+	}
+}
+
+func NewCustomContextWithVariables(module block.Module, variables customCheckVariables) *customContext {
+	return &customContext{
+		module:    module,
+		variables: variables,
+	}
+}
diff --git a/internal/app/tfsec/custom/processing.go b/internal/app/tfsec/custom/processing.go
index cdda01653..8e314989f 100644
--- a/internal/app/tfsec/custom/processing.go
+++ b/internal/app/tfsec/custom/processing.go
@@ -2,6 +2,7 @@ package custom
 
 import (
 	"fmt"
+	"regexp"
 
 	"github.com/aquasecurity/defsec/provider"
 	"github.com/aquasecurity/defsec/rules"
@@ -11,12 +12,14 @@ import (
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
 
-var matchFunctions = map[CheckAction]func(block.Block, *MatchSpec, block.Module) bool{
-	IsPresent: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+var matchFunctions = map[CheckAction]func(block.Block, *MatchSpec, *customContext) bool{
+	IsPresent: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		return block.HasChild(spec.Name) || spec.IgnoreUndefined
 	},
-	NotPresent: func(block block.Block, spec *MatchSpec, module block.Module) bool { return !block.HasChild(spec.Name) },
-	IsEmpty: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	NotPresent: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		return !block.HasChild(spec.Name)
+	},
+	IsEmpty: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		if block.MissingChild(spec.Name) {
 			return true
 		}
@@ -28,95 +31,97 @@ var matchFunctions = map[CheckAction]func(block.Block, *MatchSpec, block.Module)
 		childBlock := block.GetBlock(spec.Name)
 		return childBlock.IsEmpty()
 	},
-	StartsWith: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	StartsWith: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.StartsWith(spec.MatchValue)
+		return attribute.StartsWith(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	EndsWith: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	EndsWith: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.EndsWith(spec.MatchValue)
+		return attribute.EndsWith(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	Contains: func(b block.Block, spec *MatchSpec, module block.Module) bool {
+	Contains: func(b block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.Contains(spec.MatchValue, block.IgnoreCase)
+		return attribute.Contains(processMatchValueVariables(spec.MatchValue, customCtx.variables), block.IgnoreCase)
 	},
-	NotContains: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	NotContains: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return !attribute.Contains(spec.MatchValue)
+		return !attribute.Contains(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	Equals: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	Equals: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.Equals(spec.MatchValue)
+		return attribute.Equals(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	NotEqual: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	NotEqual: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.NotEqual(spec.MatchValue)
+		return attribute.NotEqual(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	LessThan: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	LessThan: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.LessThan(spec.MatchValue)
+		return attribute.LessThan(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	LessThanOrEqualTo: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	LessThanOrEqualTo: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.LessThanOrEqualTo(spec.MatchValue)
+		return attribute.LessThanOrEqualTo(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	GreaterThan: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	GreaterThan: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.GreaterThan(spec.MatchValue)
+		return attribute.GreaterThan(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	GreaterThanOrEqualTo: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	GreaterThanOrEqualTo: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.GreaterThanOrEqualTo(spec.MatchValue)
+		return attribute.GreaterThanOrEqualTo(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	RegexMatches: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	RegexMatches: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.RegexMatches(spec.MatchValue)
+		return attribute.RegexMatches(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	IsAny: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	IsAny: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
-		return attribute != nil && attribute.IsAny(unpackInterfaceToInterfaceSlice(spec.MatchValue)...)
+		return attribute != nil && attribute.IsAny(unpackInterfaceToInterfaceSlice(processMatchValueVariables(spec.MatchValue, customCtx.variables))...)
 	},
-	IsNone: func(block block.Block, spec *MatchSpec, module block.Module) bool {
+	IsNone: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
 		attribute := block.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.IsNone(unpackInterfaceToInterfaceSlice(spec.MatchValue)...)
+		return attribute.IsNone(unpackInterfaceToInterfaceSlice(processMatchValueVariables(spec.MatchValue, customCtx.variables))...)
+	},
+	RequiresPresence: func(block block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		return resourceFound(spec, customCtx.module)
 	},
-	RequiresPresence: func(block block.Block, spec *MatchSpec, module block.Module) bool { return resourceFound(spec, module) },
 }
 
 func processFoundChecks(checks ChecksFile) {
@@ -143,7 +148,7 @@ func processFoundChecks(checks ChecksFile) {
 				RequiredSources: customCheck.RequiredSources,
 				CheckTerraform: func(rootBlock block.Block, module block.Module) (results rules.Results) {
 					matchSpec := customCheck.MatchSpec
-					if !evalMatchSpec(rootBlock, matchSpec, module) {
+					if !evalMatchSpec(rootBlock, matchSpec, NewCustomContext(module)) {
 						results.Add(
 							fmt.Sprintf("Custom check failed for resource %s. %s", rootBlock.FullName(), customCheck.ErrorMessage),
 							rootBlock,
@@ -156,7 +161,7 @@ func processFoundChecks(checks ChecksFile) {
 	}
 }
 
-func evalMatchSpec(b block.Block, spec *MatchSpec, module block.Module) bool {
+func evalMatchSpec(b block.Block, spec *MatchSpec, customCtx *customContext) bool {
 	if b.IsNil() {
 		return false
 	}
@@ -164,7 +169,7 @@ func evalMatchSpec(b block.Block, spec *MatchSpec, module block.Module) bool {
 
 	for _, preCondition := range spec.PreConditions {
 		clone := preCondition
-		if !evalMatchSpec(b, &clone, module) {
+		if !evalMatchSpec(b, &clone, customCtx) {
 			// precondition not met
 			return true
 		}
@@ -174,51 +179,55 @@ func evalMatchSpec(b block.Block, spec *MatchSpec, module block.Module) bool {
 	case InModule:
 		return b.InModule()
 	case RegexMatches:
-		if !matchFunctions[RegexMatches](b, spec, module) {
+		if !matchFunctions[RegexMatches](b, spec, customCtx) {
 			return spec.IgnoreUnmatched
 		}
 		evalResult = true
 	case HasTag:
-		return checkTags(b, spec, module)
+		return checkTags(b, spec, customCtx.module)
 	case OfType:
 		return ofType(b, spec)
 	case Not:
-		return notifyPredicate(b, spec, module)
+		return notifyPredicate(b, spec, customCtx)
 	case And:
-		return processAndPredicate(spec, b, module)
+		return processAndPredicate(spec, b, customCtx)
 	case Or:
-		return processOrPredicate(spec, b, module)
+		return processOrPredicate(spec, b, customCtx)
 	default:
-		evalResult = matchFunctions[spec.Action](b, spec, module)
+		evalResult = matchFunctions[spec.Action](b, spec, customCtx)
+	}
+
+	if len(spec.AssignVariable) > 0 {
+		customCtx.variables[spec.AssignVariable] = b.GetAttribute(spec.Name).AsStringValueOrDefault("", b).Value()
 	}
 
 	if spec.SubMatch != nil {
-		evalResult = processSubMatches(spec, b, module, evalResult)
+		evalResult = processSubMatches(spec, b, customCtx, evalResult)
 	}
 
 	return evalResult
 }
 
-func notifyPredicate(b block.Block, spec *MatchSpec, module block.Module) bool {
-	return !evalMatchSpec(b, &spec.PredicateMatchSpec[0], module)
+func notifyPredicate(b block.Block, spec *MatchSpec, customCtx *customContext) bool {
+	return !evalMatchSpec(b, &spec.PredicateMatchSpec[0], customCtx)
 }
 
-func processOrPredicate(spec *MatchSpec, b block.Block, module block.Module) bool {
+func processOrPredicate(spec *MatchSpec, b block.Block, customCtx *customContext) bool {
 	for _, childSpec := range spec.PredicateMatchSpec {
 		clone := childSpec
-		if evalMatchSpec(b, &clone, module) {
+		if evalMatchSpec(b, &clone, customCtx) {
 			return true
 		}
 	}
 	return false
 }
 
-func processAndPredicate(spec *MatchSpec, b block.Block, module block.Module) bool {
+func processAndPredicate(spec *MatchSpec, b block.Block, customCtx *customContext) bool {
 	set := make(map[bool]bool)
 
 	for _, childSpec := range spec.PredicateMatchSpec {
 		clone := childSpec
-		result := evalMatchSpec(b, &clone, module)
+		result := evalMatchSpec(b, &clone, customCtx)
 		set[result] = true
 
 	}
@@ -226,16 +235,16 @@ func processAndPredicate(spec *MatchSpec, b block.Block, module block.Module) bo
 	return len(set) == 1 && set[true]
 }
 
-func processSubMatches(spec *MatchSpec, b block.Block, module block.Module, evalResult bool) bool {
+func processSubMatches(spec *MatchSpec, b block.Block, customCtx *customContext, evalResult bool) bool {
 	var subMatchTargets []block.Block
 	switch spec.Action {
 	case RequiresPresence:
-		subMatchTargets = module.GetResourcesByType(spec.Name)
+		subMatchTargets = customCtx.module.GetResourcesByType(spec.Name)
 	default:
 		subMatchTargets = b.GetBlocks(spec.Name)
 	}
 	for _, b := range subMatchTargets {
-		evalResult = evalMatchSpec(b, spec.SubMatch, nil)
+		evalResult = evalMatchSpec(b, spec.SubMatch, customCtx)
 		if !evalResult {
 			break
 		}
@@ -244,6 +253,19 @@ func processSubMatches(spec *MatchSpec, b block.Block, module block.Module, eval
 	return evalResult
 }
 
+func processMatchValueVariables(matchValue interface{}, variables map[string]string) interface{} {
+	switch matchValue.(type) {
+	case string:
+		matchValueString := fmt.Sprintf("%v", matchValue)
+		re := regexp.MustCompile(`TFSEC_VAR_[A-Z_]+`)
+		return re.ReplaceAllStringFunc(matchValueString, func(match string) string {
+			return variables[match]
+		})
+	default:
+		return matchValue
+	}
+}
+
 func resourceFound(spec *MatchSpec, module block.Module) bool {
 	val := fmt.Sprintf("%v", spec.Name)
 	byType := module.GetResourcesByType(val)
diff --git a/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule.go b/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule.go
index ebc466500..da321c621 100644
--- a/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule.go
+++ b/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule.go
@@ -1,9 +1,7 @@
 package keyvault
 
 import (
-	"github.com/aquasecurity/defsec/rules"
 	"github.com/aquasecurity/defsec/rules/azure/keyvault"
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
@@ -12,6 +10,9 @@ func init() {
 	scanner.RegisterCheckRule(rule.Rule{
 		LegacyID: "AZU022",
 		BadExample: []string{`
+ resource "azurerm_key_vault" "example" {
+ }
+
  resource "azurerm_key_vault_secret" "bad_example" {
    name         = "secret-sauce"
    value        = "szechuan"
@@ -19,6 +20,9 @@ func init() {
  }
  `},
 		GoodExample: []string{`
+resource "azurerm_key_vault" "example" {
+}
+
  resource "azurerm_key_vault_secret" "good_example" {
    name         = "secret-sauce"
    value        = "szechuan"
@@ -32,12 +36,5 @@ func init() {
 		RequiredTypes:  []string{"resource"},
 		RequiredLabels: []string{"azurerm_key_vault_secret"},
 		Base:           keyvault.CheckContentTypeForSecret,
-		CheckTerraform: func(resourceBlock block.Block, _ block.Module) (results rules.Results) {
-
-			if resourceBlock.MissingChild("content_type") {
-				results.Add("Resource should have a content type set.", resourceBlock)
-			}
-			return results
-		},
 	})
 }
diff --git a/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule.go b/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule.go
index af47df569..bc75b84df 100644
--- a/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule.go
+++ b/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule.go
@@ -1,9 +1,7 @@
 package keyvault
 
 import (
-	"github.com/aquasecurity/defsec/rules"
 	"github.com/aquasecurity/defsec/rules/azure/keyvault"
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
@@ -52,12 +50,5 @@ func init() {
 		RequiredTypes:  []string{"resource"},
 		RequiredLabels: []string{"azurerm_key_vault_key"},
 		Base:           keyvault.CheckEnsureKeyExpiry,
-		CheckTerraform: func(resourceBlock block.Block, _ block.Module) (results rules.Results) {
-
-			if resourceBlock.MissingChild("expiration_date") {
-				results.Add("Resource should have an expiration date set.", resourceBlock)
-			}
-			return results
-		},
 	})
 }
diff --git a/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule.go b/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule.go
index 97ff4b611..76c290308 100644
--- a/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule.go
+++ b/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule.go
@@ -1,9 +1,7 @@
 package keyvault
 
 import (
-	"github.com/aquasecurity/defsec/rules"
 	"github.com/aquasecurity/defsec/rules/azure/keyvault"
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
@@ -32,12 +30,5 @@ func init() {
 		RequiredTypes:  []string{"resource"},
 		RequiredLabels: []string{"azurerm_key_vault_secret"},
 		Base:           keyvault.CheckEnsureSecretExpiry,
-		CheckTerraform: func(resourceBlock block.Block, _ block.Module) (results rules.Results) {
-
-			if resourceBlock.MissingChild("expiration_date") {
-				results.Add("Resource should have an expiration date set.", resourceBlock)
-			}
-			return results
-		},
 	})
 }
diff --git a/internal/app/tfsec/rules/azure/keyvault/no_purge_rule.go b/internal/app/tfsec/rules/azure/keyvault/no_purge_rule.go
index 6fa656df0..742b6a8cd 100644
--- a/internal/app/tfsec/rules/azure/keyvault/no_purge_rule.go
+++ b/internal/app/tfsec/rules/azure/keyvault/no_purge_rule.go
@@ -1,9 +1,7 @@
 package keyvault
 
 import (
-	"github.com/aquasecurity/defsec/rules"
 	"github.com/aquasecurity/defsec/rules/azure/keyvault"
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
@@ -34,22 +32,5 @@ func init() {
 		RequiredTypes:  []string{"resource"},
 		RequiredLabels: []string{"azurerm_key_vault"},
 		Base:           keyvault.CheckNoPurge,
-		CheckTerraform: func(resourceBlock block.Block, _ block.Module) (results rules.Results) {
-
-			if resourceBlock.MissingChild("purge_protection_enabled") {
-				results.Add("Resource should have purge protection enabled.", resourceBlock)
-				return
-			}
-			purgeProtectionAttr := resourceBlock.GetAttribute("purge_protection_enabled")
-			if purgeProtectionAttr.IsFalse() {
-				results.Add("Resource should have purge protection enabled.", purgeProtectionAttr)
-				return
-			}
-
-			if resourceBlock.MissingChild("soft_delete_retention_days") || resourceBlock.GetAttribute("soft_delete_retention_days").LessThan(1) {
-				results.Add("Resource should have soft_delete_retention_days set in order to enabled purge protection.", resourceBlock)
-			}
-			return results
-		},
 	})
 }
diff --git a/internal/app/tfsec/rules/azure/keyvault/specify_network_acl_rule.go b/internal/app/tfsec/rules/azure/keyvault/specify_network_acl_rule.go
index ab69ddcdd..79a97decb 100644
--- a/internal/app/tfsec/rules/azure/keyvault/specify_network_acl_rule.go
+++ b/internal/app/tfsec/rules/azure/keyvault/specify_network_acl_rule.go
@@ -1,9 +1,7 @@
 package keyvault
 
 import (
-	"github.com/aquasecurity/defsec/rules"
 	"github.com/aquasecurity/defsec/rules/azure/keyvault"
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
@@ -40,19 +38,5 @@ func init() {
 		RequiredTypes:  []string{"resource"},
 		RequiredLabels: []string{"azurerm_key_vault"},
 		Base:           keyvault.CheckSpecifyNetworkAcl,
-		CheckTerraform: func(resourceBlock block.Block, _ block.Module) (results rules.Results) {
-
-			defaultActionAttr := resourceBlock.GetNestedAttribute("network_acls.default_action")
-			if defaultActionAttr.IsNil() {
-				results.Add("Resource specifies does not specify a network acl block with default action.", resourceBlock)
-				return
-			}
-
-			if !defaultActionAttr.Equals("Deny") {
-				results.Add("Resource specifies does not specify a network acl block.", defaultActionAttr)
-			}
-
-			return results
-		},
 	})
 }
diff --git a/vendor/github.com/aquasecurity/defsec/provider/azure/keyvault/keyvault.go b/vendor/github.com/aquasecurity/defsec/provider/azure/keyvault/keyvault.go
index 98906fb05..fd20e340e 100644
--- a/vendor/github.com/aquasecurity/defsec/provider/azure/keyvault/keyvault.go
+++ b/vendor/github.com/aquasecurity/defsec/provider/azure/keyvault/keyvault.go
@@ -9,10 +9,11 @@ type KeyVault struct {
 }
 
 type Vault struct {
-	Secrets               []Secret
-	Keys                  []Key
-	EnablePurgeProtection types.BoolValue
-	NetworkACLs           NetworkACLs
+	Secrets                 []Secret
+	Keys                    []Key
+	EnablePurgeProtection   types.BoolValue
+	SoftDeleteRetentionDays types.IntValue
+	NetworkACLs             NetworkACLs
 }
 
 type NetworkACLs struct {
diff --git a/vendor/github.com/aquasecurity/defsec/rules/azure/keyvault/no_purge.go b/vendor/github.com/aquasecurity/defsec/rules/azure/keyvault/no_purge.go
index bde9b0696..27bf15ce9 100644
--- a/vendor/github.com/aquasecurity/defsec/rules/azure/keyvault/no_purge.go
+++ b/vendor/github.com/aquasecurity/defsec/rules/azure/keyvault/no_purge.go
@@ -22,13 +22,13 @@ Purge protection can only be enabled once soft-delete is enabled. It can be turn
 		Links: []string{
 			"https://docs.microsoft.com/en-us/azure/key-vault/general/soft-delete-overview#purge-protection",
 		},
-		Terraform:   &rules.EngineMetadata{
-            GoodExamples:        terraformNoPurgeGoodExamples,
-            BadExamples:         terraformNoPurgeBadExamples,
-            Links:               terraformNoPurgeLinks,
-            RemediationMarkdown: terraformNoPurgeRemediationMarkdown,
-        },
-        Severity: severity.Medium,
+		Terraform: &rules.EngineMetadata{
+			GoodExamples:        terraformNoPurgeGoodExamples,
+			BadExamples:         terraformNoPurgeBadExamples,
+			Links:               terraformNoPurgeLinks,
+			RemediationMarkdown: terraformNoPurgeRemediationMarkdown,
+		},
+		Severity: severity.Medium,
 	},
 	func(s *state.State) (results rules.Results) {
 		for _, vault := range s.Azure.KeyVault.Vaults {
@@ -38,6 +38,12 @@ Purge protection can only be enabled once soft-delete is enabled. It can be turn
 					vault.EnablePurgeProtection,
 				)
 			}
+			if vault.EnablePurgeProtection.IsTrue() && (vault.SoftDeleteRetentionDays.LessThan(7) || vault.SoftDeleteRetentionDays.GreaterThan(90)) {
+				results.Add(
+					"Resource should have soft_delete_retention_days set between 7 and 90 days in order to enable purge protection.",
+					vault.EnablePurgeProtection, vault.SoftDeleteRetentionDays,
+				)
+			}
 		}
 		return
 	},
diff --git a/vendor/github.com/aquasecurity/defsec/types/time_value.go b/vendor/github.com/aquasecurity/defsec/types/time_value.go
index 1e14f89c2..3296fbbbd 100644
--- a/vendor/github.com/aquasecurity/defsec/types/time_value.go
+++ b/vendor/github.com/aquasecurity/defsec/types/time_value.go
@@ -47,7 +47,7 @@ func (b *timeValue) GetRawValue() interface{} {
 }
 
 func (b *timeValue) IsNever() bool {
-	return b.value == nil
+	return b.value.IsZero()
 }
 
 func (b *timeValue) LessThan(i time.Time) bool {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index f0f7c21af..96ce3009b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -28,7 +28,7 @@ github.com/apparentlymart/go-cidr/cidr
 # github.com/apparentlymart/go-textseg/v13 v13.0.0
 ## explicit; go 1.16
 github.com/apparentlymart/go-textseg/v13/textseg
-# github.com/aquasecurity/defsec v0.3.21
+# github.com/aquasecurity/defsec v0.3.22
 ## explicit; go 1.17
 github.com/aquasecurity/defsec/cidr
 github.com/aquasecurity/defsec/formatters


Test patch:
diff --git a/internal/app/tfsec/custom/processing_test.go b/internal/app/tfsec/custom/processing_test.go
index 2ed6b5bb8..b139d55e2 100644
--- a/internal/app/tfsec/custom/processing_test.go
+++ b/internal/app/tfsec/custom/processing_test.go
@@ -122,6 +122,26 @@ var testPreConditionCheck = MatchSpec{
 	},
 }
 
+var testAssignVariableMatchSpec = MatchSpec{
+	Action: "and",
+	PredicateMatchSpec: []MatchSpec{
+		{
+			Name:           "bucket",
+			Action:         "isPresent",
+			AssignVariable: "TFSEC_VAR_BUCKET_NAME",
+		},
+		{
+			Name:   "lifecycle_rule",
+			Action: "isPresent",
+			SubMatch: &MatchSpec{
+				Name:       "id",
+				Action:     "startsWith",
+				MatchValue: "TFSEC_VAR_BUCKET_NAME",
+			},
+		},
+	},
+}
+
 func TestRequiresPresenceWithResourcePresent(t *testing.T) {
 	scanResults := scanTerraform(t, `
 resource "aws_vpc" "main" {
@@ -219,8 +239,8 @@ resource "aws_ami" "example" {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			block := ParseFromSource(test.source)[0].GetBlocks()[0]
-			result := evalMatchSpec(block, &test.predicateMatchSpec, nil)
-			assert.Equal(t, result, test.expected, "`Or` match function evaluating incorrectly.")
+			result := evalMatchSpec(block, &test.predicateMatchSpec, NewEmptyCustomContext())
+			assert.Equal(t, test.expected, result, "`Or` match function evaluating incorrectly.")
 		})
 	}
 }
@@ -266,8 +286,8 @@ resource "aws_ami" "example" {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			block := ParseFromSource(test.source)[0].GetBlocks()[0]
-			result := evalMatchSpec(block, &test.predicateMatchSpec, nil)
-			assert.Equal(t, result, test.expected, "`And` match function evaluating incorrectly.")
+			result := evalMatchSpec(block, &test.predicateMatchSpec, NewEmptyCustomContext())
+			assert.Equal(t, test.expected, result, "`And` match function evaluating incorrectly.")
 		})
 	}
 }
@@ -317,8 +337,8 @@ resource "aws_ami" "example" {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			block := ParseFromSource(test.source)[0].GetBlocks()[0]
-			result := evalMatchSpec(block, &test.predicateMatchSpec, nil)
-			assert.Equal(t, result, test.expected, "Nested match functions evaluating incorrectly.")
+			result := evalMatchSpec(block, &test.predicateMatchSpec, NewEmptyCustomContext())
+			assert.Equal(t, test.expected, result, "Nested match functions evaluating incorrectly.")
 		})
 	}
 }
@@ -344,8 +364,8 @@ resource "aws_ami" "example" {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			block := ParseFromSource(test.source)[0].GetBlocks()[0]
-			result := evalMatchSpec(block, &test.matchSpec, nil)
-			assert.Equal(t, result, test.expected, "Not match functions evaluating incorrectly.")
+			result := evalMatchSpec(block, &test.matchSpec, NewEmptyCustomContext())
+			assert.Equal(t, test.expected, result, "Not match functions evaluating incorrectly.")
 		})
 	}
 }
@@ -394,8 +414,54 @@ resource "aws_ami" "testing" {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			block := ParseFromSource(test.source)[0].GetBlocks()[0]
-			result := evalMatchSpec(block, &test.matchSpec, nil)
-			assert.Equal(t, result, test.expected, "precondition functions evaluating incorrectly.")
+			result := evalMatchSpec(block, &test.matchSpec, NewEmptyCustomContext())
+			assert.Equal(t, test.expected, result, "precondition functions evaluating incorrectly.")
+		})
+	}
+}
+
+func TestAssignVariable(t *testing.T) {
+	var tests = []struct {
+		name      string
+		source    string
+		matchSpec MatchSpec
+		expected  bool
+	}{
+		{
+			name: "check assignVariable handling in pass case",
+			source: `
+resource "aws_s3_bucket" "test-bucket" {
+  bucket = "test-bucket"
+
+  lifecycle_rule {
+    id = "test-bucket-rule-1"
+  }
+}
+`,
+			matchSpec: testAssignVariableMatchSpec,
+			expected:  true,
+		},
+		{
+			name: "check assignVariable handling in fail case",
+			source: `
+resource "aws_s3_bucket" "test-bucket" {
+  bucket = "test-bucket"
+
+  lifecycle_rule {
+    id = "not-bucket-name-rule-1"
+  }
+}
+`,
+			matchSpec: testAssignVariableMatchSpec,
+			expected:  false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			block := ParseFromSource(test.source)[0].GetBlocks()[0]
+			result := evalMatchSpec(block, &test.matchSpec, NewEmptyCustomContext())
+			assert.Equal(t, test.expected, result, "processing variable assignments incorrectly.")
 		})
 	}
 }
@@ -462,7 +528,7 @@ resource "google_compute_instance" "default" {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			block := ParseFromSource(test.source)[0].GetBlocks()[0]
-			result := evalMatchSpec(block, &test.predicateMatchSpec, nil)
+			result := evalMatchSpec(block, &test.predicateMatchSpec, NewEmptyCustomContext())
 			assert.Equal(t, result, test.expected, "`regexMatches` match function evaluating incorrectly.")
 		})
 	}
diff --git a/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule_test.go b/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule_test.go
index 95ba12d43..13da35d52 100644
--- a/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule_test.go
+++ b/internal/app/tfsec/rules/azure/keyvault/content_type_for_secret_rule_test.go
@@ -18,6 +18,9 @@ func Test_AZUKeyVaultSecretContentType(t *testing.T) {
 		{
 			name: "check if content_type is not set, check fails",
 			source: `
+resource "azurerm_key_vault" "example" {
+}
+
  resource "azurerm_key_vault_secret" "bad_example" {
    name         = "secret-sauce"
    value        = "szechuan"
@@ -29,6 +32,9 @@ func Test_AZUKeyVaultSecretContentType(t *testing.T) {
 		{
 			name: "check if content_type is set, check passes",
 			source: `
+resource "azurerm_key_vault" "example" {
+}
+			
  resource "azurerm_key_vault_secret" "good_example" {
    name         = "secret-sauce"
    value        = "szechuan"
diff --git a/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule_test.go b/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule_test.go
index 077862199..a842daab4 100644
--- a/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule_test.go
+++ b/internal/app/tfsec/rules/azure/keyvault/ensure_key_expiry_rule_test.go
@@ -18,6 +18,9 @@ func Test_AZUKeyVaultKeyExpirationDate(t *testing.T) {
 		{
 			name: "check if expiration_date is not set, check fails",
 			source: `
+ resource "azurerm_key_vault" "example" {
+ }
+
  resource "azurerm_key_vault_key" "bad_example" {
    name         = "generated-certificate"
    key_vault_id = azurerm_key_vault.example.id
@@ -39,6 +42,9 @@ func Test_AZUKeyVaultKeyExpirationDate(t *testing.T) {
 		{
 			name: "check if expiration_date is set, check passes",
 			source: `
+ resource "azurerm_key_vault" "example" {
+ }
+
  resource "azurerm_key_vault_key" "good_example" {
    name         = "generated-certificate"
    key_vault_id = azurerm_key_vault.example.id
diff --git a/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule_test.go b/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule_test.go
index fd1b30d0b..4222b6860 100644
--- a/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule_test.go
+++ b/internal/app/tfsec/rules/azure/keyvault/ensure_secret_expiry_rule_test.go
@@ -18,6 +18,9 @@ func Test_AZUKeyVaultSecretExpirationDate(t *testing.T) {
 		{
 			name: "check if expiration_date is not set, check fails",
 			source: `
+ resource "azurerm_key_vault" "example" {
+ }
+
  resource "azurerm_key_vault_secret" "bad_example" {
    name         = "secret-sauce"
    value        = "szechuan"
@@ -29,6 +32,9 @@ func Test_AZUKeyVaultSecretExpirationDate(t *testing.T) {
 		{
 			name: "check if expiration_date is set, check passes",
 			source: `
+ resource "azurerm_key_vault" "example" {
+ }
+
  resource "azurerm_key_vault_secret" "good_example" {
    name            = "secret-sauce"
    value        	  = "szechuan"