Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ An __Info__{: class="badge badge-blue" } level means that this does not necessar
| __Error__{: class="badge badge-red" } | [EventSourceMapping Failure Destination](lambda/eventsourcemapping_failure_destination.md) | ES1001 | aws_lambda_event_source_mapping_failure_destination |
| __Warning__{: class="badge badge-yellow" } | [Lambda Permission Multiple Principals](lambda/permission_multiple_principals.md) | WS1002 | aws_lambda_permission_multiple_principals |
| __Warning__{: class="badge badge-yellow" } | [Lambda Star Permissions](lambda/star_permissions.md) | WS1003 | aws_iam_role_lambda_no_star |
| __Warning__{: class="badge badge-yellow" } | [Lambda Log Retention](lambda/log_retention.md) | WS1004 |_Not implemented_|
| __Warning__{: class="badge badge-yellow" } | [Lambda Log Retention](lambda/log_retention.md) | WS1004 | aws_cloudwatch_log_group_lambda_retention |
| __Error__{: class="badge badge-red" } | [Lambda Default Memory Size](lambda/default_memory_size.md) | ES1005 | aws_lambda_function_default_memory |
| __Error__{: class="badge badge-red" } | [Lambda Default Timeout](lambda/default_timeout.md) | ES1006 | aws_lambda_function_default_timeout |
| __Error__{: class="badge badge-red" } | [Async Lambda Failure Destination](lambda/async_failure_destination.md) | ES1007 | aws_lambda_event_invoke_config_async_on_failure |
Expand Down
4 changes: 2 additions & 2 deletions docs/rules/lambda/log_retention.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ __Initial version__: 0.1.3
__cfn-lint__: WS1004
{: class="badge" }

__tflint__: _Not implemented_
__tflint__: aws_cloudwatch_log_group_lambda_retention
{: class="badge" }

By default, CloudWatch log groups created by Lambda functions have an unlimited retention time. For cost optimization purposes, you should set a retention duration on all log groups. For log archival, export and set cost-effective storage classes that best suit your needs.
Expand Down Expand Up @@ -190,7 +190,7 @@ Since `serverless-rules` evaluate infrastructure-as-code template, it cannot che

# Explicit log group
resource "aws_cloudwatch_log_group" "this" {
name = "/aws/lambda/{aws_lambda_function.this.function_name}
name = "/aws/lambda/${aws_lambda_function.this.function_name}
# Explicit retention time
retention_in_days = 7
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package rules

import (
"fmt"
"regexp"

hcl "github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/terraform/configs"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
)

type awsLambdaLogGroup struct {
functionName string
resourceName string
found bool
resourceRange hcl.Range
}

// AwsCloudwatchLogGroupLambdaRetention checks if Lambda functions have a corresponding log group with retention configured
type AwsCloudwatchLogGroupLambdaRetentionRule struct {
functionResourceType string
logGroupResourceType string
functionNameAttrName string
nameAttrName string
retentionAttrName string
}

// NewAwsCloudwatchLogGroupLambdaRetentionRule returns new rule with default attributes
func NewAwsCloudwatchLogGroupLambdaRetentionRule() *AwsCloudwatchLogGroupLambdaRetentionRule {
return &AwsCloudwatchLogGroupLambdaRetentionRule{
functionResourceType: "aws_lambda_function",
logGroupResourceType: "aws_cloudwatch_log_group",
functionNameAttrName: "function_name",
nameAttrName: "name",
retentionAttrName: "retention_in_days",
}
}

// Name returns the rule name
func (r *AwsCloudwatchLogGroupLambdaRetentionRule) Name() string {
return "aws_cloudwatch_log_group_lambda_retention"
}

// Enabled returns whether the rule is enabled by default
func (r *AwsCloudwatchLogGroupLambdaRetentionRule) Enabled() bool {
return true
}

// Severity returns the rule severity
func (r *AwsCloudwatchLogGroupLambdaRetentionRule) Severity() string {
return tflint.WARNING
}

// Link returns the rule reference link
func (r *AwsCloudwatchLogGroupLambdaRetentionRule) Link() string {
return "https://awslabs.github.io/serverless-rules/rules/lambda/log_retention.html"
}

// Check checks if Lambda functions have a corresponding log group with retention configured
func (r *AwsCloudwatchLogGroupLambdaRetentionRule) Check(runner tflint.Runner) error {

// Gather all Lambda functions
var functions []awsLambdaLogGroup
err := runner.WalkResources(r.functionResourceType, func(resource *configs.Resource) error {
function := awsLambdaLogGroup{
resourceName: resource.Name,
functionName: "",
found: false,
resourceRange: resource.Config.MissingItemRange(),
}

// Function name attribute
body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: r.functionNameAttrName,
},
},
})

if diags.HasErrors() {
return diags
}

attribute, ok := body.Attributes[r.functionNameAttrName]

if ok {
var value string
err := runner.EvaluateExpr(attribute.Expr, &value, nil)
if err != nil {
return err
}
function.functionName = value
}

functions = append(functions, function)

return nil
})

if err != nil {
return err
}

// Lookup log groups
err = runner.WalkResources(r.logGroupResourceType, func(resource *configs.Resource) error {
body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: r.nameAttrName,
},
{
Name: r.retentionAttrName,
},
},
})

if diags.HasErrors() {
return diags
}

// Get log group attributes

nameAttr, ok := body.Attributes[r.nameAttrName]
// No need to check further if there are no name, early return
if !ok {
return nil
}

var nameValue string
err := runner.EvaluateExpr(nameAttr.Expr, &nameValue, nil)
if err != nil {
return err
}

retentionAttr, ok := body.Attributes[r.retentionAttrName]
// No need to check further if there are no retention, early return
if !ok {
return nil
}

var retentionValue string
err = runner.EvaluateExpr(retentionAttr.Expr, &retentionValue, nil)
if err != nil {
return err
}

// Parse log group names
re := regexp.MustCompile(`^/aws/lambda/(.*)$`)
m := re.FindAllStringSubmatch(nameValue, -1)
// Log group name doesn't match pattern, early return
if len(m) > 1 {
return nil
}
functionName := m[0][1]

for i := range functions {
if functions[i].functionName == functionName {
functions[i].found = true
}
if fmt.Sprintf("${aws_lambda_function.%s.function_name}", functions[i].resourceName) == functionName {
functions[i].found = true
}
}

return nil
})

if err != nil {
return err
}

for _, function := range functions {
if !function.found {
runner.EmitIssue(
r,
fmt.Sprintf("\"%s\" is missing a log group with retention_in_days.", r.functionResourceType),
function.resourceRange,
)
}
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package rules

import (
"testing"

hcl "github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/helper"
)

func Test_AwsCloudwatchLogGroupLambdaRetention(t *testing.T) {
cases := []struct {
Name string
Content string
Expected helper.Issues
}{
{
Name: "missing aws_cloudwatch_log_group",
Content: `
resource "aws_lambda_function" "this" {
function_name = "my-function-name"
}
`,
Expected: helper.Issues{
{
Rule: NewAwsCloudwatchLogGroupLambdaRetentionRule(),
Message: `"aws_lambda_function" is missing a log group with retention_in_days.`,
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 2, Column: 39},
End: hcl.Pos{Line: 2, Column: 39},
},
},
},
},
{
Name: "missing retention_in_days",
Content: `
resource "aws_lambda_function" "this" {
function_name = "my-function-name"
}

resource "aws_cloudwatch_log_group" "this" {
name = "/aws/lambda/my-function-name"
}
`,
Expected: helper.Issues{
{
Rule: NewAwsCloudwatchLogGroupLambdaRetentionRule(),
Message: `"aws_lambda_function" is missing a log group with retention_in_days.`,
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 2, Column: 39},
End: hcl.Pos{Line: 2, Column: 39},
},
},
},
},
{
Name: "valid",
Content: `
resource "aws_lambda_function" "this" {
function_name = "my-function-name"
}

resource "aws_cloudwatch_log_group" "this" {
name = "/aws/lambda/my-function-name"
retention_in_days = 7
}
`,
Expected: helper.Issues{},
},
{
Name: "non-lambda",
Content: `
resource "aws_cloudwatch_log_group" "this" {
name = "not-lambda"
}
`,
Expected: helper.Issues{},
},
}

rule := NewAwsCloudwatchLogGroupLambdaRetentionRule()

for _, tc := range cases {
runner := helper.TestRunner(t, map[string]string{"resource.tf": tc.Content})

if err := rule.Check(runner); err != nil {
t.Fatalf("Unexpected error occurred: %s", err)
}

helper.AssertIssues(t, tc.Expected, runner.Issues)
}
}
1 change: 1 addition & 0 deletions tflint-ruleset-aws-serverless/rules/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var Rules = []tflint.Rule{
NewAwsApigatewayV2StageThrottlingRule(),
NewAwsAppsyncGraphqlAPITracingRule(),
NewAwsCloudwatchEventTargetNoDlqRule(),
NewAwsCloudwatchLogGroupLambdaRetentionRule(),
NewAwsIamRoleLambdaNoStarRule(),
NewAwsLambdaEventInvokeConfigAsyncOnFailureRule(),
NewAwsLambdaEventSourceMappingFailureDestinationRule(),
Expand Down