cfn-nag 0.0.34 → 0.0.35
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 +13 -4
- data/bin/cfn_nag_rules +14 -1
- data/lib/cfn_nag.rb +52 -24
- data/lib/custom_rule_loader.rb +9 -2
- data/lib/custom_rules/security_group_missing_egress.rb +10 -1
- data/lib/custom_rules/unencrypted_s3_put_allowed.rb +10 -1
- data/lib/custom_rules/user_missing_group.rb +10 -1
- data/lib/json_rules/basic_rules.rb +8 -4
- data/lib/json_rules/cfn_rules.rb +2 -1
- data/lib/json_rules/cidr_rules.rb +16 -8
- data/lib/json_rules/cloudfront_rules.rb +2 -1
- data/lib/json_rules/ebs_rules.rb +2 -1
- data/lib/json_rules/iam_policy_rules.rb +42 -21
- data/lib/json_rules/iam_user_rules.rb +6 -3
- data/lib/json_rules/lambda_rules.rb +4 -2
- data/lib/json_rules/loadbalancer_rules.rb +4 -2
- data/lib/json_rules/port_rules.rb +8 -4
- data/lib/json_rules/s3_bucket_rules.rb +10 -5
- data/lib/json_rules/sns_rules.rb +4 -2
- data/lib/json_rules/sqs_rules.rb +4 -2
- data/lib/profile.rb +18 -0
- data/lib/profile_loader.rb +27 -0
- data/lib/result_view/simple_stdout_results.rb +1 -1
- data/lib/rule.rb +43 -21
- data/lib/rule_registry.rb +45 -0
- data/lib/violation.rb +7 -3
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 599ac76a7c5bb73f2e8ad348f408f1945c9556c8
|
4
|
+
data.tar.gz: 13521f759054aba5f2abcd434e71b46c36b95dc7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02d05593b7c89b552b8b5ffe3c0cbb353b9a3d80f15c27a3920279b88a57f0639026b5eb220adaaff5b4ad8cbdd6172f0d2a38931789147e0f7fc73e2167bd26
|
7
|
+
data.tar.gz: 4ad062cf1e1040dd21549569d3ebc621dd8c63005851f6c9bdcb5858eb5294cdfdeec1ad0e235f93c3e111412c35e786ebc31e0c5ed3aa49f6fdfc801a242b8a
|
data/bin/cfn_nag
CHANGED
@@ -7,13 +7,22 @@ opts = Trollop::options do
|
|
7
7
|
opt :input_json_path, 'CloudFormation template to nag on or directory of templates - all *.json and *.template recursively', type: :io, required: true
|
8
8
|
opt :output_format, 'Format of results: [txt, json]', type: :string, default: 'txt'
|
9
9
|
opt :debug, 'Enable debug output', type: :boolean, required: false, default: false
|
10
|
-
opt :rule_directory, 'Extra rule directories', type: :strings,
|
10
|
+
opt :rule_directory, 'Extra rule directories', type: :strings, required: false, default: [], multi: true
|
11
|
+
opt :profile_path, 'Path to a profile file', type: :io, required: false, default: nil
|
11
12
|
end
|
12
13
|
|
13
14
|
Trollop::die(:output_format,
|
14
15
|
'Must be txt or json') unless %w(txt json).include?(opts[:output_format])
|
15
16
|
|
16
17
|
CfnNag::configure_logging(opts)
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
|
19
|
+
profile_definition = nil
|
20
|
+
unless opts[:profile_path].nil?
|
21
|
+
profile_definition = IO.read(opts[:profile_path])
|
22
|
+
end
|
23
|
+
|
24
|
+
cfn_nag = CfnNag.new(profile_definition: profile_definition)
|
25
|
+
|
26
|
+
exit cfn_nag.audit(input_json_path: opts[:input_json_path],
|
27
|
+
output_format: opts[:output_format],
|
28
|
+
rule_directories: opts[:rule_directory])
|
data/bin/cfn_nag_rules
CHANGED
@@ -1,4 +1,17 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
require 'trollop'
|
2
3
|
require 'cfn_nag'
|
3
4
|
|
4
|
-
|
5
|
+
opts = Trollop::options do
|
6
|
+
opt :rule_directory, 'Extra rule directories', type: :strings, required: false, default: [], multi: true
|
7
|
+
opt :profile_path, 'Path to a profile file', type: :io, required: false, default: nil
|
8
|
+
end
|
9
|
+
|
10
|
+
profile_definition = nil
|
11
|
+
unless opts[:profile_path].nil?
|
12
|
+
profile_definition = IO.read(opts[:profile_path])
|
13
|
+
end
|
14
|
+
|
15
|
+
cfn_nag = CfnNag.new(profile_definition: profile_definition)
|
16
|
+
|
17
|
+
cfn_nag.dump_rules(rule_directories: opts[:rule_directory])
|
data/lib/cfn_nag.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative 'rule'
|
2
2
|
require_relative 'custom_rule_loader'
|
3
|
+
require_relative 'rule_registry'
|
4
|
+
require_relative 'profile_loader'
|
3
5
|
require_relative 'model/cfn_model'
|
4
6
|
require_relative 'result_view/simple_stdout_results'
|
5
7
|
require_relative 'result_view/json_results'
|
@@ -8,12 +10,17 @@ require 'tempfile'
|
|
8
10
|
class CfnNag
|
9
11
|
include Rule
|
10
12
|
|
11
|
-
def initialize
|
13
|
+
def initialize(profile_definition: nil)
|
12
14
|
@warning_registry = []
|
13
15
|
@violation_registry = []
|
16
|
+
@rule_registry = RuleRegistry.new
|
17
|
+
@custom_rule_loader = CustomRuleLoader.new(@rule_registry)
|
18
|
+
@profile_definition = profile_definition
|
14
19
|
end
|
15
20
|
|
16
|
-
def dump_rules
|
21
|
+
def dump_rules(rule_directories: [])
|
22
|
+
validate_extra_rule_directories(rule_directories)
|
23
|
+
|
17
24
|
dummy_cfn = <<-END
|
18
25
|
{
|
19
26
|
"Resources": {
|
@@ -30,18 +37,32 @@ class CfnNag
|
|
30
37
|
Tempfile.open('tempfile') do |dummy_cfn_template|
|
31
38
|
dummy_cfn_template.write dummy_cfn
|
32
39
|
dummy_cfn_template.rewind
|
33
|
-
audit_file(input_json_path: dummy_cfn_template.path,
|
40
|
+
audit_file(input_json_path: dummy_cfn_template.path,
|
41
|
+
rule_directories: rule_directories)
|
34
42
|
end
|
35
43
|
|
36
|
-
|
37
|
-
|
44
|
+
profile = nil
|
45
|
+
unless @profile_definition.nil?
|
46
|
+
profile = ProfileLoader.new(@rule_registry).load(profile_definition: @profile_definition)
|
38
47
|
end
|
39
48
|
|
40
49
|
puts 'WARNING VIOLATIONS:'
|
41
|
-
|
42
|
-
|
50
|
+
@rule_registry.warnings.sort {|left, right| left.id <=> right.id}.each do |warning|
|
51
|
+
if profile.nil?
|
52
|
+
puts "#{warning.id} #{warning.message}"
|
53
|
+
else
|
54
|
+
puts "#{warning.id} #{warning.message}" if profile.execute_rule?(warning.id)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
43
58
|
puts 'FAILING VIOLATIONS:'
|
44
|
-
|
59
|
+
@rule_registry.failings.sort {|left, right| left.id <=> right.id}.each do |failing|
|
60
|
+
if profile.nil?
|
61
|
+
puts "#{failing.id} #{failing.message}"
|
62
|
+
else
|
63
|
+
puts "#{failing.id} #{failing.message}" if profile.execute_rule?(failing.id)
|
64
|
+
end
|
65
|
+
end
|
45
66
|
end
|
46
67
|
|
47
68
|
def audit(input_json_path:,
|
@@ -88,12 +109,14 @@ class CfnNag
|
|
88
109
|
logger.add_appenders Logging.appenders.stdout
|
89
110
|
end
|
90
111
|
|
91
|
-
def audit_template(input_json:,
|
112
|
+
def audit_template(input_json:,
|
113
|
+
rule_directories: [])
|
92
114
|
@stop_processing = false
|
93
115
|
@violations = []
|
94
116
|
|
95
117
|
unless legal_json?(input_json)
|
96
|
-
@violations << Violation.new(
|
118
|
+
@violations << Violation.new(id: 'FATAL',
|
119
|
+
type: Violation::FAILING_VIOLATION,
|
97
120
|
message: 'not even legit JSON',
|
98
121
|
violating_code: input_json)
|
99
122
|
@stop_processing = true
|
@@ -103,6 +126,8 @@ class CfnNag
|
|
103
126
|
|
104
127
|
@violations += custom_rules input_json unless @stop_processing == true
|
105
128
|
|
129
|
+
@violations = filter_violations_by_profile @violations
|
130
|
+
|
106
131
|
{
|
107
132
|
failure_count: Rule::count_failures(@violations),
|
108
133
|
violations: @violations
|
@@ -111,6 +136,17 @@ class CfnNag
|
|
111
136
|
|
112
137
|
private
|
113
138
|
|
139
|
+
def filter_violations_by_profile(violations)
|
140
|
+
profile = nil
|
141
|
+
unless @profile_definition.nil?
|
142
|
+
profile = ProfileLoader.new(@rule_registry).load(profile_definition: @profile_definition)
|
143
|
+
end
|
144
|
+
|
145
|
+
violations.reject do |violation|
|
146
|
+
not profile.nil? and not profile.execute_rule?(violation.id)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
114
150
|
def validate_extra_rule_directories(rule_directories)
|
115
151
|
rule_directories.flatten.each do |rule_directory|
|
116
152
|
fail "Not a real directory #{rule_directory}" unless File.directory? rule_directory
|
@@ -118,11 +154,13 @@ class CfnNag
|
|
118
154
|
end
|
119
155
|
|
120
156
|
|
121
|
-
def render_results(aggregate_results:,
|
157
|
+
def render_results(aggregate_results:,
|
158
|
+
output_format:)
|
122
159
|
results_renderer(output_format).new.render(aggregate_results)
|
123
160
|
end
|
124
161
|
|
125
|
-
def audit_file(input_json_path:,
|
162
|
+
def audit_file(input_json_path:,
|
163
|
+
rule_directories:)
|
126
164
|
audit_template(input_json: IO.read(input_json_path),
|
127
165
|
rule_directories: rule_directories)
|
128
166
|
end
|
@@ -169,21 +207,11 @@ class CfnNag
|
|
169
207
|
not system("#{command} > /dev/null 2>&1").nil?
|
170
208
|
end
|
171
209
|
|
172
|
-
def jruby_in_a_jar?
|
173
|
-
__dir__.start_with? '/uri:classloader'
|
174
|
-
end
|
175
|
-
|
176
210
|
def generic_json_rules(input_json, rule_directories)
|
177
211
|
unless command? 'jq'
|
178
212
|
fail 'jq executable must be available in PATH'
|
179
213
|
end
|
180
|
-
|
181
|
-
if jruby_in_a_jar?
|
182
|
-
rules = %w(basic_rules cfn_rules cidr_rules cloudfront_rules ebs_rules iam_policy_rules iam_user_rules lambda_rules loadbalancer_rules port_rules s3_bucket_rules sns_rules sqs_rules)
|
183
|
-
rules = rules.map { |rule| File.join(__dir__, 'json_rules', "#{rule}.rb")[1..-1] }
|
184
|
-
else
|
185
|
-
rules = Dir[File.join(__dir__, 'json_rules', '*.rb')].sort
|
186
|
-
end
|
214
|
+
rules = Dir[File.join(__dir__, 'json_rules', '*.rb')].sort
|
187
215
|
|
188
216
|
rules.each do |rule_file|
|
189
217
|
@input_json = input_json
|
@@ -201,6 +229,6 @@ class CfnNag
|
|
201
229
|
end
|
202
230
|
|
203
231
|
def custom_rules(input_json)
|
204
|
-
|
232
|
+
@custom_rule_loader.custom_rules(input_json)
|
205
233
|
end
|
206
234
|
end
|
data/lib/custom_rule_loader.rb
CHANGED
@@ -18,19 +18,26 @@ class CustomRuleLoader
|
|
18
18
|
@custom_rule_directory
|
19
19
|
end
|
20
20
|
|
21
|
-
def initialize
|
21
|
+
def initialize(rule_registry)
|
22
22
|
@custom_rule_registry = [
|
23
23
|
SecurityGroupMissingEgressRule,
|
24
24
|
UserMissingGroupRule,
|
25
25
|
UnencryptedS3PutObjectAllowedRule
|
26
26
|
]
|
27
27
|
@violations = []
|
28
|
+
@rule_registry = rule_registry
|
29
|
+
discover_rules
|
28
30
|
end
|
29
31
|
|
30
32
|
def custom_rules(input_json)
|
31
|
-
|
33
|
+
@violations = []
|
34
|
+
|
32
35
|
@custom_rule_registry.each do |rule_class|
|
33
36
|
rule = rule_class.new
|
37
|
+
@rule_registry.definition(id: rule.rule_id,
|
38
|
+
type: rule.rule_type,
|
39
|
+
message: rule.rule_text)
|
40
|
+
|
34
41
|
if rule.respond_to? 'custom_parsers'
|
35
42
|
rule.custom_parsers.each do |custom_parser|
|
36
43
|
ParserRegistry.instance.add_parser custom_parser[0], custom_parser[1]
|
@@ -6,6 +6,14 @@ class SecurityGroupMissingEgressRule
|
|
6
6
|
'Missing egress rule means all traffic is allowed outbound. Make this explicit if it is desired configuration'
|
7
7
|
end
|
8
8
|
|
9
|
+
def rule_type
|
10
|
+
Violation::FAILING_VIOLATION
|
11
|
+
end
|
12
|
+
|
13
|
+
def rule_id
|
14
|
+
'F1000'
|
15
|
+
end
|
16
|
+
|
9
17
|
def audit(cfn_model)
|
10
18
|
logical_resource_ids = []
|
11
19
|
cfn_model.security_groups.each do |security_group|
|
@@ -15,7 +23,8 @@ class SecurityGroupMissingEgressRule
|
|
15
23
|
end
|
16
24
|
|
17
25
|
if logical_resource_ids.size > 0
|
18
|
-
Violation.new(
|
26
|
+
Violation.new(id: rule_id,
|
27
|
+
type: rule_type,
|
19
28
|
message: rule_text,
|
20
29
|
logical_resource_ids: logical_resource_ids)
|
21
30
|
else
|
@@ -7,6 +7,14 @@ class UnencryptedS3PutObjectAllowedRule
|
|
7
7
|
'It appears that the S3 Bucket Policy allows s3:PutObject without server-side encryption'
|
8
8
|
end
|
9
9
|
|
10
|
+
def rule_type
|
11
|
+
Violation::WARNING
|
12
|
+
end
|
13
|
+
|
14
|
+
def rule_id
|
15
|
+
'W1000'
|
16
|
+
end
|
17
|
+
|
10
18
|
def audit(cfn_model)
|
11
19
|
logical_resource_ids = []
|
12
20
|
cfn_model.bucket_policies.each do |bucket_policy|
|
@@ -19,7 +27,8 @@ class UnencryptedS3PutObjectAllowedRule
|
|
19
27
|
end
|
20
28
|
|
21
29
|
if logical_resource_ids.size > 0
|
22
|
-
Violation.new(
|
30
|
+
Violation.new(id: rule_id,
|
31
|
+
type: rule_type,
|
23
32
|
message: rule_text,
|
24
33
|
logical_resource_ids: logical_resource_ids)
|
25
34
|
else
|
@@ -6,6 +6,14 @@ class UserMissingGroupRule
|
|
6
6
|
'User is not assigned to a group'
|
7
7
|
end
|
8
8
|
|
9
|
+
def rule_type
|
10
|
+
Violation::FAILING_VIOLATION
|
11
|
+
end
|
12
|
+
|
13
|
+
def rule_id
|
14
|
+
'F2000'
|
15
|
+
end
|
16
|
+
|
9
17
|
def audit(cfn_model)
|
10
18
|
logical_resource_ids = []
|
11
19
|
cfn_model.iam_users.each do |iam_user|
|
@@ -15,7 +23,8 @@ class UserMissingGroupRule
|
|
15
23
|
end
|
16
24
|
|
17
25
|
if logical_resource_ids.size > 0
|
18
|
-
Violation.new(
|
26
|
+
Violation.new(id: rule_id,
|
27
|
+
type: rule_type,
|
19
28
|
message: rule_text,
|
20
29
|
logical_resource_ids: logical_resource_ids)
|
21
30
|
else
|
@@ -1,4 +1,5 @@
|
|
1
|
-
raw_fatal_assertion
|
1
|
+
raw_fatal_assertion id: 'FATAL',
|
2
|
+
jq: '.Resources|length > 0',
|
2
3
|
message: 'A CloudFormation template must have at least 1 resource'
|
3
4
|
|
4
5
|
|
@@ -14,7 +15,8 @@ raw_fatal_assertion jq: '.Resources|length > 0',
|
|
14
15
|
AWS::EC2::SecurityGroupIngress
|
15
16
|
AWS::EC2::SecurityGroupEgress
|
16
17
|
).each do |resource_must_have_properties|
|
17
|
-
fatal_violation
|
18
|
+
fatal_violation id: 'FATAL',
|
19
|
+
jq: "[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == \"#{resource_must_have_properties}\" and .Properties == null)]|map(.LogicalResourceId)",
|
18
20
|
message: "#{resource_must_have_properties} must have Properties"
|
19
21
|
end
|
20
22
|
|
@@ -32,7 +34,8 @@ missing_reference_jq = <<END
|
|
32
34
|
)|if length==0 then false else . end
|
33
35
|
END
|
34
36
|
|
35
|
-
raw_fatal_violation
|
37
|
+
raw_fatal_violation id: 'FATAL',
|
38
|
+
jq: missing_reference_jq,
|
36
39
|
message: 'All Ref and Fn::GetAtt must reference existing logical resource ids'
|
37
40
|
|
38
41
|
|
@@ -40,6 +43,7 @@ raw_fatal_violation jq: missing_reference_jq,
|
|
40
43
|
AWS::EC2::SecurityGroupIngress
|
41
44
|
AWS::EC2::SecurityGroupEgress
|
42
45
|
).each do |xgress|
|
43
|
-
fatal_violation
|
46
|
+
fatal_violation id: 'FATAL',
|
47
|
+
jq: "[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == \"#{xgress}\" and .Properties.GroupName != null)]|map(.LogicalResourceId)",
|
44
48
|
message: "#{xgress} must not have GroupName - EC2 classic is a no-go!"
|
45
49
|
end
|
data/lib/json_rules/cfn_rules.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
-
warning
|
1
|
+
warning id: 'W1',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Metadata."AWS::CloudFormation::Authentication") ]|'\
|
2
3
|
'map(.LogicalResourceId) ',
|
3
4
|
message: 'Specifying credentials in the template itself is probably not the safest thing'
|
@@ -1,31 +1,38 @@
|
|
1
1
|
##### inline ingress
|
2
|
-
warning
|
2
|
+
warning id: 'W2',
|
3
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupIngress|type == "object"))|select(.Properties.SecurityGroupIngress.CidrIp? == "0.0.0.0/0")]|map(.LogicalResourceId)',
|
3
4
|
message: 'Security Groups found with cidr open to world on ingress. This should never be true on instance. Permissible on ELB'
|
4
5
|
|
5
|
-
warning
|
6
|
+
warning id: 'W3',
|
7
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupIngress|type == "array"))|first(select(.Properties.SecurityGroupIngress[].CidrIp? == "0.0.0.0/0"))]|map(.LogicalResourceId)',
|
6
8
|
message: 'Security Groups found with cidr open to world on ingress array. This should never be true on instance. Permissible on ELB'
|
7
9
|
|
8
10
|
##### external ingress
|
9
|
-
warning
|
11
|
+
warning id: 'W4',
|
12
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupIngress")|select(.Properties.CidrIp? == "0.0.0.0/0")]|map(.LogicalResourceId)',
|
10
13
|
message: 'Security Group Standalone Ingress found with cidr open to world. This should never be true on instance. Permissible on ELB'
|
11
14
|
|
12
15
|
|
13
16
|
###### inline egress
|
14
|
-
warning
|
17
|
+
warning id: 'W5',
|
18
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupEgress|type == "object"))|select(.Properties.SecurityGroupEgress.CidrIp? == "0.0.0.0/0")]|map(.LogicalResourceId)',
|
15
19
|
message: 'Security Groups found with cidr open to world on egress'
|
16
20
|
|
17
21
|
|
18
|
-
warning
|
22
|
+
warning id: 'W6',
|
23
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupEgress|type == "array"))|first(select(.Properties.SecurityGroupEgress[].CidrIp? == "0.0.0.0/0"))]|map(.LogicalResourceId)',
|
19
24
|
message: 'Security Groups found with cidr open to world on egress array'
|
20
25
|
|
21
26
|
|
22
27
|
##### external egress
|
23
|
-
warning
|
28
|
+
warning id: 'W7',
|
29
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupEgress")|select(.Properties.CidrIp? == "0.0.0.0/0")]|map(.LogicalResourceId)',
|
24
30
|
message: 'Security Group Standalone Egress found with cidr open to world.'
|
25
31
|
|
26
32
|
|
27
33
|
# BEWARE with escapes \d -> \\\d because of how the escapes get munged from ruby through to shell
|
28
|
-
warning
|
34
|
+
warning id: 'W8',
|
35
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupIngress" and .Properties.CidrIp|type == "string")|select(.Properties.CidrIp | test("^\\\d{1,3}\\\.\\\d{1,3}\\\.\\\d{1,3}\\\.\\\d{1,3}/(?!32)$") )]|map(.LogicalResourceId)',
|
29
36
|
message: 'Security Group Standalone Ingress cidr found that is not /32'
|
30
37
|
|
31
38
|
non_32_cidr_jq_expression = <<END
|
@@ -57,5 +64,6 @@ non_32_cidr_jq_expression = <<END
|
|
57
64
|
]|map(.LogicalResourceId)
|
58
65
|
END
|
59
66
|
|
60
|
-
warning
|
67
|
+
warning id: 'W9',
|
68
|
+
jq: non_32_cidr_jq_expression,
|
61
69
|
message: 'Security Groups found with cidr that is not /32'
|
@@ -1,3 +1,4 @@
|
|
1
|
-
warning
|
1
|
+
warning id: 'W10',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::CloudFront::Distribution")|'\
|
2
3
|
'select(.Properties.DistributionConfig.Logging == null)]|map(.LogicalResourceId) ',
|
3
4
|
message: 'CloudFront Distribution should enable access logging'
|
data/lib/json_rules/ebs_rules.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
-
violation
|
1
|
+
violation id: 'F1',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::Volume")|'\
|
2
3
|
'select(.Properties.Encrypted == null or .Properties.Encrypted == false)]|map(.LogicalResourceId) ',
|
3
4
|
message: 'EBS volume should have server-side encryption enabled'
|
@@ -6,21 +6,25 @@ def wildcard_action:
|
|
6
6
|
end;
|
7
7
|
END
|
8
8
|
|
9
|
-
violation
|
9
|
+
violation id: 'F2',
|
10
|
+
jq: wildcard_action_filter +
|
10
11
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|wildcard_action)]|map(.LogicalResourceId) ",
|
11
12
|
message: 'IAM role should not allow * action on its trust policy'
|
12
13
|
|
13
|
-
violation
|
14
|
+
violation id: 'F3',
|
15
|
+
jq: wildcard_action_filter +
|
14
16
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|wildcard_action)]|map(.LogicalResourceId)",
|
15
17
|
message: 'IAM role should not allow * action on its permissions policy'
|
16
18
|
|
17
19
|
|
18
|
-
violation
|
20
|
+
violation id: 'F4',
|
21
|
+
jq: wildcard_action_filter +
|
19
22
|
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|wildcard_action)]|map(.LogicalResourceId) ",
|
20
23
|
message: 'IAM policy should not allow * action'
|
21
24
|
|
22
25
|
|
23
|
-
violation
|
26
|
+
violation id: 'F5',
|
27
|
+
jq: wildcard_action_filter +
|
24
28
|
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|wildcard_action)]|map(.LogicalResourceId) ",
|
25
29
|
message: 'IAM managed policy should not allow * action'
|
26
30
|
|
@@ -32,16 +36,19 @@ def wildcard_resource:
|
|
32
36
|
end;
|
33
37
|
END
|
34
38
|
|
35
|
-
warning
|
39
|
+
warning id: 'W11',
|
40
|
+
jq: wildcard_resource_filter +
|
36
41
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|wildcard_resource)]|map(.LogicalResourceId)",
|
37
42
|
message: 'IAM role should not allow * resource on its permissions policy'
|
38
43
|
|
39
44
|
|
40
|
-
warning
|
45
|
+
warning id: 'W12',
|
46
|
+
jq: wildcard_resource_filter +
|
41
47
|
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|wildcard_resource)]|map(.LogicalResourceId)",
|
42
48
|
message: 'IAM policy should not allow * resource'
|
43
49
|
|
44
|
-
warning
|
50
|
+
warning id: 'W13',
|
51
|
+
jq: wildcard_resource_filter +
|
45
52
|
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|wildcard_resource)]|map(.LogicalResourceId)",
|
46
53
|
message: 'IAM managed policy should not allow * resource'
|
47
54
|
|
@@ -53,34 +60,41 @@ def allow_not_action:
|
|
53
60
|
end;
|
54
61
|
END
|
55
62
|
|
56
|
-
warning
|
63
|
+
warning id: 'W14',
|
64
|
+
jq: allow_not_action_filter +
|
57
65
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
58
66
|
message: 'IAM role should not allow Allow+NotAction on trust permissinos'
|
59
67
|
|
60
68
|
|
61
|
-
warning
|
69
|
+
warning id: 'W15',
|
70
|
+
jq: allow_not_action_filter +
|
62
71
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
63
72
|
message: 'IAM role should not allow Allow+NotAction'
|
64
73
|
|
65
74
|
|
66
|
-
warning
|
75
|
+
warning id: 'W16',
|
76
|
+
jq: allow_not_action_filter +
|
67
77
|
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
68
78
|
message: 'IAM policy should not allow Allow+NotAction'
|
69
79
|
|
70
80
|
|
71
|
-
warning
|
81
|
+
warning id: 'W17',
|
82
|
+
jq: allow_not_action_filter +
|
72
83
|
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
73
84
|
message: 'IAM managed policy should not allow Allow+NotAction'
|
74
85
|
|
75
|
-
warning
|
86
|
+
warning id: 'W18',
|
87
|
+
jq: allow_not_action_filter +
|
76
88
|
"[#{resources_by_type('AWS::SQS::QueuePolicy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
77
89
|
message: 'SQS Queue policy should not allow Allow+NotAction'
|
78
90
|
|
79
|
-
warning
|
91
|
+
warning id: 'W19',
|
92
|
+
jq: allow_not_action_filter +
|
80
93
|
"[#{resources_by_type('AWS::SNS::TopicPolicy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
81
94
|
message: 'SNS Topic policy should not allow Allow+NotAction'
|
82
95
|
|
83
|
-
warning
|
96
|
+
warning id: 'W20',
|
97
|
+
jq: allow_not_action_filter +
|
84
98
|
"[#{resources_by_type('AWS::S3::BucketPolicy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
85
99
|
message: 'S3 Bucket policy should not allow Allow+NotAction'
|
86
100
|
|
@@ -92,17 +106,20 @@ def allow_not_resource:
|
|
92
106
|
end;
|
93
107
|
END
|
94
108
|
|
95
|
-
warning
|
109
|
+
warning id: 'W21',
|
110
|
+
jq: allow_not_resource_filter +
|
96
111
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|allow_not_resource)]|map(.LogicalResourceId)",
|
97
112
|
message: 'IAM role should not allow Allow+NotResource'
|
98
113
|
|
99
114
|
|
100
|
-
warning
|
115
|
+
warning id: 'W22',
|
116
|
+
jq: allow_not_resource_filter +
|
101
117
|
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|allow_not_resource)]|map(.LogicalResourceId)",
|
102
118
|
message: 'IAM policy should not allow Allow+NotResource'
|
103
119
|
|
104
120
|
|
105
|
-
warning
|
121
|
+
warning id: 'W23',
|
122
|
+
jq: allow_not_resource_filter +
|
106
123
|
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|allow_not_resource)]|map(.LogicalResourceId)",
|
107
124
|
message: 'IAM managed policy should not allow Allow+NotResource'
|
108
125
|
|
@@ -115,18 +132,22 @@ def allow_not_principal:
|
|
115
132
|
end;
|
116
133
|
END
|
117
134
|
|
118
|
-
violation
|
135
|
+
violation id: 'F6',
|
136
|
+
jq: allow_not_principal_filter +
|
119
137
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|allow_not_principal)]|map(.LogicalResourceId)",
|
120
138
|
message: 'IAM role should not allow Allow+NotPrincipal in its trust policy'
|
121
139
|
|
122
|
-
violation
|
140
|
+
violation id: 'F7',
|
141
|
+
jq: allow_not_principal_filter +
|
123
142
|
"[#{resources_by_type('AWS::SQS::QueuePolicy')}|select(.Properties.PolicyDocument|allow_not_principal)]|map(.LogicalResourceId)",
|
124
143
|
message: 'SQS Queue policy should not allow Allow+NotPrincipal'
|
125
144
|
|
126
|
-
violation
|
145
|
+
violation id: 'F8',
|
146
|
+
jq: allow_not_principal_filter +
|
127
147
|
"[#{resources_by_type('AWS::SNS::TopicPolicy')}|select(.Properties.PolicyDocument|allow_not_principal)]|map(.LogicalResourceId)",
|
128
148
|
message: 'SNS Topic policy should not allow Allow+NotPrincipal'
|
129
149
|
|
130
|
-
violation
|
150
|
+
violation id: 'F9',
|
151
|
+
jq: allow_not_principal_filter +
|
131
152
|
"[#{resources_by_type('AWS::S3::BucketPolicy')}|select(.Properties.PolicyDocument|allow_not_principal)]|map(.LogicalResourceId)",
|
132
153
|
message: 'S3 Bucket policy should not allow Allow+NotPrincipal'
|
@@ -1,12 +1,15 @@
|
|
1
|
-
violation
|
1
|
+
violation id: 'F10',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::User")|'\
|
2
3
|
'select(.Properties.Policies|length > 0)]|map(.LogicalResourceId) ',
|
3
4
|
message: 'IAM user should not have any directly attached policies. Should be on group'
|
4
5
|
|
5
|
-
violation
|
6
|
+
violation id: 'F11',
|
7
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::Policy")|'\
|
6
8
|
'select(.Properties.Users|length > 0)]|map(.LogicalResourceId) ',
|
7
9
|
message: 'IAM policy should not apply directly to users. Should be on group'
|
8
10
|
|
9
|
-
violation
|
11
|
+
violation id: 'F12',
|
12
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::ManagedPolicy")|'\
|
10
13
|
'select(.Properties.Users|length > 0)]|map(.LogicalResourceId) ',
|
11
14
|
message: 'IAM managed policy should not apply directly to users. Should be on group'
|
12
15
|
|
@@ -1,7 +1,9 @@
|
|
1
|
-
warning
|
1
|
+
warning id: 'W24',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::Lambda::Permission")|'\
|
2
3
|
'select(.Properties.Action != "lambda:InvokeFunction")]|map(.LogicalResourceId) ',
|
3
4
|
message: 'Lambda permission beside InvokeFunction might not be what you want? Not sure!?'
|
4
5
|
|
5
|
-
violation
|
6
|
+
violation id: 'F13',
|
7
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::Lambda::Permission")|'\
|
6
8
|
'select(.Properties.Principal == "*")]|map(.LogicalResourceId) ',
|
7
9
|
message: 'Lambda permission principal should not be wildcard'
|
@@ -1,7 +1,9 @@
|
|
1
|
-
warning
|
1
|
+
warning id: 'W25',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::ElasticLoadBalancing::LoadBalancer")|'\
|
2
3
|
'select(.Properties.AccessLoggingPolicy == null)]|map(.LogicalResourceId) ',
|
3
4
|
message: 'Elastic Load Balancer should have access logging configured'
|
4
5
|
|
5
|
-
warning
|
6
|
+
warning id: 'W26',
|
7
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::ElasticLoadBalancing::LoadBalancer")|'\
|
6
8
|
'select(.Properties.AccessLoggingPolicy?.Enabled == false)]|map(.LogicalResourceId) ',
|
7
9
|
message: 'Elastic Load Balancer should have access logging enabled'
|
@@ -1,4 +1,5 @@
|
|
1
|
-
warning
|
1
|
+
warning id: 'W27',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | '\
|
2
3
|
'select(.Type == "AWS::EC2::SecurityGroup")| '\
|
3
4
|
'if (.Properties.SecurityGroupIngress|type == "object") '\
|
4
5
|
'then select(.Properties.SecurityGroupIngress.ToPort != .Properties.SecurityGroupIngress.FromPort) '\
|
@@ -10,10 +11,12 @@ warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | '\
|
|
10
11
|
']|map(.LogicalResourceId)',
|
11
12
|
message: 'Security Groups found ingress with port range instead of just a single port'
|
12
13
|
|
13
|
-
warning
|
14
|
+
warning id: 'W28',
|
15
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupIngress")|select(.Properties.ToPort != .Properties.FromPort)]|map(.LogicalResourceId)',
|
14
16
|
message: 'Security Group ingress with port range instead of just a single port'
|
15
17
|
|
16
|
-
warning
|
18
|
+
warning id: 'W29',
|
19
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | '\
|
17
20
|
'select(.Type == "AWS::EC2::SecurityGroup")| '\
|
18
21
|
'if (.Properties.SecurityGroupEgress|type == "object") '\
|
19
22
|
'then select(.Properties.SecurityGroupEgress.ToPort != .Properties.SecurityGroupEgress.FromPort) '\
|
@@ -25,5 +28,6 @@ warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | '\
|
|
25
28
|
']|map(.LogicalResourceId)',
|
26
29
|
message: 'Security Groups found egress with port range instead of just a single port'
|
27
30
|
|
28
|
-
warning
|
31
|
+
warning id: 'W30',
|
32
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupEgress")|select(.Properties.ToPort != .Properties.FromPort)]|map(.LogicalResourceId)',
|
29
33
|
message: 'Security Group egress with port range instead of just a single port'
|
@@ -1,8 +1,10 @@
|
|
1
|
-
warning
|
1
|
+
warning id: 'W31',
|
2
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::S3::Bucket")|'\
|
2
3
|
'select(.Properties.AccessControl? == "PublicRead")]|map(.LogicalResourceId) ',
|
3
4
|
message: 'S3 Bucket likely should not have a public read acl'
|
4
5
|
|
5
|
-
violation
|
6
|
+
violation id: 'F14',
|
7
|
+
jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::S3::Bucket")|'\
|
6
8
|
'select(.Properties.AccessControl? == "PublicReadWrite")]|map(.LogicalResourceId) ',
|
7
9
|
message: 'S3 Bucket should not have a public read-write acl'
|
8
10
|
|
@@ -33,14 +35,17 @@ def s3_wildcard_aws_principal:
|
|
33
35
|
END
|
34
36
|
|
35
37
|
|
36
|
-
violation
|
38
|
+
violation id: 'F15',
|
39
|
+
jq: s3_wildcard_action_filter +
|
37
40
|
"[#{resources_by_type('AWS::S3::BucketPolicy')}|select(.Properties.PolicyDocument|s3_wildcard_action)]|map(.LogicalResourceId) ",
|
38
41
|
message: 'S3 Bucket policy should not allow * action'
|
39
42
|
|
40
|
-
violation
|
43
|
+
violation id: 'F16',
|
44
|
+
jq: s3_wildcard_principal_filter +
|
41
45
|
"[#{resources_by_type('AWS::S3::BucketPolicy')}|select(.Properties.PolicyDocument|s3_wildcard_principal)]|map(.LogicalResourceId) ",
|
42
46
|
message: 'S3 Bucket policy should not allow * principal'
|
43
47
|
|
44
|
-
violation
|
48
|
+
violation id: 'F17',
|
49
|
+
jq: s3_wildcard_aws_principal_filter +
|
45
50
|
"[#{resources_by_type('AWS::S3::BucketPolicy')}|select(.Properties.PolicyDocument|s3_wildcard_aws_principal)]|map(.LogicalResourceId) ",
|
46
51
|
message: 'S3 Bucket policy should not allow * AWS principal'
|
data/lib/json_rules/sns_rules.rb
CHANGED
@@ -18,10 +18,12 @@ END
|
|
18
18
|
|
19
19
|
#sns action wildcard doesnt seem to be accepted by sns so dont sweat it
|
20
20
|
|
21
|
-
violation
|
21
|
+
violation id: 'F18',
|
22
|
+
jq: sns_wildcard_principal_filter +
|
22
23
|
"[#{resources_by_type('AWS::SNS::TopicPolicy')}|select(.Properties.PolicyDocument|sns_wildcard_principal)]|map(.LogicalResourceId) ",
|
23
24
|
message: 'SNS topic policy should not allow * principal'
|
24
25
|
|
25
|
-
violation
|
26
|
+
violation id: 'F19',
|
27
|
+
jq: sns_wildcard_aws_principal_filter +
|
26
28
|
"[#{resources_by_type('AWS::SNS::TopicPolicy')}|select(.Properties.PolicyDocument|sns_wildcard_aws_principal)]|map(.LogicalResourceId) ",
|
27
29
|
message: 'SNS topic policy should not allow * AWS principal'
|
data/lib/json_rules/sqs_rules.rb
CHANGED
@@ -14,10 +14,12 @@ def sqs_wildcard_principal:
|
|
14
14
|
end;
|
15
15
|
END
|
16
16
|
|
17
|
-
violation
|
17
|
+
violation id: 'F20',
|
18
|
+
jq: sqs_wildcard_action_filter +
|
18
19
|
"[#{resources_by_type('AWS::SQS::QueuePolicy')}|select(.Properties.PolicyDocument|sqs_wildcard_action)]|map(.LogicalResourceId) ",
|
19
20
|
message: 'SQS Queue policy should not allow * action'
|
20
21
|
|
21
|
-
violation
|
22
|
+
violation id: 'F21',
|
23
|
+
jq: sqs_wildcard_principal_filter +
|
22
24
|
"[#{resources_by_type('AWS::SQS::QueuePolicy')}|select(.Properties.PolicyDocument|sqs_wildcard_principal)]|map(.LogicalResourceId) ",
|
23
25
|
message: 'SQS Queue policy should not allow * principal'
|
data/lib/profile.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'profile'
|
2
|
+
|
3
|
+
class ProfileLoader
|
4
|
+
|
5
|
+
def initialize(rules_registry)
|
6
|
+
@rules_registry = rules_registry
|
7
|
+
end
|
8
|
+
|
9
|
+
def load(profile_definition:)
|
10
|
+
|
11
|
+
if profile_definition.nil? or profile_definition.strip == ''
|
12
|
+
raise 'Empty profile'
|
13
|
+
end
|
14
|
+
|
15
|
+
new_profile = Profile.new
|
16
|
+
|
17
|
+
profile_definition.each_line do |line|
|
18
|
+
rule_id = line.chomp
|
19
|
+
if @rules_registry.by_id(rule_id) == []
|
20
|
+
raise "#{rule_id} is not a legal rule identifier"
|
21
|
+
else
|
22
|
+
new_profile.add_rule rule_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
new_profile
|
26
|
+
end
|
27
|
+
end
|
@@ -9,7 +9,7 @@ class SimpleStdoutResults
|
|
9
9
|
(1..60).each { print '-' }
|
10
10
|
|
11
11
|
result[:file_results][:violations].each do |violation|
|
12
|
-
message message_type: violation.type,
|
12
|
+
message message_type: "#{violation.type} #{violation.id}",
|
13
13
|
message: violation.message,
|
14
14
|
logical_resource_ids: violation.logical_resource_ids,
|
15
15
|
violating_code: violation.violating_code
|
data/lib/rule.rb
CHANGED
@@ -16,8 +16,10 @@ module Rule
|
|
16
16
|
"#{resources}| select(.Type == \"#{resource}\")"
|
17
17
|
end
|
18
18
|
|
19
|
-
def warning(jq:, message:)
|
20
|
-
@
|
19
|
+
def warning(id:, jq:, message:)
|
20
|
+
warning_def = @rule_registry.definition(id: id,
|
21
|
+
type: Violation::WARNING,
|
22
|
+
message: message)
|
21
23
|
|
22
24
|
return if @stop_processing
|
23
25
|
|
@@ -30,50 +32,57 @@ module Rule
|
|
30
32
|
resource_ids = parse_logical_resource_ids(stdout)
|
31
33
|
new_warnings = resource_ids.size
|
32
34
|
if result == 0 and new_warnings > 0
|
33
|
-
add_violation(
|
35
|
+
add_violation(id: warning_def.id,
|
36
|
+
type: Violation::WARNING,
|
34
37
|
message: message,
|
35
38
|
logical_resource_ids: resource_ids)
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
def raw_fatal_assertion(jq:, message:)
|
40
|
-
failing_rule(
|
42
|
+
def raw_fatal_assertion(id:, jq:, message:)
|
43
|
+
failing_rule(id: id,
|
44
|
+
jq_expression: jq,
|
41
45
|
fail_if_found: false,
|
42
46
|
fatal: true,
|
43
47
|
message: message,
|
44
48
|
raw: true)
|
45
49
|
end
|
46
50
|
|
47
|
-
def fatal_assertion(jq:, message:)
|
48
|
-
failing_rule(
|
51
|
+
def fatal_assertion(id:, jq:, message:)
|
52
|
+
failing_rule(id: id,
|
53
|
+
jq_expression: jq,
|
49
54
|
fail_if_found: false,
|
50
55
|
fatal: true,
|
51
56
|
message: message)
|
52
57
|
end
|
53
58
|
|
54
|
-
def raw_fatal_violation(jq:, message:)
|
55
|
-
failing_rule(
|
59
|
+
def raw_fatal_violation(id:, jq:, message:)
|
60
|
+
failing_rule(id: id,
|
61
|
+
jq_expression: jq,
|
56
62
|
fail_if_found: true,
|
57
63
|
fatal: true,
|
58
64
|
message: message,
|
59
65
|
raw: true)
|
60
66
|
end
|
61
67
|
|
62
|
-
def fatal_violation(jq:, message:)
|
63
|
-
failing_rule(
|
68
|
+
def fatal_violation(id:, jq:, message:)
|
69
|
+
failing_rule(id: id,
|
70
|
+
jq_expression: jq,
|
64
71
|
fail_if_found: true,
|
65
72
|
fatal: true,
|
66
73
|
message: message)
|
67
74
|
end
|
68
75
|
|
69
|
-
def violation(jq:, message:)
|
70
|
-
failing_rule(
|
76
|
+
def violation(id:, jq:, message:)
|
77
|
+
failing_rule(id: id,
|
78
|
+
jq_expression: jq,
|
71
79
|
fail_if_found: true,
|
72
80
|
message: message)
|
73
81
|
end
|
74
82
|
|
75
|
-
def assertion(jq:, message:)
|
76
|
-
failing_rule(
|
83
|
+
def assertion(id:, jq:, message:)
|
84
|
+
failing_rule(id: id,
|
85
|
+
jq_expression: jq,
|
77
86
|
fail_if_found: false,
|
78
87
|
message: message)
|
79
88
|
end
|
@@ -108,11 +117,17 @@ module Rule
|
|
108
117
|
end
|
109
118
|
end
|
110
119
|
|
111
|
-
|
120
|
+
# this is to record a violation as opposed to "registering" a violation
|
121
|
+
#
|
122
|
+
# not super keen on this looking at it after the fact.... @violations
|
123
|
+
# will have to live in the object this Rule is mixed into i.e. CfnNag
|
124
|
+
def add_violation(id:,
|
125
|
+
type:,
|
112
126
|
message:,
|
113
127
|
logical_resource_ids: nil,
|
114
128
|
violating_code: nil)
|
115
|
-
violation = Violation.new(
|
129
|
+
violation = Violation.new(id: id,
|
130
|
+
type: type,
|
116
131
|
message: message,
|
117
132
|
logical_resource_ids: logical_resource_ids,
|
118
133
|
violating_code: violating_code)
|
@@ -135,12 +150,17 @@ module Rule
|
|
135
150
|
# failure count by 1
|
136
151
|
#
|
137
152
|
# fatal: if true, any match of the rule causes immediate shutdown to avoid more complicated downstream error checking
|
138
|
-
def failing_rule(
|
153
|
+
def failing_rule(id:,
|
154
|
+
jq_expression:,
|
139
155
|
fail_if_found:,
|
140
156
|
message:,
|
141
157
|
fatal: false,
|
142
158
|
raw: false)
|
143
|
-
|
159
|
+
|
160
|
+
fail_def = @rule_registry.definition(id: id,
|
161
|
+
type: Violation::FAILING_VIOLATION,
|
162
|
+
message: message)
|
163
|
+
|
144
164
|
return if @stop_processing
|
145
165
|
|
146
166
|
Logging.logger['log'].debug jq_expression
|
@@ -152,7 +172,8 @@ module Rule
|
|
152
172
|
(not fail_if_found and result != 0)
|
153
173
|
|
154
174
|
if raw
|
155
|
-
add_violation(
|
175
|
+
add_violation(id: fail_def.id,
|
176
|
+
type: Violation::FAILING_VIOLATION,
|
156
177
|
message: message,
|
157
178
|
violating_code: stdout)
|
158
179
|
|
@@ -163,7 +184,8 @@ module Rule
|
|
163
184
|
resource_ids = parse_logical_resource_ids(stdout)
|
164
185
|
|
165
186
|
if resource_ids.size > 0
|
166
|
-
add_violation(
|
187
|
+
add_violation(id: fail_def.id,
|
188
|
+
type: Violation::FAILING_VIOLATION,
|
167
189
|
message: message,
|
168
190
|
logical_resource_ids: resource_ids)
|
169
191
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative 'violation'
|
2
|
+
|
3
|
+
class RuleRegistry
|
4
|
+
|
5
|
+
attr_reader :rules
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@rules = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def definition(id:,
|
12
|
+
type:,
|
13
|
+
message:)
|
14
|
+
violation_def = Violation.new(id: id,
|
15
|
+
type: type,
|
16
|
+
message: message)
|
17
|
+
existing_def = @rules.find { |definition| definition == violation_def }
|
18
|
+
|
19
|
+
if existing_def.nil?
|
20
|
+
add_rule violation_def
|
21
|
+
else
|
22
|
+
existing_def
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# FATAL applies to multiple rules
|
27
|
+
def by_id(id)
|
28
|
+
@rules.select { |rule| rule.id == id }
|
29
|
+
end
|
30
|
+
|
31
|
+
def warnings
|
32
|
+
@rules.select { |rule| rule.type == Violation::WARNING }
|
33
|
+
end
|
34
|
+
|
35
|
+
def failings
|
36
|
+
@rules.select { |rule| rule.type == Violation::FAILING_VIOLATION }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def add_rule(violation_def)
|
42
|
+
@rules << violation_def
|
43
|
+
violation_def
|
44
|
+
end
|
45
|
+
end
|
data/lib/violation.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
require 'json'
|
2
|
+
|
2
3
|
class Violation
|
3
4
|
WARNING = 'WARN'
|
4
5
|
FAILING_VIOLATION = 'FAIL'
|
5
6
|
|
6
|
-
attr_reader :type, :message, :logical_resource_ids, :violating_code
|
7
|
+
attr_reader :id, :type, :message, :logical_resource_ids, :violating_code
|
7
8
|
|
8
|
-
def initialize(
|
9
|
+
def initialize(id:,
|
10
|
+
type:,
|
9
11
|
message:,
|
10
12
|
logical_resource_ids: nil,
|
11
13
|
violating_code: nil)
|
14
|
+
@id = id
|
12
15
|
@type = type
|
13
16
|
@message = message
|
14
17
|
@logical_resource_ids = logical_resource_ids
|
@@ -19,11 +22,12 @@ class Violation
|
|
19
22
|
end
|
20
23
|
|
21
24
|
def to_s
|
22
|
-
puts "#{@type} #{@message} #{@logical_resource_ids} #{@violating_code}"
|
25
|
+
puts "#{@id} #{@type} #{@message} #{@logical_resource_ids} #{@violating_code}"
|
23
26
|
end
|
24
27
|
|
25
28
|
def to_h
|
26
29
|
{
|
30
|
+
id: @id,
|
27
31
|
type: @type,
|
28
32
|
message: @message,
|
29
33
|
logical_resource_ids: @logical_resource_ids,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.35
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- someguy
|
@@ -73,9 +73,12 @@ files:
|
|
73
73
|
- lib/model/s3_bucket_policy.rb
|
74
74
|
- lib/model/s3_bucket_policy_parser.rb
|
75
75
|
- lib/model/security_group_parser.rb
|
76
|
+
- lib/profile.rb
|
77
|
+
- lib/profile_loader.rb
|
76
78
|
- lib/result_view/json_results.rb
|
77
79
|
- lib/result_view/simple_stdout_results.rb
|
78
80
|
- lib/rule.rb
|
81
|
+
- lib/rule_registry.rb
|
79
82
|
- lib/violation.rb
|
80
83
|
homepage: https://github.com/stelligent/cfn_nag
|
81
84
|
licenses:
|