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,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