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 +4 -4
- data/bin/cfn_nag +6 -1
- data/lib/cfn_nag.rb +18 -7
- data/lib/custom_rules/security_group_missing_egress.rb +7 -12
- data/lib/custom_rules/user_missing_group.rb +7 -12
- data/lib/result_view/json_results.rb +15 -0
- data/lib/result_view/simple_stdout_results.rb +45 -0
- data/lib/rule.rb +63 -44
- data/lib/violation.rb +22 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7833544e3b4320a57b9f59b63ca2d0d84cf1edce
|
4
|
+
data.tar.gz: 4c62a7a3ebd450650943cf9133c77b2567d8a91c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
@
|
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
|
-
|
20
|
-
|
21
|
-
@
|
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
|
-
|
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 '../
|
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
|
20
|
-
|
21
|
-
|
22
|
-
|
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 '../
|
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
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
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
|
-
|
25
|
-
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
80
|
+
message_type: Violation::FAILING_VIOLATION)
|
79
81
|
end
|
80
82
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
violating_code: nil)
|
83
|
+
def self.empty?(array)
|
84
|
+
array.nil? or array.size ==0
|
85
|
+
end
|
85
86
|
|
86
|
-
|
87
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
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.
|
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-
|
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: {}
|