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