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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +20 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.md +21 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/terracop +52 -0
- data/default_config.yml +1 -0
- data/lib/terracop/cop/aws/describe_security_group_rules.rb +35 -0
- data/lib/terracop/cop/aws/ensure_tags.rb +51 -0
- data/lib/terracop/cop/aws/iam_role_policy.rb +47 -0
- data/lib/terracop/cop/aws/open_egress.rb +42 -0
- data/lib/terracop/cop/aws/open_ingress.rb +44 -0
- data/lib/terracop/cop/aws/open_ssh.rb +39 -0
- data/lib/terracop/cop/aws/security_group_rule_cop.rb +45 -0
- data/lib/terracop/cop/aws/unrestricted_egress_ports.rb +37 -0
- data/lib/terracop/cop/aws/unrestricted_ingress_ports.rb +38 -0
- data/lib/terracop/cop/aws/wide_egress.rb +53 -0
- data/lib/terracop/cop/aws/wide_ingress.rb +53 -0
- data/lib/terracop/cop/base.rb +105 -0
- data/lib/terracop/cop/style/dash_in_resource_name.rb +35 -0
- data/lib/terracop/cop/style/resource_type_in_name.rb +53 -0
- data/lib/terracop/cop/style/snake_case.rb +35 -0
- data/lib/terracop/formatters/default.rb +25 -0
- data/lib/terracop/formatters/html.rb +16 -0
- data/lib/terracop/formatters/json.rb +53 -0
- data/lib/terracop/formatters/report.html.erb +289 -0
- data/lib/terracop/plan_loader.rb +39 -0
- data/lib/terracop/runner.rb +50 -0
- data/lib/terracop/state_loader.rb +39 -0
- data/lib/terracop/version.rb +5 -0
- data/lib/terracop.rb +49 -0
- data/terracop.gemspec +45 -0
- 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
|