terracop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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