terracop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +20 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +14 -0
  6. data/.travis.yml +7 -0
  7. data/CHANGELOG.md +14 -0
  8. data/Gemfile +10 -0
  9. data/Gemfile.lock +66 -0
  10. data/LICENSE.md +21 -0
  11. data/README.md +49 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/bin/terracop +52 -0
  16. data/default_config.yml +1 -0
  17. data/lib/terracop/cop/aws/describe_security_group_rules.rb +35 -0
  18. data/lib/terracop/cop/aws/ensure_tags.rb +51 -0
  19. data/lib/terracop/cop/aws/iam_role_policy.rb +47 -0
  20. data/lib/terracop/cop/aws/open_egress.rb +42 -0
  21. data/lib/terracop/cop/aws/open_ingress.rb +44 -0
  22. data/lib/terracop/cop/aws/open_ssh.rb +39 -0
  23. data/lib/terracop/cop/aws/security_group_rule_cop.rb +45 -0
  24. data/lib/terracop/cop/aws/unrestricted_egress_ports.rb +37 -0
  25. data/lib/terracop/cop/aws/unrestricted_ingress_ports.rb +38 -0
  26. data/lib/terracop/cop/aws/wide_egress.rb +53 -0
  27. data/lib/terracop/cop/aws/wide_ingress.rb +53 -0
  28. data/lib/terracop/cop/base.rb +105 -0
  29. data/lib/terracop/cop/style/dash_in_resource_name.rb +35 -0
  30. data/lib/terracop/cop/style/resource_type_in_name.rb +53 -0
  31. data/lib/terracop/cop/style/snake_case.rb +35 -0
  32. data/lib/terracop/formatters/default.rb +25 -0
  33. data/lib/terracop/formatters/html.rb +16 -0
  34. data/lib/terracop/formatters/json.rb +53 -0
  35. data/lib/terracop/formatters/report.html.erb +289 -0
  36. data/lib/terracop/plan_loader.rb +39 -0
  37. data/lib/terracop/runner.rb +50 -0
  38. data/lib/terracop/state_loader.rb +39 -0
  39. data/lib/terracop/version.rb +5 -0
  40. data/lib/terracop.rb +49 -0
  41. data/terracop.gemspec +45 -0
  42. metadata +200 -0
@@ -0,0 +1,38 @@
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 ingress security group rules that allow any port.
9
+ # Servers usually run multiple services that might open different ports,
10
+ # exposing them to a range of vulnerabilities. Only allow the specific
11
+ # ports you want to receive traffic on, and no more.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # resource "aws_security_group_rule" "ingress" {
16
+ # type = "ingress"
17
+ # from_port = 0
18
+ # to_port = 65535
19
+ # }
20
+ #
21
+ # # good
22
+ # resource "aws_security_group_rule" "ingress" {
23
+ # type = "ingress"
24
+ # from_port = 443
25
+ # to_port = 443
26
+ # }
27
+ class UnrestrictedIngressPorts < SecurityGroupRuleCop
28
+ register
29
+
30
+ def check
31
+ return unless ingress? && (tcp? || udp?) && any_port?
32
+
33
+ offense('Limit ingress traffic to small port ranges.', :security)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,53 @@
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 egress security group rules that allow very wide
9
+ # address ranges.
10
+ # This goes hand in hand with OpenEgress, but also warns against blocks
11
+ # like 10.0.0.0/8.
12
+ # Always pick the smallest possible choice of sources/destinations.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # resource "aws_security_group_rule" "egress" {
17
+ # type = "egress"
18
+ # cidr_blocks = ["10.0.0.0/8"]
19
+ # }
20
+ #
21
+ # # good
22
+ # resource "aws_security_group_rule" "egress" {
23
+ # type = "egress"
24
+ # cidr_blocks = ["10.4.3.0/24"]
25
+ # }
26
+ #
27
+ # # better
28
+ # resource "aws_security_group_rule" "egress" {
29
+ # type = "egress"
30
+ # security_group_id = aws_security_group.destination.id
31
+ # }
32
+ class WideEgress < SecurityGroupRuleCop
33
+ register
34
+
35
+ MSG = 'Avoid allowing egress traffic from wide address blocks ' \
36
+ '(%<cidr>s).'
37
+
38
+ def check
39
+ return unless egress?
40
+
41
+ attributes['cidr_blocks'].each do |cidr|
42
+ # Handled by OpenEgress
43
+ next if cidr == '0.0.0.0/0'
44
+
45
+ _, bits = cidr.split('/')
46
+
47
+ offense(format(MSG, cidr: cidr), :security) if bits.to_i < 16
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
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 ingress security group rules that allow very wide
9
+ # address ranges.
10
+ # This goes hand in hand with OpenIngress, but also warns against blocks
11
+ # like 10.0.0.0/8.
12
+ # Always pick the smallest possible choice of sources/destinations.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # resource "aws_security_group_rule" "ingress" {
17
+ # type = "ingress"
18
+ # cidr_blocks = ["10.0.0.0/8"]
19
+ # }
20
+ #
21
+ # # good
22
+ # resource "aws_security_group_rule" "ingress" {
23
+ # type = "ingress"
24
+ # cidr_blocks = ["10.4.3.0/24"]
25
+ # }
26
+ #
27
+ # # better
28
+ # resource "aws_security_group_rule" "ingress" {
29
+ # type = "ingress"
30
+ # security_group_id = aws_security_group.destination.id
31
+ # }
32
+ class WideIngress < SecurityGroupRuleCop
33
+ register
34
+
35
+ MSG = 'Avoid allowing ingress traffic from wide address blocks ' \
36
+ '(%<cidr>s).'
37
+
38
+ def check
39
+ return unless ingress?
40
+
41
+ attributes['cidr_blocks'].each do |cidr|
42
+ # Handled by OpenIngress
43
+ next if cidr == '0.0.0.0/0'
44
+
45
+ _, bits = cidr.split('/')
46
+
47
+ offense(format(MSG, cidr: cidr), :security) if bits.to_i < 16
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terracop
4
+ # This namespace holds all the individual Cop (checks).
5
+ module Cop
6
+ class << self
7
+ def all
8
+ @all ||= []
9
+ end
10
+
11
+ def run_for(type, name, index, attributes)
12
+ offenses = all.map do |cop|
13
+ cop.run(type, name, index, attributes)
14
+ end
15
+
16
+ offenses.flatten.compact
17
+ end
18
+ end
19
+
20
+ # Base class for all cops.
21
+ class Base
22
+ attr_accessor :type, :name, :index, :attributes
23
+
24
+ class << self
25
+ def run(type, name, index, attributes)
26
+ return unless applies_to?(type, name)
27
+
28
+ cop = new(type, name, index, attributes)
29
+ cop.check
30
+ cop.offenses
31
+ end
32
+
33
+ def cop_name
34
+ name.gsub(/^Terracop::Cop::/, '').gsub('::', '/')
35
+ end
36
+
37
+ def config
38
+ config = Terracop.config[cop_name] || {}
39
+ config['Enabled'] = config['Enabled'].nil? ? true : config['Enabled']
40
+ config
41
+ end
42
+
43
+ protected
44
+
45
+ def register
46
+ Terracop::Cop.all << self
47
+ end
48
+
49
+ def applies_to(*types)
50
+ @applies_to ||= []
51
+ @applies_to += types.map(&:to_s)
52
+ end
53
+
54
+ def applies_to?(type, name)
55
+ return unless enabled?
56
+ return unless @applies_to.nil? || @applies_to.include?(type)
57
+
58
+ !excludes?(type, name)
59
+ end
60
+
61
+ def enabled?
62
+ config['Enabled']
63
+ end
64
+
65
+ def excludes?(type, name)
66
+ excludes = config['Exclude'] || []
67
+ excludes.each do |rule|
68
+ return true if ["#{type}.*", "#{type}.#{name}"].include?(rule)
69
+ end
70
+
71
+ false
72
+ end
73
+ end
74
+
75
+ def initialize(type, name, index, attributes)
76
+ self.type = type
77
+ self.name = name
78
+ self.index = index
79
+ self.attributes = attributes
80
+ @offenses = []
81
+ end
82
+
83
+ def human_name
84
+ index ? "#{name}[#{index}]" : name
85
+ end
86
+
87
+ def check
88
+ message = "#{self.class.name} does not implement the #check method"
89
+ raise NotImplementedError, message
90
+ end
91
+
92
+ def offense(message, severity = :convention)
93
+ @offenses << {
94
+ cop_name: self.class.cop_name,
95
+ severity: severity,
96
+ type: type,
97
+ name: human_name,
98
+ message: message
99
+ }
100
+ end
101
+
102
+ attr_reader :offenses
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Style
8
+ # This cop checks for the use of dashes in terraform resource names.
9
+ # Terraform uses underscores for resource types and attributes.
10
+ # Using dashes for resource names makes for awkward combinations.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # resource "aws_security_group" "load-balancer" { }
15
+ #
16
+ # # good
17
+ # resource "aws_security_group" "load_balancer" { }
18
+ #
19
+ # @note
20
+ # When you rename a resource terraform will destroy and recreate it.
21
+ # Use `terraform mv` on the state file to avoid this from happening.
22
+ class DashInResourceName < Base
23
+ register
24
+
25
+ MSG = 'Use underscores in terraform resource names instead of dashes.'
26
+
27
+ def check
28
+ return unless name.index('-')
29
+
30
+ offense(MSG)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Style
8
+ # This cop checks terraform resource names that include the type in them.
9
+ # This makes for very long and redundant names.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # resource "aws_security_group" "load_balancer_security_group" { }
14
+ # resource "aws_security_group" "load_balancer_sg" { }
15
+ # resource "aws_security_group_rule" "ingress_rule" { }
16
+ #
17
+ # # good
18
+ # resource "aws_security_group" "load_balancer" { }
19
+ # resource "aws_security_group_rule" "ingress" { }
20
+ #
21
+ # @note
22
+ # When you rename a resource terraform will destroy and recreate it.
23
+ # Use `terraform mv` on the state file to avoid this from happening.
24
+ class ResourceTypeInName < Base
25
+ register
26
+
27
+ BLACKLIST = {
28
+ 'aws_alb' => %w[alb lb load_balancer],
29
+ 'aws_alb_listener' => %w[listener],
30
+ 'aws_alb_target_group' => %w[tg],
31
+ 'aws_autoscaling_group' => %w[asg],
32
+ 'aws_cloudwatch_metric_alarm' => %w[metric alarm],
33
+ 'aws_iam_instance_profile' => %w[profile],
34
+ 'aws_iam_role' => %w[role],
35
+ 'aws_iam_role_policy' => %w[policy],
36
+ 'aws_route53_record' => %w[record],
37
+ 'aws_security_group' => %w[sg group security_group],
38
+ 'aws_security_group_rule' => %w[rule]
39
+ }.freeze
40
+
41
+ def check
42
+ blacklist = BLACKLIST[type]
43
+ blacklist&.each do |word|
44
+ if name.downcase.gsub('-', '_').include?(word)
45
+ offense 'Do not include the resource type in its name.'
46
+ return
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terracop/cop/base'
4
+
5
+ module Terracop
6
+ module Cop
7
+ module Style
8
+ # This cop checks terraform resource names that are using CamelCase or
9
+ # in general, contain capital letters. Terraform prefers snake_case.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # resource "aws_security_group" "LoadBalancer" { }
14
+ # resource "aws_security_group" "app_ALB" { }
15
+ #
16
+ # # good
17
+ # resource "aws_security_group" "load_balancer" { }
18
+ # resource "aws_security_group_rule" "app_alb" { }
19
+ #
20
+ # @note
21
+ # When you rename a resource terraform will destroy and recreate it.
22
+ # Use `terraform mv` on the state file to avoid this from happening.
23
+ class SnakeCase < Base
24
+ register
25
+
26
+ def check
27
+ # Allow dashes here as there is another cop to complain about that.
28
+ return if name =~ /^[\da-z_\-]+$/
29
+
30
+ offense 'Use snake_case for resource names.'
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ module Terracop
6
+ module Formatters
7
+ # Default CLI-friendly output formatter.
8
+ class Default
9
+ def generate(resources)
10
+ out = []
11
+ resources.each do |resource, offenses|
12
+ out << "#{resource.cyan}:"
13
+
14
+ offenses.each do |offense|
15
+ out << "#{offense[:cop_name].yellow}: #{offense[:message]}"
16
+ end
17
+
18
+ out << ''
19
+ end
20
+
21
+ out.join("\n")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Terracop
6
+ module Formatters
7
+ # Generates a single page HTML report listing all the offenses.
8
+ # Ideal for human readable reports.
9
+ class Html
10
+ def generate(resources)
11
+ template = ERB.new(File.read(File.join(__dir__, './report.html.erb')))
12
+ template.result(binding)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Terracop
6
+ module Formatters
7
+ # Generates a JSON document listing all the offenses.
8
+ # Ideal to generate ouputs to be digested by other tools in a CI pipeline.
9
+ class Json
10
+ def generate(resources)
11
+ {
12
+ metadata: meta,
13
+ resources: build_resources(resources),
14
+ summary: {
15
+ offense_count: resources.values.map(&:count).sum,
16
+ resource_count: resources.count
17
+ }
18
+ }.to_json
19
+ end
20
+
21
+ private
22
+
23
+ def meta
24
+ {
25
+ terracop_version: Terracop::VERSION,
26
+ ruby_engine: RUBY_ENGINE,
27
+ ruby_version: RUBY_ENGINE_VERSION,
28
+ ruby_patchlevel: RUBY_PATCHLEVEL,
29
+ ruby_platform: RUBY_PLATFORM
30
+ }
31
+ end
32
+
33
+ def build_resources(resources)
34
+ resources.map do |resource, offenses|
35
+ {
36
+ resource: resource,
37
+ offsenses: build_offenses(offenses)
38
+ }
39
+ end
40
+ end
41
+
42
+ def build_offenses(offenses)
43
+ offenses.map do |offense|
44
+ {
45
+ severity: offense[:severity],
46
+ cop_name: offense[:cop_name],
47
+ message: offense[:message]
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end