cfn-nag 0.0.5 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f93ef6134730e9cc39c51ef51cb79a77b578ccf7
4
- data.tar.gz: afafe1a96594ffec2b1e864112b334ba4789d280
3
+ metadata.gz: 7833544e3b4320a57b9f59b63ca2d0d84cf1edce
4
+ data.tar.gz: 4c62a7a3ebd450650943cf9133c77b2567d8a91c
5
5
  SHA512:
6
- metadata.gz: c4ac8b4947e706f03392110ce40cc15e1158dd8f275a18ef72feae49816e9fe8748a812dbf0fbe90a52b3b63022a6ef202b88b9d94ec3f2217bc9c8a7b62b732
7
- data.tar.gz: c7b7ddebc99d3b635b999e0e2011ee91a3fb351f21021d6ca10160c0fb7568fca6ed29a79961de22698dc578b4343bfacc6f5b4891a3b0fee012b7910f5a320c
6
+ metadata.gz: a21b9bdb4501e68f750fa6749a10d203f54c552312fd80cc011eec1d05cb31cc61a6551cfacdbd8c73c8b4ab03322f8268685adf4f16fb9d2c56ebad8515a26a
7
+ data.tar.gz: 47ed2d380831246c3d08159c8db668f6d1613eaa9e4563558dd8a342519b13ffd2f551bc1888e88778f6f642487d720ff01607ff706dea95cf2c02c796cd6ac9
data/bin/cfn_nag CHANGED
@@ -5,8 +5,13 @@ require 'logging'
5
5
 
6
6
  opts = Trollop::options do
7
7
  opt :input_json, 'Cloudformation template to nag on', type: :string, required: true
8
+ opt :output_format, 'Format of results: [txt, json]', type: :string, default: 'txt'
8
9
  opt :debug, 'Enable debug output', type: :boolean, required: false, default: false
9
10
  end
10
11
 
12
+ Trollop::die(:output_format,
13
+ 'Must be txt or json') unless %w(txt json).include?(opts[:output_format])
14
+
11
15
  CfnNag::configure_logging(opts)
12
- exit CfnNag.new.audit(opts[:input_json])
16
+ exit CfnNag.new.audit(input_json_path: opts[:input_json],
17
+ output_format: opts[:output_format])
data/lib/cfn_nag.rb CHANGED
@@ -2,23 +2,25 @@ require_relative 'rule'
2
2
  require_relative 'custom_rules/security_group_missing_egress'
3
3
  require_relative 'custom_rules/user_missing_group'
4
4
  require_relative 'model/cfn_model'
5
+ require_relative 'result_view/simple_stdout_results'
6
+ require_relative 'result_view/json_results'
5
7
 
6
8
  class CfnNag
7
9
  include Rule
8
10
 
9
- def audit(input_json_path)
11
+ def audit(input_json_path:,
12
+ output_format:'txt')
10
13
  fail 'not even legit JSON' unless legal_json?(input_json_path)
11
14
 
12
- @violation_count = 0
13
- @warning_count = 0
15
+ @violations = []
14
16
 
15
17
  generic_json_rules input_json_path
16
18
 
17
19
  custom_rules input_json_path
18
20
 
19
- puts "Violations count: #{@violation_count}"
20
- puts "Warnings count: #{@warning_count}"
21
- @violation_count
21
+ results_renderer(output_format).new.render(@violations)
22
+
23
+ Rule::count_failures(@violations)
22
24
  end
23
25
 
24
26
  def self.configure_logging(opts)
@@ -34,6 +36,14 @@ class CfnNag
34
36
 
35
37
  private
36
38
 
39
+ def results_renderer(output_format)
40
+ registry = {
41
+ 'txt' => SimpleStdoutResults,
42
+ 'json' => JsonResults
43
+ }
44
+ registry[output_format]
45
+ end
46
+
37
47
  def legal_json?(input_json_path)
38
48
  begin
39
49
  JSON.parse(IO.read(input_json_path))
@@ -65,7 +75,8 @@ class CfnNag
65
75
  UserMissingGroupRule
66
76
  ]
67
77
  rules.each do |rule_class|
68
- @violation_count += rule_class.new.audit(cfn_model)
78
+ audit_result = rule_class.new.audit(cfn_model)
79
+ @violations << audit_result unless audit_result.nil?
69
80
  end
70
81
  end
71
82
  end
@@ -1,26 +1,21 @@
1
- require_relative '../rule'
1
+ require_relative '../violation'
2
2
 
3
3
  class SecurityGroupMissingEgressRule
4
- include Rule
5
4
 
6
5
  def audit(cfn_model)
7
- violation_count = 0
8
-
9
6
  logical_resource_ids = []
10
- violating_security_groups = []
11
7
  cfn_model.security_groups.each do |security_group|
12
8
  if security_group.egress_rules.size == 0
13
9
  logical_resource_ids << security_group.logical_resource_id
14
- violating_security_groups << security_group
15
- violation_count += 1
16
10
  end
17
11
  end
18
12
 
19
- if violation_count > 0
20
- message message_type: 'violation',
21
- message: 'Missing egress rule means all traffic is allowed outbound. Make this explicit if it is desired configuration',
22
- logical_resource_ids: logical_resource_ids
13
+ if logical_resource_ids.size > 0
14
+ Violation.new(type: Violation::FAILING_VIOLATION,
15
+ message: 'Missing egress rule means all traffic is allowed outbound. Make this explicit if it is desired configuration',
16
+ logical_resource_ids: logical_resource_ids)
17
+ else
18
+ nil
23
19
  end
24
- violation_count
25
20
  end
26
21
  end
@@ -1,26 +1,21 @@
1
- require_relative '../rule'
1
+ require_relative '../violation'
2
2
 
3
3
  class UserMissingGroupRule
4
- include Rule
5
4
 
6
5
  def audit(cfn_model)
7
- violation_count = 0
8
-
9
6
  logical_resource_ids = []
10
- violating_iam_users = []
11
7
  cfn_model.iam_users.each do |iam_user|
12
8
  if iam_user.groups.size == 0
13
9
  logical_resource_ids << iam_user.logical_resource_id
14
- violating_iam_users << iam_user
15
- violation_count += 1
16
10
  end
17
11
  end
18
12
 
19
- if violation_count > 0
20
- message message_type: 'violation',
21
- message: 'User is not assigned to a group',
22
- logical_resource_ids: logical_resource_ids
13
+ if logical_resource_ids.size > 0
14
+ Violation.new(type: Violation::FAILING_VIOLATION,
15
+ message: 'User is not assigned to a group',
16
+ logical_resource_ids: logical_resource_ids)
17
+ else
18
+ nil
23
19
  end
24
- violation_count
25
20
  end
26
21
  end
@@ -0,0 +1,15 @@
1
+ require 'json'
2
+ class JsonResults
3
+
4
+ def render(violations)
5
+ violations_hashes = violations.map do |violation|
6
+ {
7
+ type: violation.type,
8
+ message: violation.message,
9
+ logical_resource_ids: violation.logical_resource_ids,
10
+ violating_code: violation.violating_code
11
+ }
12
+ end
13
+ puts JSON.pretty_generate(violations_hashes)
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ require 'rule'
2
+
3
+ class SimpleStdoutResults
4
+
5
+ def render(violations)
6
+ violations.each do |violation|
7
+ message message_type: violation.type,
8
+ message: violation.message,
9
+ logical_resource_ids: violation.logical_resource_ids,
10
+ violating_code: violation.violating_code
11
+ end
12
+
13
+ puts "Violations count: #{Rule::count_warnings(violations)}"
14
+ puts "Warnings count: #{Rule::count_failures(violations)}"
15
+ end
16
+
17
+ private
18
+
19
+ def message(message_type:,
20
+ message:,
21
+ logical_resource_ids: nil,
22
+ violating_code: nil)
23
+
24
+ if logical_resource_ids == []
25
+ logical_resource_ids = nil
26
+ end
27
+
28
+ (1..60).each { print '-' }
29
+ puts
30
+ puts "| #{message_type.upcase}"
31
+ puts '|'
32
+ puts "| Resources: #{logical_resource_ids}" unless logical_resource_ids.nil?
33
+ puts '|' unless logical_resource_ids.nil?
34
+ puts "| #{message}"
35
+
36
+ unless violating_code.nil?
37
+ puts '|'
38
+ puts indent_multiline_string_with_prefix('|', violating_code.to_s)
39
+ end
40
+ end
41
+
42
+ def indent_multiline_string_with_prefix(prefix, multiline_string)
43
+ prefix + ' ' + multiline_string.gsub(/\n/, "\n#{prefix} ")
44
+ end
45
+ end
data/lib/rule.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  require 'logging'
2
+ require_relative 'violation'
2
3
 
3
4
  module Rule
4
- attr_accessor :input_json_path, :failure_count
5
+ attr_accessor :input_json_path
5
6
 
7
+ # jq preamble to spit out Resources but as an array of key-value pairs
8
+ # can be used in jq rule definition but... this is probably reducing replication at the cost of opaqueness
6
9
  def resources
7
10
  '.Resources|with_entries(.value.LogicalResourceId = .key)[]'
8
11
  end
9
12
 
13
+ # jq to filter Cloudformation resources by Type
14
+ # can be used in jq rule definition but... this is probably reducing replication at the cost of opaqueness
10
15
  def resources_by_type(resource)
11
16
  "#{resources}| select(.Type == \"#{resource}\")"
12
17
  end
@@ -21,12 +26,9 @@ module Rule
21
26
  resource_ids = parse_logical_resource_ids(stdout)
22
27
  new_warnings = resource_ids.size
23
28
  if result == 0 and new_warnings > 0
24
- @warning_count ||= 0
25
- @warning_count += new_warnings
26
-
27
- message(message_type: 'warning',
28
- message: message,
29
- logical_resource_ids: resource_ids)
29
+ add_violation(type: Violation::WARNING,
30
+ message: message,
31
+ logical_resource_ids: resource_ids)
30
32
  end
31
33
  end
32
34
 
@@ -35,7 +37,7 @@ module Rule
35
37
  fail_if_found: false,
36
38
  fatal: true,
37
39
  message: message,
38
- message_type: 'fatal assertion',
40
+ message_type: Violation::FATAL_VIOLATION,
39
41
  raw: true)
40
42
  end
41
43
 
@@ -44,7 +46,7 @@ module Rule
44
46
  fail_if_found: false,
45
47
  fatal: true,
46
48
  message: message,
47
- message_type: 'fatal assertion')
49
+ message_type: Violation::FATAL_VIOLATION)
48
50
  end
49
51
 
50
52
  def raw_fatal_violation(jq:, message:)
@@ -52,7 +54,7 @@ module Rule
52
54
  fail_if_found: true,
53
55
  fatal: true,
54
56
  message: message,
55
- message_type: 'fatal violation',
57
+ message_type: Violation::FATAL_VIOLATION,
56
58
  raw: true)
57
59
  end
58
60
 
@@ -61,46 +63,64 @@ module Rule
61
63
  fail_if_found: true,
62
64
  fatal: true,
63
65
  message: message,
64
- message_type: 'fatal violation')
66
+ message_type: Violation::FATAL_VIOLATION)
65
67
  end
66
68
 
67
69
  def violation(jq:, message:)
68
70
  failing_rule(jq_expression: jq,
69
71
  fail_if_found: true,
70
72
  message: message,
71
- message_type: 'violation')
73
+ message_type: Violation::FAILING_VIOLATION)
72
74
  end
73
75
 
74
76
  def assertion(jq:, message:)
75
77
  failing_rule(jq_expression: jq,
76
78
  fail_if_found: false,
77
79
  message: message,
78
- message_type: 'assertion')
80
+ message_type: Violation::FAILING_VIOLATION)
79
81
  end
80
82
 
81
- def message(message_type:,
82
- message:,
83
- logical_resource_ids: nil,
84
- violating_code: nil)
83
+ def self.empty?(array)
84
+ array.nil? or array.size ==0
85
+ end
85
86
 
86
- if logical_resource_ids == []
87
- logical_resource_ids = nil
87
+ def self.count_warnings(violations)
88
+ violations.inject(0) do |count, violation|
89
+ if violation.type == Violation::WARNING
90
+ if empty?(violation.logical_resource_ids)
91
+ count += 1
92
+ else
93
+ count += violation.logical_resource_ids.size
94
+ end
95
+ end
96
+ count
88
97
  end
98
+ end
89
99
 
90
- (1..60).each { print '-' }
91
- puts
92
- puts "| #{message_type.upcase}"
93
- puts '|'
94
- puts "| Resources: #{logical_resource_ids}" unless logical_resource_ids.nil?
95
- puts '|' unless logical_resource_ids.nil?
96
- puts "| #{message}"
97
-
98
- unless violating_code.nil?
99
- puts '|'
100
- puts indent_multiline_string_with_prefix('|', violating_code.to_s)
100
+ def self.count_failures(violations)
101
+ violations.inject(0) do |count, violation|
102
+ if violation.type == Violation::FAILING_VIOLATION
103
+ if empty?(violation.logical_resource_ids)
104
+ count += 1
105
+ else
106
+ count += violation.logical_resource_ids.size
107
+ end
108
+ end
109
+ count
101
110
  end
102
111
  end
103
112
 
113
+ def add_violation(type:,
114
+ message:,
115
+ logical_resource_ids: nil,
116
+ violating_code: nil)
117
+ violation = Violation.new(type: type,
118
+ message: message,
119
+ logical_resource_ids: logical_resource_ids,
120
+ violating_code: violating_code)
121
+ @violations << violation
122
+ end
123
+
104
124
  private
105
125
 
106
126
  def parse_logical_resource_ids(stdout)
@@ -111,10 +131,12 @@ module Rule
111
131
  fail 'json rule is likely not complete' if stdout.match /jq: error/
112
132
  end
113
133
 
114
- def indent_multiline_string_with_prefix(prefix, multiline_string)
115
- prefix + ' ' + multiline_string.gsub(/\n/, "\n#{prefix} ")
116
- end
117
-
134
+ # fail_if_found: this is false for an assertion, true for a violation. either way this rule ups the "failure" count
135
+ #
136
+ # raw: don't try to parse the output in any way. the rule is some kind of oddball so just show what matched and up
137
+ # failure count by 1
138
+ #
139
+ # fatal: if true, any match of the rule causes immediate shutdown to avoid more complicated downstream error checking
118
140
  def failing_rule(jq_expression:,
119
141
  fail_if_found:,
120
142
  message:,
@@ -128,26 +150,22 @@ module Rule
128
150
  scrape_jq_output_for_error(stdout)
129
151
  if (fail_if_found and result == 0) or
130
152
  (not fail_if_found and result != 0)
131
- @violation_count ||= 0
132
153
 
133
154
  if raw
134
- @violation_count += 1
135
-
136
- message(message_type: message_type,
137
- message: message,
138
- violating_code: stdout)
155
+ add_violation(type: message_type,
156
+ message: message,
157
+ violating_code: stdout)
139
158
 
140
159
  if fatal
141
160
  exit 1
142
161
  end
143
162
  else
144
163
  resource_ids = parse_logical_resource_ids(stdout)
145
- @violation_count += resource_ids.size
146
164
 
147
165
  if resource_ids.size > 0
148
- message(message_type: message_type,
149
- message: message,
150
- logical_resource_ids: resource_ids)
166
+ add_violation(type: message_type,
167
+ message: message,
168
+ logical_resource_ids: resource_ids)
151
169
 
152
170
  if fatal
153
171
  exit 1
@@ -157,6 +175,7 @@ module Rule
157
175
  end
158
176
  end
159
177
 
178
+ # the -e will return an exit code
160
179
  def jq_command(input_json_path, jq_expression)
161
180
  command = "cat #{input_json_path} | jq '#{jq_expression}' -e"
162
181
 
data/lib/violation.rb ADDED
@@ -0,0 +1,22 @@
1
+
2
+ class Violation
3
+ WARNING = 'warning'
4
+ FATAL_VIOLATION = 'fatal violation'
5
+ FAILING_VIOLATION = 'failing violation'
6
+
7
+ attr_reader :type, :message, :logical_resource_ids, :violating_code
8
+
9
+ def initialize(type:,
10
+ message:,
11
+ logical_resource_ids: nil,
12
+ violating_code: nil)
13
+ @type = type
14
+ @message = message
15
+ @logical_resource_ids = logical_resource_ids
16
+ @violating_code = violating_code
17
+ end
18
+
19
+ def to_s
20
+ puts "#{@type} #{@message} #{@logical_resource_ids} #{@violating_code}"
21
+ end
22
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfn-nag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - someguy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-24 00:00:00.000000000 Z
11
+ date: 2016-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logging
@@ -57,7 +57,10 @@ files:
57
57
  - lib/model/cfn_model.rb
58
58
  - lib/model/iam_user_parser.rb
59
59
  - lib/model/security_group_parser.rb
60
+ - lib/result_view/json_results.rb
61
+ - lib/result_view/simple_stdout_results.rb
60
62
  - lib/rule.rb
63
+ - lib/violation.rb
61
64
  homepage:
62
65
  licenses: []
63
66
  metadata: {}