Skip to content

Commit

Permalink
feat: add API Gateway structured logging rule for tflint (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmoutschen authored Jun 8, 2021
1 parent 6938673 commit 1c7b1eb
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 4 deletions.
3 changes: 2 additions & 1 deletion docs/rules/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,8 @@ Amazon API Gateway can send logs to Amazon CloudWatch Logs and Amazon Kinesis Da

* __Level__: Error
* __cfn-lint__: WS2001
* __tflint__: _Not implemented_
* __tflint (REST APIs)__: aws_api_gateway_stage_structured_logging
* __tflint (HTTP APIs)__: aws_apigatewayv2_stage_structured_logging

You can customize the log format that Amazon API Gateway uses to send logs. Structured logging makes it easier to derive queries to answer arbitrary questions about the health of your application.

Expand Down
4 changes: 2 additions & 2 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ An __Info__ level means that this does not necessarily align with recommended pr
| Level | Name | cfn-lint | tflint |
|:-----------:|---------------------------------------------------------------------|:--------:|:------:|
| __Error__ | [API Gateway Logging](api_gateway.md#logging) | ES2000 | aws_apigateway_stage_logging_rule |
| __Warning__ | [API Gateway Structured Logging](api_gateway.md#structured-logging) | WS2001 |_Not implemented_|
| __Warning__ | [API Gateway Structured Logging](api_gateway.md#structured-logging) | WS2001 | aws_api_gateway_stage_structured_logging |
| __Warning__ | [API Gateway Tracing](api_gateway.md#tracing) | WS2002 | aws_apigateway_stage_tracing_rule |
| __Warning__ | [API Gateway Default Throttling](api_gateway.md#default-throttling) | ES2003 | aws_apigateway_stage_throttling_rule |

Expand All @@ -41,7 +41,7 @@ An __Info__ level means that this does not necessarily align with recommended pr
| Level | Name | cfn-lint | tflint |
|:-----------:|---------------------------------------------------------------------|:--------:|:------:|
| __Error__ | [API Gateway Logging](api_gateway.md#logging) | ES2000 | aws_apigatewayv2_stage_logging_rule |
| __Warning__ | [API Gateway Structured Logging](api_gateway.md#structured-logging) | WS2001 |_Not implemented_|
| __Warning__ | [API Gateway Structured Logging](api_gateway.md#structured-logging) | WS2001 | aws_apigatewayv2_stage_structured_logging |
| __Warning__ | [API Gateway Default Throttling](api_gateway.md#default-throttling) | ES2003 | aws_apigatewayv2_stage_throttling_rule |

## AWS AppSync
Expand Down
5 changes: 5 additions & 0 deletions tflint-ruleset-aws-serverless/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ test:
go test ./...

pr: lint test
ifneq ($(shell grep -oP "^\t+New[A-Za-z0-9]+\(\)," rules/provider.go | wc -l), $(shell grep -oP "^func New[A-Za-z0-9]+\(\)" rules/* | wc -l))
$(error Mismatch in rule count ($(shell grep -oP "^\t+New[A-Za-z0-9]+\(\)," rules/provider.go | wc -l) vs $(shell grep -oP "^func New[A-Za-z0-9]+\(\)" rules/* | wc -l)) - check rules/provider.go)
else
$(info Match in rule count)
endif

build:
go build
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package rules

import (
"encoding/json"
"fmt"
"regexp"

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

// AwsApigatewayStageStructuredLogging checks if API Gateway logging format is in JSON
type AwsApigatewayStageStructuredLoggingRule struct {
resourceType string
blockName string
attributeName string
}

// NewAwsApigatewayStageStructuredLoggingRule returns new rule with default attributes
func NewAwsApigatewayStageStructuredLoggingRule() *AwsApigatewayStageStructuredLoggingRule {
return &AwsApigatewayStageStructuredLoggingRule{
resourceType: "aws_api_gateway_stage",
blockName: "access_log_settings",
attributeName: "format",
}
}

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

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

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

// Link returns the rule reference link
func (r *AwsApigatewayStageStructuredLoggingRule) Link() string {
return ""
}

// Check checks if API Gateway logging format is in JSON
func (r *AwsApigatewayStageStructuredLoggingRule) Check(runner tflint.Runner) error {
// Regexp to substitute all $context. variables
re := regexp.MustCompile(`\$context\.[a-zA-Z\.]+`)

return runner.WalkResourceBlocks(r.resourceType, r.blockName, func(block *hcl.Block) error {
body, _, diags := block.Body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: r.attributeName,
},
},
})

if diags.HasErrors() {
return diags
}

var attrValue string
attribute, ok := body.Attributes[r.attributeName]
if !ok {
runner.EmitIssue(
r,
fmt.Sprintf("\"%s\" is not present.", r.attributeName),
body.MissingItemRange,
)
} else {
err := runner.EvaluateExpr(attribute.Expr, &attrValue, nil)
if err != nil {
return err
}

attrValue = re.ReplaceAllLiteralString(attrValue, "4")

// TODO: test if JSON
var js map[string]interface{}
if json.Unmarshal([]byte(attrValue), &js) != nil {
runner.EmitIssueOnExpr(
r,
fmt.Sprintf("\"%s\" is not valid JSON.", r.attributeName),
attribute.Expr,
)
}
}

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

import (
"testing"

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

func Test_AwsApigatewayStageStructuredLogging(t *testing.T) {
cases := []struct {
Name string
Content string
Expected helper.Issues
}{
{
Name: "missing format",
Content: `
resource "aws_api_gateway_stage" "valid" {
access_log_settings {
destination_arn = "ARN"
}
}`,
Expected: helper.Issues{
{
Rule: NewAwsApigatewayStageStructuredLoggingRule(),
Message: "\"format\" is not present.",
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 3, Column: 22},
End: hcl.Pos{Line: 3, Column: 22},
},
},
},
},
{
Name: "non-json",
Content: `
resource "aws_api_gateway_stage" "valid" {
access_log_settings {
destination_arn = "ARN"
format = "FORMAT"
}
}`,
Expected: helper.Issues{
{
Rule: NewAwsApigatewayStageStructuredLoggingRule(),
Message: "\"format\" is not valid JSON.",
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 5, Column: 12},
End: hcl.Pos{Line: 5, Column: 20},
},
},
},
},
{
Name: "malformed json",
Content: `
resource "aws_api_gateway_stage" "valid" {
access_log_settings {
destination_arn = "ARN"
format = <<EOF
{
"stage" : "$context.stage",
"request_id" : "$context.requestId",
"api_id" : "$context.apiId",
"resource_path" : "$context.resourcePath",
"resource_id" : "$context.resourceId",
"http_method" : "$context.httpMethod",
"source_ip" : "$context.identity.sourceIp",
"user-agent" : "$context.identity.userAgent",
"account_id" : "$context.identity.accountId",
"api_key" : "$context.identity.apiKey",
"caller" : "$context.identity.caller",
"user" : "$context.identity.user",
"user_arn" : "$context.identity.userArn",
"integration_latency": $context.integration.latency
EOF
}
}`,
Expected: helper.Issues{
{
Rule: NewAwsApigatewayStageStructuredLoggingRule(),
Message: "\"format\" is not valid JSON.",
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 5, Column: 12},
End: hcl.Pos{Line: 21, Column: 4},
},
},
},
},
{
Name: "valid",
Content: `
resource "aws_api_gateway_stage" "valid" {
access_log_settings {
destination_arn = "ARN"
format = <<EOF
{
"stage" : "$context.stage",
"request_id" : "$context.requestId",
"api_id" : "$context.apiId",
"resource_path" : "$context.resourcePath",
"resource_id" : "$context.resourceId",
"http_method" : "$context.httpMethod",
"source_ip" : "$context.identity.sourceIp",
"user-agent" : "$context.identity.userAgent",
"account_id" : "$context.identity.accountId",
"api_key" : "$context.identity.apiKey",
"caller" : "$context.identity.caller",
"user" : "$context.identity.user",
"user_arn" : "$context.identity.userArn",
"integration_latency": $context.integration.latency
}
EOF
}
}`,
Expected: helper.Issues{},
},
}

rule := NewAwsApigatewayStageStructuredLoggingRule()

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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package rules

import (
"encoding/json"
"fmt"
"regexp"

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

// AwsApigatewayV2StageStructuredLogging checks if API Gateway logging format is in JSON
type AwsApigatewayV2StageStructuredLoggingRule struct {
resourceType string
blockName string
attributeName string
}

// NewAwsApigatewayV2StageStructuredLoggingRule returns new rule with default attributes
func NewAwsApigatewayV2StageStructuredLoggingRule() *AwsApigatewayV2StageStructuredLoggingRule {
return &AwsApigatewayV2StageStructuredLoggingRule{
resourceType: "aws_apigatewayv2_stage",
blockName: "access_log_settings",
attributeName: "format",
}
}

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

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

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

// Link returns the rule reference link
func (r *AwsApigatewayV2StageStructuredLoggingRule) Link() string {
return ""
}

// Check checks if API Gateway logging format is in JSON
func (r *AwsApigatewayV2StageStructuredLoggingRule) Check(runner tflint.Runner) error {
// Regexp to substitute all $context. variables
re := regexp.MustCompile(`\$context\.[a-zA-Z\.]+`)

return runner.WalkResourceBlocks(r.resourceType, r.blockName, func(block *hcl.Block) error {
body, _, diags := block.Body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: r.attributeName,
},
},
})

if diags.HasErrors() {
return diags
}

var attrValue string
attribute, ok := body.Attributes[r.attributeName]
if !ok {
runner.EmitIssue(
r,
fmt.Sprintf("\"%s\" is not present.", r.attributeName),
body.MissingItemRange,
)
} else {
err := runner.EvaluateExpr(attribute.Expr, &attrValue, nil)
if err != nil {
return err
}

attrValue = re.ReplaceAllLiteralString(attrValue, "4")

// TODO: test if JSON
var js map[string]interface{}
if json.Unmarshal([]byte(attrValue), &js) != nil {
runner.EmitIssueOnExpr(
r,
fmt.Sprintf("\"%s\" is not valid JSON.", r.attributeName),
attribute.Expr,
)
}
}

return nil
})
}
Loading

0 comments on commit 1c7b1eb

Please sign in to comment.