diff --git a/docs/README.md b/docs/README.md index e5124d8..539626e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ |[azurerm_eventhub_namespace_public_network_access_enabled](./rules/azurerm_eventhub_namespace_public_network_access_enabled.md)|Notice|✔| |[azurerm_eventhub_namespace_unsecure_tls](./rules/azurerm_eventhub_namespace_unsecure_tls.md)|Warning|✔| |[azurerm_iothub_endpoint_eventhub_authentication_type](./rules/azurerm_iothub_endpoint_eventhub_authentication_type.md)|Notice|✔| +|[azurerm_key_vault_certificate_lifetime_action](./rules/azurerm_key_vault_certificate_lifetime_action.md)|Warning|✔| |[azurerm_key_vault_enable_rbac_authorization](./rules/azurerm_key_vault_enable_rbac_authorization.md)|Warning|| |[azurerm_key_vault_network_acls_default_deny](./rules/azurerm_key_vault_network_acls_default_deny.md)|Warning|✔| |[azurerm_key_vault_public_network_access_enabled](./rules/azurerm_key_vault_public_network_access_enabled.md)|Notice|| @@ -60,6 +61,10 @@ - [azurerm_key_vault_network_acls_default_deny](./rules/azurerm_key_vault_network_acls_default_deny.md) - [azurerm_key_vault_public_network_access_enabled](./rules/azurerm_key_vault_public_network_access_enabled.md) +### azurerm_key_vault_certificate + +- [azurerm_key_vault_certificate_lifetime_action](./rules/azurerm_key_vault_certificate_lifetime_action.md) + ### azurerm_linux_function_app - [azurerm_linux_function_app_ftps_state](./rules/azurerm_linux_function_app_ftps_state.md) diff --git a/docs/rules/azurerm_key_vault_certificate_lifetime_action.md b/docs/rules/azurerm_key_vault_certificate_lifetime_action.md new file mode 100644 index 0000000..d26678a --- /dev/null +++ b/docs/rules/azurerm_key_vault_certificate_lifetime_action.md @@ -0,0 +1,42 @@ +# azurerm_key_vault_certificate_lifetime_action + +**Severity:** Warning + + +## Example + +```hcl +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + # missing lifetime_policy + } +} +``` + +## Why + +Setting lifetime_action to AutoRenew or EmailContacts ensures proactive management of certificate expiration, reducing the risk of service interruptions or security vulnerabilities caused by expired certificates. + +## How to Fix + +```hcl +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + lifetime_action { + action { + action_type = "AutoRenew" + } + } + } +} +``` + + +## How to disable + +```hcl +rule "azurerm_key_vault_certificate_lifetime_action" { + enabled = false +} +``` + diff --git a/main.go b/main.go index 80ba352..f50569e 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ func createRuleSet() *tflint.BuiltinRuleSet { rules.NewAzurermIoTHubEndpointEventHubAuthenticationType(), rules.NewAzurermKeyVaultNetworkACLsDefaultDeny(), rules.NewAzurermKeyVaultPublicNetworkAccessEnabled(), + rules.NewAzurermKeyVaultCertificateLifetimeAction(), rules.NewAzurermKeyVaultRbacDisabled(), rules.NewAzurermLinuxFunctionAppFtpsState(), rules.NewAzurermLinuxFunctionAppHTTPSOnly(), diff --git a/rules/azurerm_key_vault_certificate_lifetime_action_action_type.go b/rules/azurerm_key_vault_certificate_lifetime_action_action_type.go new file mode 100644 index 0000000..42afa4d --- /dev/null +++ b/rules/azurerm_key_vault_certificate_lifetime_action_action_type.go @@ -0,0 +1,151 @@ +package rules + +import ( + "fmt" + "strings" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + + "github.com/terraform-linters/tflint-ruleset-azurerm-security/project" +) + +// AzurermKeyVaultCertificateLifetimeAction checks that certificate_policy.lifetime_action.action.action_type +// is set to either "AutoRenew" or "EmailContacts" +type AzurermKeyVaultCertificateLifetimeAction struct { + tflint.DefaultRule + + resourceType string + attributePath []string + validValues []string +} + +// NewAzurermKeyVaultCertificateLifetimeAction returns a new rule instance +func NewAzurermKeyVaultCertificateLifetimeAction() *AzurermKeyVaultCertificateLifetimeAction { + return &AzurermKeyVaultCertificateLifetimeAction{ + resourceType: "azurerm_key_vault_certificate", + attributePath: []string{"certificate_policy", "lifetime_action", "action", "action_type"}, + validValues: []string{"AutoRenew", "EmailContacts"}, + } +} + +// Name returns the rule name +func (r *AzurermKeyVaultCertificateLifetimeAction) Name() string { + return "azurerm_key_vault_certificate_lifetime_action" +} + +// Enabled returns whether the rule is enabled by default +func (r *AzurermKeyVaultCertificateLifetimeAction) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *AzurermKeyVaultCertificateLifetimeAction) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *AzurermKeyVaultCertificateLifetimeAction) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check verifies that the certificate policy lifetime action is properly configured +func (r *AzurermKeyVaultCertificateLifetimeAction) Check(runner tflint.Runner) error { + resources, err := runner.GetResourceContent(r.resourceType, &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "certificate_policy", + Body: &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "lifetime_action", + Body: &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "action", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "action_type"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + certPolicyBlocks := resource.Body.Blocks.OfType("certificate_policy") + if len(certPolicyBlocks) == 0 { + runner.EmitIssue( + r, + "certificate_policy block is missing", + resource.DefRange, + ) + continue + } + + certPolicy := certPolicyBlocks[0] + lifetimeActionBlocks := certPolicy.Body.Blocks.OfType("lifetime_action") + if len(lifetimeActionBlocks) == 0 { + runner.EmitIssue( + r, + "lifetime_action block is missing in certificate_policy", + certPolicy.DefRange, + ) + continue + } + + lifetimeAction := lifetimeActionBlocks[0] + actionBlocks := lifetimeAction.Body.Blocks.OfType("action") + if len(actionBlocks) == 0 { + runner.EmitIssue( + r, + "action block is missing in lifetime_action", + lifetimeAction.DefRange, + ) + continue + } + + action := actionBlocks[0] + attribute, exists := action.Body.Attributes["action_type"] + if !exists { + runner.EmitIssue( + r, + "action_type is missing in action block, should be set to either AutoRenew or EmailContacts", + action.DefRange, + ) + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func(val string) error { + valid := false + for _, validValue := range r.validValues { + if strings.EqualFold(val, validValue) { + valid = true + break + } + } + if !valid { + runner.EmitIssue( + r, + fmt.Sprintf("action_type is set to %s, should be set to either AutoRenew or EmailContacts", val), + attribute.Expr.Range(), + ) + } + return nil + }, nil) + if err != nil { + return err + } + } + + return nil +} \ No newline at end of file diff --git a/rules/azurerm_key_vault_certificate_lifetime_action_action_type_test.go b/rules/azurerm_key_vault_certificate_lifetime_action_action_type_test.go new file mode 100644 index 0000000..7918c70 --- /dev/null +++ b/rules/azurerm_key_vault_certificate_lifetime_action_action_type_test.go @@ -0,0 +1,193 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AzurermKeyVaultCertificateLifetimeAction(t *testing.T) { + tests := []struct { + Name string + Content string + Expected helper.Issues + }{ + { + Name: "action_type set to invalid value", + Content: ` +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + lifetime_action { + action { + action_type = "Invalid" + } + } + } +}`, + Expected: helper.Issues{ + { + Rule: NewAzurermKeyVaultCertificateLifetimeAction(), + Message: "action_type is set to Invalid, should be set to either AutoRenew or EmailContacts", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{ + Line: 6, + Column: 31, + }, + End: hcl.Pos{ + Line: 6, + Column: 40, + }, + }, + }, + }, + }, + { + Name: "action_type set to AutoRenew", + Content: ` +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + lifetime_action { + action { + action_type = "AutoRenew" + } + } + } +}`, + Expected: helper.Issues{}, + }, + { + Name: "action_type set to EmailContacts", + Content: ` +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + lifetime_action { + action { + action_type = "EmailContacts" + } + } + } +}`, + Expected: helper.Issues{}, + }, + { + Name: "action_type attribute missing", + Content: ` +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + lifetime_action { + action { + } + } + } +}`, + Expected: helper.Issues{ + { + Rule: NewAzurermKeyVaultCertificateLifetimeAction(), + Message: "action_type is missing in action block, should be set to either AutoRenew or EmailContacts", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{ + Line: 5, + Column: 13, + }, + End: hcl.Pos{ + Line: 5, + Column: 19, + }, + }, + }, + }, + }, + { + Name: "action block missing", + Content: ` +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + lifetime_action { + } + } +}`, + Expected: helper.Issues{ + { + Rule: NewAzurermKeyVaultCertificateLifetimeAction(), + Message: "action block is missing in lifetime_action", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{ + Line: 4, + Column: 9, + }, + End: hcl.Pos{ + Line: 4, + Column: 24, + }, + }, + }, + }, + }, + { + Name: "lifetime_action block missing", + Content: ` +resource "azurerm_key_vault_certificate" "example" { + certificate_policy { + } +}`, + Expected: helper.Issues{ + { + Rule: NewAzurermKeyVaultCertificateLifetimeAction(), + Message: "lifetime_action block is missing in certificate_policy", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{ + Line: 3, + Column: 5, + }, + End: hcl.Pos{ + Line: 3, + Column: 23, + }, + }, + }, + }, + }, + { + Name: "certificate_policy block missing", + Content: ` +resource "azurerm_key_vault_certificate" "example" { +}`, + Expected: helper.Issues{ + { + Rule: NewAzurermKeyVaultCertificateLifetimeAction(), + Message: "certificate_policy block is missing", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{ + Line: 2, + Column: 1, + }, + End: hcl.Pos{ + Line: 2, + Column: 51, + }, + }, + }, + }, + }, + } + + rule := NewAzurermKeyVaultCertificateLifetimeAction() + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + runner := helper.TestRunner(t, map[string]string{"resource.tf": test.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssues(t, test.Expected, runner.Issues) + }) + } +} \ No newline at end of file