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,289 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset='UTF-8' />
5
+ <title>Terracop Inspection Report</title>
6
+ <style>
7
+ * {
8
+ -webkit-box-sizing: border-box;
9
+ -moz-box-sizing: border-box;
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ body, html {
14
+ font-size: 62.5%;
15
+ }
16
+ body {
17
+ background-color: #ecedf0;
18
+ font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
19
+ margin: 0;
20
+ }
21
+ code {
22
+ font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
23
+ font-size: 85%;
24
+ }
25
+ #header {
26
+ background: #f9f9f9;
27
+ color: #333;
28
+ border-bottom: 3px solid #ccc;
29
+ height: 50px;
30
+ padding: 0;
31
+ }
32
+ #header .logo {
33
+ float: left;
34
+ margin: 5px 12px 7px 20px;
35
+ width: 38px;
36
+ height: 38px;
37
+ }
38
+ #header .title {
39
+ display: inline-block;
40
+ float: left;
41
+ height: 50px;
42
+ font-size: 2.4rem;
43
+ letter-spacing: normal;
44
+ line-height: 50px;
45
+ margin: 0;
46
+ }
47
+
48
+ .information, #offenses {
49
+ width: 100%;
50
+ padding: 20px;
51
+ color: #333;
52
+ }
53
+ #offenses {
54
+ padding: 0 20px;
55
+ }
56
+
57
+ .information .infobox {
58
+ border-left: 3px solid;
59
+ border-radius: 4px;
60
+ background-color: #fff;
61
+ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
62
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
63
+ padding: 15px;
64
+ border-color: #0088cc;
65
+ font-size: 1.4rem;
66
+ }
67
+ .information .infobox .info-title {
68
+ font-size: 1.8rem;
69
+ line-height: 2.2rem;
70
+ margin: 0 0 0.5em;
71
+ }
72
+ .information .offenses-list li {
73
+ line-height: 1.8rem
74
+ }
75
+ .information .offenses-list {
76
+ padding-left: 20px;
77
+ margin-bottom: 0;
78
+ }
79
+
80
+ #offenses .offense-box {
81
+ border-radius: 4px;
82
+ margin-bottom: 20px;
83
+ background-color: #fff;
84
+ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
85
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
86
+ }
87
+ .fixed .box-title {
88
+ position: fixed;
89
+ top: 0;
90
+ z-index: 10;
91
+ width: 100%;
92
+ }
93
+ .box-title-placeholder {
94
+ display: none;
95
+ }
96
+ .fixed .box-title-placeholder {
97
+ display: block;
98
+ }
99
+ #offenses .offense-box .box-title h3, #offenses .offense-box .box-title-placeholder h3 {
100
+ color: #33353f;
101
+ background-color: #f6f6f6;
102
+ font-size: 2rem;
103
+ line-height: 2rem;
104
+ display: block;
105
+ padding: 15px;
106
+ border-radius: 5px;
107
+ margin: 0;
108
+ }
109
+ #offenses .offense-box .offense-reports {
110
+ padding: 0 15px;
111
+ }
112
+ #offenses .offense-box .offense-reports .report {
113
+ border-bottom: 1px dotted #ddd;
114
+ padding: 15px 0px;
115
+ position: relative;
116
+ font-size: 1.3rem;
117
+ }
118
+ #offenses .offense-box .offense-reports .report:last-child {
119
+ border-bottom: none;
120
+ }
121
+ #offenses .offense-box .offense-reports .report pre code {
122
+ display: block;
123
+ background: #000;
124
+ color: #fff;
125
+ padding: 10px 15px;
126
+ border-radius: 5px;
127
+ line-height: 1.6rem;
128
+ }
129
+ #offenses .offense-box .offense-reports .report .location {
130
+ font-weight: bold;
131
+ }
132
+ #offenses .offense-box .offense-reports .report .message code {
133
+ padding: 0.3em;
134
+ background-color: rgba(0,0,0,0.07);
135
+ border-radius: 3px;
136
+ }
137
+ .severity {
138
+ text-transform: capitalize;
139
+ font-weight: bold;
140
+ }
141
+ .highlight {
142
+ padding: 2px;
143
+ border-radius: 2px;
144
+ font-weight: bold;
145
+ }
146
+
147
+ .severity.refactor {
148
+ color: rgba(237, 156, 40, 1.0);
149
+ }
150
+ .highlight.refactor {
151
+ background-color: rgba(237, 156, 40, 0.6);
152
+ border: 1px solid rgba(237, 156, 40, 0.4);
153
+ }
154
+
155
+ .severity.convention {
156
+ color: rgba(237, 156, 40, 1.0);
157
+ }
158
+ .severity.security {
159
+ color: red;
160
+ }
161
+ .highlight.convention {
162
+ background-color: rgba(237, 156, 40, 0.6);
163
+ border: 1px solid rgba(237, 156, 40, 0.4);
164
+ }
165
+
166
+ .severity.warning {
167
+ color: rgba(150, 40, 239, 1.0);
168
+ }
169
+ .highlight.warning {
170
+ background-color: rgba(150, 40, 239, 0.6);
171
+ border: 1px solid rgba(150, 40, 239, 0.4);
172
+ }
173
+
174
+ .severity.error {
175
+ color: rgba(210, 50, 45, 1.0);
176
+ }
177
+ .highlight.error {
178
+ background-color: rgba(210, 50, 45, 0.6);
179
+ border: 1px solid rgba(210, 50, 45, 0.4);
180
+ }
181
+
182
+ .severity.fatal {
183
+ color: rgba(210, 50, 45, 1.0);
184
+ }
185
+ .highlight.fatal {
186
+ background-color: rgba(210, 50, 45, 0.6);
187
+ border: 1px solid rgba(210, 50, 45, 0.4);
188
+ }
189
+
190
+ footer {
191
+ margin-bottom: 20px;
192
+ margin-right: 20px;
193
+ font-size: 1.3rem;
194
+ color: #777;
195
+ text-align: right;
196
+ }
197
+ .extra-code {
198
+ color: #ED9C28
199
+ }
200
+ </style>
201
+
202
+ <script>
203
+ (function() {
204
+ // floating headers. requires classList support.
205
+ if (!('classList' in document.createElement("_"))) return;
206
+
207
+ var loaded = false,
208
+ boxes,
209
+ boxPositions;
210
+
211
+ window.onload = function() {
212
+ var scrollY = window.scrollY;
213
+ boxes = document.querySelectorAll('.offense-box');
214
+ boxPositions = [];
215
+ for (var i = 0; i < boxes.length; i++)
216
+ // need to add scrollY because the page might be somewhere other than the top when loaded.
217
+ boxPositions[i] = boxes[i].getBoundingClientRect().top + scrollY;
218
+ loaded = true;
219
+ };
220
+
221
+ window.onscroll = function() {
222
+ if (!loaded) return;
223
+ var i,
224
+ idx,
225
+ scrollY = window.scrollY;
226
+ for (i = 0; i < boxPositions.length; i++) {
227
+ if (scrollY <= boxPositions[i] - 1) {
228
+ idx = i;
229
+ break;
230
+ }
231
+ }
232
+ if (typeof idx == 'undefined') idx = boxes.length;
233
+ if (idx > 0)
234
+ boxes[idx - 1].classList.add('fixed');
235
+ for (i = 0; i < boxes.length; i++) {
236
+ if (i < idx) continue;
237
+ boxes[i].classList.remove('fixed');
238
+ }
239
+ };
240
+ })();
241
+ </script>
242
+ </head>
243
+ <body>
244
+ <div id="header">
245
+ <h1 class="title">TerraCop Inspection Report</h1>
246
+ </div>
247
+ <div class="information">
248
+ <div class="infobox">
249
+ <div class="total">
250
+ <%= resources.count %> resources inspected,
251
+ <%= resources.values.map(&:count).sum %> offenses detected:
252
+ </div>
253
+ <ul class="offenses-list">
254
+ <% resources.each do |resource, offenses| %>
255
+ <li>
256
+ <a href="#<%= resource %>">
257
+ <%= resource %> - <%= offenses.count %> offenses
258
+ </a>
259
+ </li>
260
+ <% end %>
261
+ </ul>
262
+ </div>
263
+ </div>
264
+
265
+ <div id="offenses">
266
+ <% resources.each do |resource, offenses| %>
267
+ <div class="offense-box" id="<%= resource %>">
268
+ <div class="box-title-placeholder"><h3>&nbsp;</h3></div>
269
+ <div class="box-title"><h3><%= resource %> - <%= offenses.count %> offenses</h3></div>
270
+ <div class="offense-reports">
271
+ <% offenses.each do |offense| %>
272
+ <div class="report">
273
+ <div class="meta">
274
+ <span class="severity <%= offense[:severity] %>"><%= offense[:severity] %>:</span>
275
+ <span class="message"><b><%= offense[:cop_name] %></b>: <%= offense[:message] %></span>
276
+ </div>
277
+ </div>
278
+ <% end %>
279
+ </div>
280
+ </div>
281
+ <% end %>
282
+ </div>
283
+
284
+ <footer>
285
+ Generated by <a href="https://github.com/aomega08/terracop">TerraCop</a>
286
+ <span class="version"><%= Terracop::VERSION %></span>
287
+ </footer>
288
+ </body>
289
+ </html>
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terracop
4
+ # Loads a Terraform plan file and transforms it into a Terracop-friendly list
5
+ # of instances.
6
+ class PlanLoader
7
+ class << self
8
+ def load(file)
9
+ plan = decode(file)
10
+
11
+ changed_resources = plan['resource_changes'].reject! do |resource|
12
+ resource['change']['actions'] == ['no-op']
13
+ end
14
+
15
+ restruct_resources(changed_resources)
16
+ end
17
+
18
+ private
19
+
20
+ def decode(file)
21
+ JSON.parse(`terraform show -json #{file}`)
22
+ rescue JSON::ParserError
23
+ puts 'Terraform failed to decode the plan file.'
24
+ exit
25
+ end
26
+
27
+ def restruct_resources(resources)
28
+ resources.map do |resource|
29
+ {
30
+ type: resource['type'],
31
+ name: resource['name'],
32
+ index: resource['index'],
33
+ attributes: resource['change']['after']
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terracop
4
+ # Executes Terracop on a given state file.
5
+ class Runner
6
+ attr_accessor :state
7
+
8
+ def initialize(type, file, formatter)
9
+ @formatter = Formatters.const_get(formatter.to_s.capitalize).new
10
+
11
+ if file
12
+ load_state_from_file(type, file)
13
+ else
14
+ load_state_from_terraform
15
+ end
16
+ end
17
+
18
+ def run
19
+ offenses = @state.map do |instance|
20
+ Terracop::Cop.run_for(
21
+ instance[:type], instance[:name],
22
+ instance[:index], instance[:attributes]
23
+ )
24
+ end
25
+
26
+ by_res = offenses.flatten.group_by { |o| "#{o[:type]}.#{o[:name]}" }
27
+ print @formatter.generate(by_res)
28
+ end
29
+
30
+ private
31
+
32
+ def load_state_from_file(type, file)
33
+ @state = if type == :plan
34
+ PlanLoader.load(file)
35
+ else
36
+ StateLoader.load(file)
37
+ end
38
+ end
39
+
40
+ # :nocov:
41
+ def load_state_from_terraform
42
+ @state = StateLoader.load_from_text(`terraform state pull`)
43
+ rescue JSON::ParserError
44
+ puts 'Run terracop somewhere with a state file or pass it directly ' \
45
+ 'with --state FILE'
46
+ exit
47
+ end
48
+ # :nocov:
49
+ end
50
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terracop
4
+ # Loads a Terraform state file and transforms it into a Terracop-friendly list
5
+ # of instances.
6
+ class StateLoader
7
+ class << self
8
+ def load(file)
9
+ state = File.read(file)
10
+ load_from_text(state)
11
+ end
12
+
13
+ def load_from_text(text)
14
+ state = JSON.parse(text)
15
+
16
+ managed_resources = state['resources'].select do |resource|
17
+ resource['mode'] == 'managed'
18
+ end
19
+
20
+ flatten_instances(managed_resources)
21
+ end
22
+
23
+ private
24
+
25
+ def flatten_instances(resources)
26
+ resources.map do |resource|
27
+ resource['instances'].map do |instance|
28
+ {
29
+ type: resource['type'],
30
+ name: resource['name'],
31
+ index: instance['index_key'],
32
+ attributes: instance['attributes']
33
+ }
34
+ end
35
+ end.flatten
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terracop
4
+ VERSION = '0.1.0'
5
+ end
data/lib/terracop.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ require 'terracop/cop/aws/describe_security_group_rules'
7
+ require 'terracop/cop/aws/ensure_tags'
8
+ require 'terracop/cop/aws/iam_role_policy'
9
+ require 'terracop/cop/aws/open_egress'
10
+ require 'terracop/cop/aws/open_ingress'
11
+ require 'terracop/cop/aws/open_ssh'
12
+ require 'terracop/cop/aws/unrestricted_egress_ports'
13
+ require 'terracop/cop/aws/unrestricted_ingress_ports'
14
+ require 'terracop/cop/aws/wide_egress'
15
+ require 'terracop/cop/aws/wide_ingress'
16
+
17
+ require 'terracop/cop/style/dash_in_resource_name'
18
+ require 'terracop/cop/style/resource_type_in_name'
19
+ require 'terracop/cop/style/snake_case'
20
+
21
+ require 'terracop/formatters/default'
22
+ require 'terracop/formatters/html'
23
+ require 'terracop/formatters/json'
24
+
25
+ require 'terracop/plan_loader'
26
+ require 'terracop/runner'
27
+ require 'terracop/state_loader'
28
+ require 'terracop/version'
29
+
30
+ # Wrapper module for the gem.
31
+ module Terracop
32
+ class Error < StandardError; end
33
+
34
+ class << self
35
+ def config
36
+ @config ||= begin
37
+ defaults_path = File.join(__dir__, '../default_config.yml')
38
+ overrides_path = '.terracop.yml'
39
+
40
+ config = YAML.safe_load(File.read(defaults_path)) || {}
41
+ if File.exist?(overrides_path)
42
+ config.merge!(YAML.safe_load(File.read(overrides_path)) || {})
43
+ end
44
+
45
+ config
46
+ end
47
+ end
48
+ end
49
+ end
data/terracop.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'terracop/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'terracop'
9
+ spec.version = Terracop::VERSION
10
+ spec.authors = ['Francesco Boffa']
11
+ spec.email = ['fra.boffa@gmail.com']
12
+ spec.license = 'MIT'
13
+
14
+ spec.summary = 'Automatic Terraform state/plan checking tool'
15
+ spec.description = <<-DESCRIPTION
16
+ Automatic Terraform state/plan checking tool.
17
+ Aims to enforce best practices for Terraform and "the cloud".
18
+ DESCRIPTION
19
+
20
+ spec.homepage = 'https://github.com/aomega08/terracop'
21
+
22
+ spec.metadata['homepage_uri'] = spec.homepage
23
+ spec.metadata['source_code_uri'] = spec.homepage
24
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
25
+
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`
28
+ .split("\x0")
29
+ .reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ end
31
+
32
+ spec.bindir = 'exe'
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ['lib']
35
+
36
+ spec.add_development_dependency 'bundler', '~> 2.0'
37
+ spec.add_development_dependency 'byebug', '~> 11.0'
38
+ spec.add_development_dependency 'rake', '~> 10.0'
39
+ spec.add_development_dependency 'rspec', '~> 3.0'
40
+ spec.add_development_dependency 'rubocop', '~> 0.78'
41
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.37'
42
+ spec.add_development_dependency 'simplecov', '~> 0.10'
43
+
44
+ spec.add_dependency 'colorize', '~> 0.8'
45
+ end