terracop 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f057dfe056a3c276f9f3376612f36a92e6c4b7e0bf38a9a720240d63325f60b1
4
- data.tar.gz: 10f2142bf9bd9d9bca724e4f24a94fca78b11cb796a90663a7399ffa32d5939a
3
+ metadata.gz: 5fb344f85daa3aa3f6d3c3c4f0d436c0b4c0d4211e4fbc6ff3b19f44e1ea1148
4
+ data.tar.gz: 2bbff8adc3eeeedf696296e9ce90c4cbebe2ced06c781fe703f14477f8a58f93
5
5
  SHA512:
6
- metadata.gz: cf20e57e9125a9a03a296537e2b69e09e721872a4fb4ab54cd75e638f98f1d5a4c027bb054e9a30d066722e90abb9c98009dc7d597ce011fb60b64e31fd2914a
7
- data.tar.gz: aa5804ad797a3bfa980c8ca28085540f20dfdd52e9f1389e4ea98f75b38a6c6619e0280b13481b590631a447648bc7f674576642b31d0e4e05a1a83cea1aeb8f
6
+ metadata.gz: 9b695431e3b74dd944dc60f5df9fb70ac88c9884c2a51397ef1ad59539359efabdfdd767302faf2be925d31e61cc2540558deea6e5870029287a424fc7607ac7
7
+ data.tar.gz: a5b1ef53d62e241551a38aaab925df92c3811355fd42a66075e56247d78c43f3162b356a06125b8d03a176538d1ba270cc652a4c3715f9837e0cb56b645b1e3f
data/.rubocop.yml CHANGED
@@ -12,3 +12,11 @@ Style/AsciiComments:
12
12
 
13
13
  RSpec/NestedGroups:
14
14
  Max: 7
15
+
16
+ Style/GuardClause:
17
+ # Usually good, sometimes dumb.
18
+ Enabled: false
19
+
20
+ RSpec/SubjectStub:
21
+ # Sometimes, there's no other way.
22
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -4,7 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## 0.1.0
7
+ ## 0.2.0 (2019-12-28)
8
+
9
+ - New cops:
10
+ - Aws/BadPasswordPolicy
11
+ - Aws/EnsurePropagatedTags
12
+ - Aws/FaultIntolerant
13
+ - Aws/IamInlinePolicy
14
+ - Aws/IamPolicyAttachment
15
+ - Aws/PreferLaunchTemplates
16
+ - Deprecated cop: IamRolePolicy.
17
+ - Improve test coverage.
18
+ - Improve Aws/EnsureTags to handle weird resources.
19
+ - Ignore deletions when analyzing plans.
20
+
21
+ ## 0.1.1 (2019-12-22)
22
+
23
+ - Exit with failure when offenses are found.
24
+ - Add `terracop` executable.
25
+
26
+ ## 0.1.0 (2019-12-22)
8
27
 
9
28
  - Initial release.
10
29
  - Can load Terraform 0.12 state and plan files.
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Terracop
2
2
 
3
- Terracop is a HashiCorp [Terraform](https://www.terraform.io/) state / plan
4
- parser and analyzer. Put it in a CI pipeline to analyze your Terraform plans
5
- or run it on already applied states and see what could be improved.
3
+ Terracop is an opinionated HashiCorp [Terraform](https://www.terraform.io/)
4
+ state / plan parser and analyzer. Put it in a CI pipeline to analyze your
5
+ Terraform plans or run it on already applied states and see what could be
6
+ improved.
6
7
 
7
8
  The checks run by Terracop go anywhere from resource names guidelines to
8
9
  identifying security holes in your configuration.
@@ -33,8 +34,8 @@ like this:
33
34
 
34
35
  $ terracop --state path/to/state/file
35
36
 
36
- Terracop can (will) parse also terraform plan files, in order to report
37
- potential issues before you apply the plan and make the problem permanent. Eg:
37
+ Terracop can also parse terraform plan files, in order to report potential
38
+ issues before you apply the plan and persist the problem. Eg:
38
39
 
39
40
  $ terraform plan -out tfplan
40
41
  $ terracop --plan tfplan
data/lib/terracop.rb CHANGED
@@ -3,12 +3,17 @@
3
3
  require 'json'
4
4
  require 'yaml'
5
5
 
6
+ require 'terracop/cop/aws/bad_password_policy'
6
7
  require 'terracop/cop/aws/describe_security_group_rules'
8
+ require 'terracop/cop/aws/ensure_propagated_tags'
7
9
  require 'terracop/cop/aws/ensure_tags'
8
- require 'terracop/cop/aws/iam_role_policy'
10
+ require 'terracop/cop/aws/fault_intolerant'
11
+ require 'terracop/cop/aws/iam_inline_policy'
12
+ require 'terracop/cop/aws/iam_policy_attachment'
9
13
  require 'terracop/cop/aws/open_egress'
10
14
  require 'terracop/cop/aws/open_ingress'
11
15
  require 'terracop/cop/aws/open_ssh'
16
+ require 'terracop/cop/aws/prefer_launch_templates'
12
17
  require 'terracop/cop/aws/unrestricted_egress_ports'
13
18
  require 'terracop/cop/aws/unrestricted_ingress_ports'
14
19
  require 'terracop/cop/aws/wide_egress'
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Aws
8
+ # This cop warns against a password policy that goes against industry
9
+ # best practices. Ideally the password policy should be strict enough
10
+ # to require the use of a password manager, and never expire passwords.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # resource "aws_iam_account_password_policy" "policy" {
15
+ # minimum_password_length = 4
16
+ # require_lowercase_characters = true
17
+ # require_numbers = true
18
+ # allow_users_to_change_password = false
19
+ # max_password_age = 7
20
+ # }
21
+ #
22
+ # # good
23
+ # resource "aws_iam_account_password_policy" "policy" {
24
+ # minimum_password_length = 20
25
+ # require_lowercase_characters = true
26
+ # require_uppercase_characters = true
27
+ # require_numbers = true
28
+ # require_symbols = true
29
+ # allow_users_to_change_password = true
30
+ # }
31
+ class BadPasswordPolicy < Base
32
+ register
33
+ applies_to :aws_iam_account_password_policy
34
+
35
+ def check
36
+ check_length
37
+ check_characters
38
+ check_age
39
+ end
40
+
41
+ def check_length
42
+ length = attributes['minimum_password_length']
43
+ if length && length < 14
44
+ offense('Set the minimum password length policy to at least 14.')
45
+ end
46
+ end
47
+
48
+ def check_characters
49
+ if !attributes['require_uppercase_characters'] ||
50
+ !attributes['require_lowercase_characters']
51
+ offense('Require both lowercase and uppercase characters.')
52
+ end
53
+
54
+ unless attributes['require_numbers']
55
+ offense('Require numbers in passwords.')
56
+ end
57
+
58
+ unless attributes['require_symbols']
59
+ offense('Require symbols in passwords.')
60
+ end
61
+ end
62
+
63
+ def check_age
64
+ age = attributes['max_password_age']
65
+ if age && age < 90
66
+ offense('Expiring passwords is discouraged. If you really have ' \
67
+ 'to, do not do it more than once every 3 months.')
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Aws
8
+ # This cop makes sure that EC2 instances launched by Autoscaling Groups
9
+ # also have tags. It is configured like the Aws/EnsureTags cop.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # resource "aws_asg" "asg" {
14
+ # tag {
15
+ # key = "Name"
16
+ # value = "asg"
17
+ # propagate_at_launch = false
18
+ # }
19
+ # }
20
+ #
21
+ # # good
22
+ # resource "aws_asg" "asg" {
23
+ # tag {
24
+ # key = "Name"
25
+ # value = "asg"
26
+ # propagate_at_launch = true
27
+ # }
28
+ # }
29
+ class EnsurePropagatedTags < Base
30
+ register
31
+ applies_to :aws_autoscaling_group
32
+
33
+ def check
34
+ if self.class.config['Required']
35
+ check_required(propagated_tags, self.class.config['Required'])
36
+ elsif propagated_tags.empty?
37
+ offense 'The EC2 instances launched by this Autoscaling group ' \
38
+ 'will not have any tags.'
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def propagated_tags
45
+ tags = attributes['tags'] || attributes['tag']
46
+ tags.select { |t| t['propagate_at_launch'] }
47
+ end
48
+
49
+ def check_required(tags, required_tags)
50
+ required_tags.each do |key|
51
+ unless tags.find { |t| t['key'] == key }
52
+ offense "Required tag \"#{key}\" is not propagated to EC2 " \
53
+ 'instances launched by this Autoscaling Group.'
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -27,24 +27,42 @@ module Terracop
27
27
  register
28
28
 
29
29
  def check
30
- return unless attributes['tags']
30
+ tags = tags_for(attributes)
31
+ return unless tags
31
32
 
32
33
  if self.class.config['Required']
33
- check_required(self.class.config['Required'])
34
- elsif attributes['tags'].empty?
34
+ check_required(tags, self.class.config['Required'])
35
+ elsif tags.empty?
35
36
  offense 'Tag resources properly.'
36
37
  end
37
38
  end
38
39
 
39
40
  private
40
41
 
41
- def check_required(required_tags)
42
+ def check_required(tags, required_tags)
42
43
  required_tags.each do |key|
43
- unless attributes['tags'][key]
44
+ unless tag(tags, key)
44
45
  offense "Required tag \"#{key}\" is missing on this resource."
45
46
  end
46
47
  end
47
48
  end
49
+
50
+ def tags_for(attributes)
51
+ case type
52
+ when 'aws_autoscaling_group'
53
+ attributes['tags'] || attributes['tag']
54
+ else
55
+ attributes['tags']
56
+ end
57
+ end
58
+
59
+ def tag(list, name)
60
+ if list.is_a?(Hash)
61
+ list[name]
62
+ else
63
+ list.find { |tag| tag['key'] == name }
64
+ end
65
+ end
48
66
  end
49
67
  end
50
68
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Aws
8
+ # This cop checks for Autoscaling Groups that can only launch instances
9
+ # in a specific Availability Zone. This creates an availability risk,
10
+ # as if that AZ is lost, the ASG will not be able to launch instances
11
+ # anywhere else.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # resource "aws_autoscaling_group" "asg" {
16
+ # vpc_zone_identifier = ["subnet-123"]
17
+ # }
18
+ #
19
+ # # good
20
+ # resource "aws_autoscaling_group" "asg" {
21
+ # # Note that to pass this cop, the two subnets must live in
22
+ # # different AZs.
23
+ # vpc_zone_identifier = ["subnet-123", "subnet-456"]
24
+ # }
25
+ class FaultIntolerant < Base
26
+ register
27
+ applies_to :aws_autoscaling_group
28
+
29
+ MSG = 'This Autoscaling Group can launch instances in only one AZ ' \
30
+ '(%<az>s). This setup would not tolerate the loss of that AZ.'
31
+
32
+ def check
33
+ return unless attributes['availability_zones'].count < 2
34
+
35
+ offense(format(MSG, az: attributes['availability_zones'][0]))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -5,7 +5,7 @@ require 'terracop/cop/base'
5
5
  module Terracop
6
6
  module Cop
7
7
  module Aws
8
- # This cop warns against the use of inline role policies.
8
+ # This cop warns against the use of inline group/role/user policies.
9
9
  # Inline policies tend to be copy/pasted, sometimes with minor changes
10
10
  # and are not shown in the "Policies" tab of AWS IAM.
11
11
  #
@@ -33,13 +33,15 @@ module Terracop
33
33
  # role = aws_iam_role.role.name
34
34
  # policy_arn = aws_iam_policy.policy.arn
35
35
  # }
36
- class IamRolePolicy < Base
36
+ class IamInlinePolicy < Base
37
37
  register
38
- applies_to :aws_iam_role_policy
38
+ applies_to :aws_iam_group_policy, :aws_iam_role_policy,
39
+ :aws_iam_user_policy
39
40
 
40
41
  def check
41
- offense('Use aws_iam_role_policy_attachment instead of attaching ' \
42
- 'inline policies with aws_iam_role_policy.')
42
+ entity = type.scan(/aws_iam_(.+)_policy/).first.first
43
+ offense("Use aws_iam_#{entity}_policy_attachment instead of " \
44
+ "attaching inline policies with aws_iam_#{entity}_policy.")
43
45
  end
44
46
  end
45
47
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Aws
8
+ # This cop warns against the use of an evil all encompassing
9
+ # aws_iam_policy_attachment.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # resource "aws_iam_policy_attachment" "attach" {
14
+ # name = "test-attachment"
15
+ # policy_arn = aws_iam_policy.policy.arn
16
+ # users = [aws_iam_user.user.name]
17
+ # roles = [aws_iam_role.role.name]
18
+ # groups = [aws_iam_group.group.name]
19
+ # }
20
+ #
21
+ # # good
22
+ # resource "aws_iam_role_policy_attachment" "attach" {
23
+ # role = aws_iam_role.role.name
24
+ # policy_arn = aws_iam_policy.policy.arn
25
+ # }
26
+ #
27
+ # resource "aws_iam_user_policy_attachment" "attach" {
28
+ # user = aws_iam_user.user.name
29
+ # policy_arn = aws_iam_policy.policy.arn
30
+ # }
31
+ #
32
+ # resource "aws_iam_group_policy_attachment" "attach" {
33
+ # group = aws_iam_group.user.name
34
+ # policy_arn = aws_iam_policy.policy.arn
35
+ # }
36
+ class IamPolicyAttachment < Base
37
+ register
38
+ applies_to :iam_policy_attachment
39
+
40
+ def check
41
+ offense('Use aws_iam_role_policy_attachment, ' \
42
+ 'aws_iam_user_policy_attachment, or ' \
43
+ 'aws_iam_group_policy_attachment instead.')
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/aws/security_group_rule_cop'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Aws
8
+ # This cop warns against an ingress rule from 0.0.0.0/0 on port 22 (SSH).
9
+ # That is a Very Bad Idea™.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # resource "aws_launch_configuration" "lc" {}
14
+ #
15
+ # resource "aws_autoscaling_group" "asg" {
16
+ # launch_configuration = aws_launch_configuration.lc.name
17
+ # }
18
+ #
19
+ # # good
20
+ # resource "aws_launch_template" "tpl" {}
21
+ #
22
+ # resource "aws_autoscaling_group" "asg" {
23
+ # launch_template {
24
+ # id = aws_launch_template.tpl.id
25
+ # version = "$Latest"
26
+ # }
27
+ # }
28
+ class PreferLaunchTemplates < SecurityGroupRuleCop
29
+ register
30
+ applies_to :aws_launch_configuration, :aws_autoscaling_group
31
+
32
+ def check
33
+ if type == 'aws_launch_configuration' ||
34
+ attributes['launch_configuration']
35
+ offense('Prefer Launch Templates to Launch Configurations.')
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -9,7 +9,8 @@ module Terracop
9
9
  plan = decode(file)
10
10
 
11
11
  changed_resources = plan['resource_changes'].reject! do |resource|
12
- resource['change']['actions'] == ['no-op']
12
+ resource['change']['actions'] == ['no-op'] ||
13
+ resource['change']['actions'] == ['delete']
13
14
  end
14
15
 
15
16
  restruct_resources(changed_resources)
@@ -17,12 +18,14 @@ module Terracop
17
18
 
18
19
  private
19
20
 
21
+ # :nocov:
20
22
  def decode(file)
21
23
  JSON.parse(`terraform show -json #{file}`)
22
24
  rescue JSON::ParserError
23
25
  puts 'Terraform failed to decode the plan file.'
24
26
  exit
25
27
  end
28
+ # :nocov:
26
29
 
27
30
  def restruct_resources(resources)
28
31
  resources.map do |resource|
@@ -3,50 +3,59 @@
3
3
  module Terracop
4
4
  # Executes Terracop on a given state file.
5
5
  class Runner
6
- attr_accessor :state
7
-
8
6
  def initialize(type, file, formatter)
9
7
  @formatter = Formatters.const_get(formatter.to_s.capitalize).new
10
8
 
11
- if file
12
- load_state_from_file(type, file)
13
- else
14
- load_state_from_terraform
15
- end
9
+ @type = type
10
+ @file = file
16
11
  end
17
12
 
18
13
  def run
19
- offenses = @state.map do |instance|
14
+ offenses = state.map do |instance|
20
15
  Terracop::Cop.run_for(
21
16
  instance[:type], instance[:name],
22
17
  instance[:index], instance[:attributes]
23
18
  )
24
19
  end
25
20
 
26
- by_res = offenses.flatten.group_by { |o| "#{o[:type]}.#{o[:name]}" }
27
- print @formatter.generate(by_res)
21
+ print formatted(offenses)
28
22
 
29
23
  offenses.flatten.count
30
24
  end
31
25
 
26
+ def state
27
+ @state ||= begin
28
+ if @file
29
+ load_state_from_file(@type, @file)
30
+ else
31
+ load_state_from_terraform
32
+ end
33
+ end
34
+ end
35
+
32
36
  private
33
37
 
34
38
  def load_state_from_file(type, file)
35
- @state = if type == :plan
36
- PlanLoader.load(file)
37
- else
38
- StateLoader.load(file)
39
- end
39
+ if type == :plan
40
+ PlanLoader.load(file)
41
+ else
42
+ StateLoader.load(file)
43
+ end
40
44
  end
41
45
 
42
46
  # :nocov:
43
47
  def load_state_from_terraform
44
- @state = StateLoader.load_from_text(`terraform state pull`)
48
+ StateLoader.load_from_text(`terraform state pull`)
45
49
  rescue JSON::ParserError
46
50
  puts 'Run terracop somewhere with a state file or pass it directly ' \
47
51
  'with --state FILE'
48
52
  exit
49
53
  end
50
54
  # :nocov:
55
+
56
+ def formatted(offenses)
57
+ by_res = offenses.flatten.group_by { |o| "#{o[:type]}.#{o[:name]}" }
58
+ @formatter.generate(by_res)
59
+ end
51
60
  end
52
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Terracop
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terracop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francesco Boffa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-22 00:00:00.000000000 Z
11
+ date: 2019-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -148,12 +148,17 @@ files:
148
148
  - default_config.yml
149
149
  - exe/terracop
150
150
  - lib/terracop.rb
151
+ - lib/terracop/cop/aws/bad_password_policy.rb
151
152
  - lib/terracop/cop/aws/describe_security_group_rules.rb
153
+ - lib/terracop/cop/aws/ensure_propagated_tags.rb
152
154
  - lib/terracop/cop/aws/ensure_tags.rb
153
- - lib/terracop/cop/aws/iam_role_policy.rb
155
+ - lib/terracop/cop/aws/fault_intolerant.rb
156
+ - lib/terracop/cop/aws/iam_inline_policy.rb
157
+ - lib/terracop/cop/aws/iam_policy_attachment.rb
154
158
  - lib/terracop/cop/aws/open_egress.rb
155
159
  - lib/terracop/cop/aws/open_ingress.rb
156
160
  - lib/terracop/cop/aws/open_ssh.rb
161
+ - lib/terracop/cop/aws/prefer_launch_templates.rb
157
162
  - lib/terracop/cop/aws/security_group_rule_cop.rb
158
163
  - lib/terracop/cop/aws/unrestricted_egress_ports.rb
159
164
  - lib/terracop/cop/aws/unrestricted_ingress_ports.rb
@@ -194,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
199
  - !ruby/object:Gem::Version
195
200
  version: '0'
196
201
  requirements: []
197
- rubygems_version: 3.0.6
202
+ rubygems_version: 3.1.2
198
203
  signing_key:
199
204
  specification_version: 4
200
205
  summary: Automatic Terraform state/plan checking tool