cfn-nag 0.0.34 → 0.0.35
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|