diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8db7090 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5f46120 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: build + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * *" + +jobs: + test: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Run tests + run: make test + - name: Run build + run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..34327ad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: release + +on: + push: + branches: + - '!*' + tags: + - v*.*.* + +permissions: + contents: write + id-token: write + attestations: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + - uses: actions/attest-build-provenance@v1 + with: + subject-path: 'dist/checksums.txt' diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..728ce3e --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,31 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +env: + - CGO_ENABLED=0 +builds: + - targets: + - darwin_amd64 + - darwin_arm64 + - linux_386 + - linux_amd64 + - linux_arm + - linux_arm64 + - windows_386 + - windows_amd64 + hooks: + post: + - mkdir -p ./dist/raw + - cp "{{ .Path }}" "./dist/raw/{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" +archives: + - id: zip + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" + format: zip + files: + - none* +checksum: + name_template: 'checksums.txt' + extra_files: + - glob: ./dist/raw/* +signs: + - artifacts: checksum + args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cd6a73a --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +default: build + +test: + go test ./... + +build: + go build + +install: build + mkdir -p ~/.tflint.d/plugins + mv ./tflint-ruleset-template ~/.tflint.d/plugins diff --git a/README.md b/README.md new file mode 100644 index 0000000..31f5389 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# TFLint Ruleset Template +[![Build Status](https://github.com/terraform-linters/tflint-ruleset-template/workflows/build/badge.svg?branch=main)](https://github.com/terraform-linters/tflint-ruleset-template/actions) + +This is a template repository for building a custom ruleset. You can create a plugin repository from "Use this template". See also [Writing Plugins](https://github.com/terraform-linters/tflint/blob/master/docs/developer-guide/plugins.md). + +## Requirements + +- TFLint v0.42+ +- Go v1.22 + +## Installation + +TODO: This template repository does not contain release binaries, so this installation will not work. Please rewrite for your repository. See the "Building the plugin" section to get this template ruleset working. + +You can install the plugin with `tflint --init`. Declare a config in `.tflint.hcl` as follows: + +```hcl +plugin "template" { + enabled = true + + version = "0.1.0" + source = "github.com/terraform-linters/tflint-ruleset-template" + + signing_key = <<-KEY + -----BEGIN PGP PUBLIC KEY BLOCK----- + mQINBGCqS2YBEADJ7gHktSV5NgUe08hD/uWWPwY07d5WZ1+F9I9SoiK/mtcNGz4P + JLrYAIUTMBvrxk3I+kuwhp7MCk7CD/tRVkPRIklONgtKsp8jCke7FB3PuFlP/ptL + SlbaXx53FCZSOzCJo9puZajVWydoGfnZi5apddd11Zw1FuJma3YElHZ1A1D2YvrF + ... + KEY +} +``` + +## Rules + +|Name|Description|Severity|Enabled|Link| +| --- | --- | --- | --- | --- | +|aws_instance_example_type|Example rule for accessing and evaluating top-level attributes|ERROR|✔|| +|aws_s3_bucket_example_lifecycle_rule|Example rule for accessing top-level/nested blocks and attributes under the blocks|ERROR|✔|| +|google_compute_ssl_policy|Example rule with a custom rule config|WARNING|✔|| +|terraform_backend_type|Example rule for accessing other than resources|ERROR|✔|| + +## Building the plugin + +Clone the repository locally and run the following command: + +``` +$ make +``` + +You can easily install the built plugin with the following: + +``` +$ make install +``` + +You can run the built plugin like the following: + +``` +$ cat << EOS > .tflint.hcl +plugin "template" { + enabled = true +} +EOS +$ tflint +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6be88e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module github.com/terraform-linters/tflint-ruleset-template + +go 1.22.2 + +require ( + github.com/hashicorp/hcl/v2 v2.20.1 + github.com/terraform-linters/tflint-plugin-sdk v0.20.0 +) + +require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fd1d9d4 --- /dev/null +++ b/go.sum @@ -0,0 +1,84 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +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/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= +github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/terraform-linters/tflint-plugin-sdk v0.20.0 h1:e7vfAI2rvAtClPx+eY0fd7kvmY0plVRDycgSQL9UQ4o= +github.com/terraform-linters/tflint-plugin-sdk v0.20.0/go.mod h1:ySt9h+KoEhCM/zjjigIZC2J2Tboyzf53437PAYjrKxc= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9c1dfc0 --- /dev/null +++ b/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/terraform-linters/tflint-plugin-sdk/plugin" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-template/rules" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + RuleSet: &tflint.BuiltinRuleSet{ + Name: "template", + Version: "0.1.0", + Rules: []tflint.Rule{ + rules.NewAwsInstanceExampleTypeRule(), + rules.NewAwsS3BucketExampleLifecycleRule(), + rules.NewGoogleComputeSSLPolicyRule(), + rules.NewTerraformBackendTypeRule(), + }, + }, + }) +} diff --git a/rules/aws_instance_example_type.go b/rules/aws_instance_example_type.go new file mode 100644 index 0000000..5290594 --- /dev/null +++ b/rules/aws_instance_example_type.go @@ -0,0 +1,75 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/logger" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// AwsInstanceExampleTypeRule checks whether ... +type AwsInstanceExampleTypeRule struct { + tflint.DefaultRule +} + +// NewAwsInstanceExampleTypeRule returns a new rule +func NewAwsInstanceExampleTypeRule() *AwsInstanceExampleTypeRule { + return &AwsInstanceExampleTypeRule{} +} + +// Name returns the rule name +func (r *AwsInstanceExampleTypeRule) Name() string { + return "aws_instance_example_type" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsInstanceExampleTypeRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *AwsInstanceExampleTypeRule) Severity() tflint.Severity { + return tflint.ERROR +} + +// Link returns the rule reference link +func (r *AwsInstanceExampleTypeRule) Link() string { + return "" +} + +// Check checks whether ... +func (r *AwsInstanceExampleTypeRule) Check(runner tflint.Runner) error { + // This rule is an example to get a top-level resource attribute. + resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "instance_type"}, + }, + }, nil) + if err != nil { + return err + } + + // Put a log that can be output with `TFLINT_LOG=debug` + logger.Debug(fmt.Sprintf("Get %d instances", len(resources.Blocks))) + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes["instance_type"] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func (instanceType string) error { + return runner.EmitIssue( + r, + fmt.Sprintf("instance type is %s", instanceType), + attribute.Expr.Range(), + ) + }, nil) + if err != nil { + return err + } + } + + return nil +} diff --git a/rules/aws_instance_example_type_test.go b/rules/aws_instance_example_type_test.go new file mode 100644 index 0000000..279f4e5 --- /dev/null +++ b/rules/aws_instance_example_type_test.go @@ -0,0 +1,49 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AwsInstanceExampleType(t *testing.T) { + tests := []struct { + Name string + Content string + Expected helper.Issues + }{ + { + Name: "issue found", + Content: ` +resource "aws_instance" "web" { + instance_type = "t2.micro" +}`, + Expected: helper.Issues{ + { + Rule: NewAwsInstanceExampleTypeRule(), + Message: "instance type is t2.micro", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 21}, + End: hcl.Pos{Line: 3, Column: 31}, + }, + }, + }, + }, + } + + rule := NewAwsInstanceExampleTypeRule() + + 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) + }) + } +} diff --git a/rules/aws_s3_bucket_example_lifecycle_rule.go b/rules/aws_s3_bucket_example_lifecycle_rule.go new file mode 100644 index 0000000..87c2db1 --- /dev/null +++ b/rules/aws_s3_bucket_example_lifecycle_rule.go @@ -0,0 +1,81 @@ +package rules + +import ( + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// AwsS3BucketExampleLifecycleRule checks whether ... +type AwsS3BucketExampleLifecycleRule struct { + tflint.DefaultRule +} + +// NewAwsS3BucketExampleLifecycleRule returns a new rule +func NewAwsS3BucketExampleLifecycleRule() *AwsS3BucketExampleLifecycleRule { + return &AwsS3BucketExampleLifecycleRule{} +} + +// Name returns the rule name +func (r *AwsS3BucketExampleLifecycleRule) Name() string { + return "aws_s3_bucket_example_lifecycle_rule" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsS3BucketExampleLifecycleRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *AwsS3BucketExampleLifecycleRule) Severity() tflint.Severity { + return tflint.ERROR +} + +// Link returns the rule reference link +func (r *AwsS3BucketExampleLifecycleRule) Link() string { + return "" +} + +// Check checks whether ... +func (r *AwsS3BucketExampleLifecycleRule) Check(runner tflint.Runner) error { + // This rule is an example to get nested resource attributes. + resources, err := runner.GetResourceContent("aws_s3_bucket", &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "lifecycle_rule", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "enabled"}, + }, + Blocks: []hclext.BlockSchema{ + {Type: "transition"}, + }, + }, + }, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + for _, rule := range resource.Body.Blocks { + if err := runner.EmitIssue(r, "`lifecycle_rule` block found", rule.DefRange); err != nil { + return err + } + + if attr, exists := rule.Body.Attributes["enabled"]; exists { + if err := runner.EmitIssue(r, "`enabled` attribute found", attr.Expr.Range()); err != nil { + return err + } + } + + for _, transitions := range rule.Body.Blocks { + if err := runner.EmitIssue(r, "`transition` block found", transitions.DefRange); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/rules/aws_s3_bucket_example_lifecycle_rule_test.go b/rules/aws_s3_bucket_example_lifecycle_rule_test.go new file mode 100644 index 0000000..98d0524 --- /dev/null +++ b/rules/aws_s3_bucket_example_lifecycle_rule_test.go @@ -0,0 +1,74 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_AwsS3BucketExampleLifecycleRule(t *testing.T) { + tests := []struct { + Name string + Content string + Expected helper.Issues + }{ + { + Name: "issue found", + Content: ` +resource "aws_s3_bucket" "bucket" { + lifecycle_rule { + enabled = false + + transition { + days = 30 + storage_class = "STANDARD_IA" + } + } +}`, + Expected: helper.Issues{ + { + Rule: NewAwsS3BucketExampleLifecycleRule(), + Message: "`lifecycle_rule` block found", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 17}, + }, + }, + { + Rule: NewAwsS3BucketExampleLifecycleRule(), + Message: "`enabled` attribute found", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 4, Column: 15}, + End: hcl.Pos{Line: 4, Column: 20}, + }, + }, + { + Rule: NewAwsS3BucketExampleLifecycleRule(), + Message: "`transition` block found", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 6, Column: 5}, + End: hcl.Pos{Line: 6, Column: 15}, + }, + }, + }, + }, + } + + rule := NewAwsS3BucketExampleLifecycleRule() + + 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) + }) + } +} diff --git a/rules/google_compute_ssl_policy.go b/rules/google_compute_ssl_policy.go new file mode 100644 index 0000000..fd345f9 --- /dev/null +++ b/rules/google_compute_ssl_policy.go @@ -0,0 +1,87 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// GoogleComputeSSLPolicyRule checks whether ... +type GoogleComputeSSLPolicyRule struct { + tflint.DefaultRule +} + +// GoogleComputeSSLPolicyRuleConfig is a config of GoogleComputeSSLPolicyRule +type GoogleComputeSSLPolicyRuleConfig struct { + AllowedVersions []string `hclext:"allowed_versions"` +} + +// NewGoogleComputeSSLPolicyRule returns a new rule +func NewGoogleComputeSSLPolicyRule() *GoogleComputeSSLPolicyRule { + return &GoogleComputeSSLPolicyRule{} +} + +// Name returns the rule name +func (r *GoogleComputeSSLPolicyRule) Name() string { + return "google_compute_ssl_policy" +} + +// Enabled returns whether the rule is enabled by default +func (r *GoogleComputeSSLPolicyRule) Enabled() bool { + // Need to configure `allowed_versions` + return false +} + +// Severity returns the rule severity +func (r *GoogleComputeSSLPolicyRule) Severity() tflint.Severity { + return tflint.WARNING +} + +// Link returns the rule reference link +func (r *GoogleComputeSSLPolicyRule) Link() string { + return "" +} + +// Check checks whether ... +func (r *GoogleComputeSSLPolicyRule) Check(runner tflint.Runner) error { + // This rule is an example to use custom rule config. + config := &GoogleComputeSSLPolicyRuleConfig{} + if err := runner.DecodeRuleConfig(r.Name(), config); err != nil { + return err + } + + resources, err := runner.GetResourceContent("google_compute_ssl_policy", &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "min_tls_version"}, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + attribute, exists := resource.Body.Attributes["min_tls_version"] + if !exists { + continue + } + + err := runner.EvaluateExpr(attribute.Expr, func (version string) error { + for _, allow := range config.AllowedVersions { + if version == allow { + return nil + } + } + return runner.EmitIssue( + r, + fmt.Sprintf(`"%s" is not allowed`, version), + attribute.Expr.Range(), + ) + }, nil) + if err != nil { + return err + } + } + + return nil +} diff --git a/rules/google_compute_ssl_policy_test.go b/rules/google_compute_ssl_policy_test.go new file mode 100644 index 0000000..d5f22b7 --- /dev/null +++ b/rules/google_compute_ssl_policy_test.go @@ -0,0 +1,68 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_GoogleComputeSSLPolicyRule(t *testing.T) { + tests := []struct { + Name string + Content string + Config string + Expected helper.Issues + }{ + { + Name: "issue found", + Content: ` +resource "google_compute_ssl_policy" "allowed" { + min_tls_version = "TLS_1_1" +}`, + Config: ` +rule "google_compute_ssl_policy" { + enabled = true + allowed_versions = ["TLS_1_2"] +}`, + Expected: helper.Issues{ + { + Rule: NewGoogleComputeSSLPolicyRule(), + Message: `"TLS_1_1" is not allowed`, + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 21}, + End: hcl.Pos{Line: 3, Column: 30}, + }, + }, + }, + }, + { + Name: "issue not found", + Content: ` +resource "google_compute_ssl_policy" "allowed" { + min_tls_version = "TLS_1_1" +}`, + Config: ` +rule "google_compute_ssl_policy" { + enabled = true + allowed_versions = ["TLS_1_1", "TLS_1_2"] +}`, + Expected: helper.Issues{}, + }, + } + + rule := NewGoogleComputeSSLPolicyRule() + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + runner := helper.TestRunner(t, map[string]string{"resource.tf": test.Content, ".tflint.hcl": test.Config}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssues(t, test.Expected, runner.Issues) + }) + } +} diff --git a/rules/terraform_backend_type.go b/rules/terraform_backend_type.go new file mode 100644 index 0000000..b7cd682 --- /dev/null +++ b/rules/terraform_backend_type.go @@ -0,0 +1,76 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// TerraformBackendTypeRule checks whether ... +type TerraformBackendTypeRule struct { + tflint.DefaultRule +} + +// NewTerraformBackendTypeRule returns a new rule +func NewTerraformBackendTypeRule() *TerraformBackendTypeRule { + return &TerraformBackendTypeRule{} +} + +// Name returns the rule name +func (r *TerraformBackendTypeRule) Name() string { + return "terraform_backend_type" +} + +// Enabled returns whether the rule is enabled by default +func (r *TerraformBackendTypeRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *TerraformBackendTypeRule) Severity() tflint.Severity { + return tflint.ERROR +} + +// Link returns the rule reference link +func (r *TerraformBackendTypeRule) Link() string { + return "" +} + +// Check checks whether ... +func (r *TerraformBackendTypeRule) Check(runner tflint.Runner) error { + // This rule is an example to get attributes of blocks other than resources. + content, err := runner.GetModuleContent(&hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "terraform", + Body: &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "backend", + LabelNames: []string{"type"}, + }, + }, + }, + }, + }, + }, nil) + if err != nil { + return err + } + + for _, terraform := range content.Blocks { + for _, backend := range terraform.Body.Blocks { + err := runner.EmitIssue( + r, + fmt.Sprintf("backend type is %s", backend.Labels[0]), + backend.DefRange, + ) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/rules/terraform_backend_type_test.go b/rules/terraform_backend_type_test.go new file mode 100644 index 0000000..d3a23ae --- /dev/null +++ b/rules/terraform_backend_type_test.go @@ -0,0 +1,53 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func Test_TerraformBackendType(t *testing.T) { + tests := []struct { + Name string + Content string + Expected helper.Issues + }{ + { + Name: "issue found", + Content: ` +terraform { + backend "s3" { + bucket = "mybucket" + key = "path/to/my/key" + region = "us-east-1" + } +}`, + Expected: helper.Issues{ + { + Rule: NewTerraformBackendTypeRule(), + Message: "backend type is s3", + Range: hcl.Range{ + Filename: "resource.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 15}, + }, + }, + }, + }, + } + + rule := NewTerraformBackendTypeRule() + + 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) + }) + } +}