cfn-nag 0.0.16 → 0.0.17
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_rules +4 -0
- data/lib/cfn_nag.rb +45 -5
- data/lib/custom_rules/security_group_missing_egress.rb +5 -1
- data/lib/custom_rules/user_missing_group.rb +5 -1
- data/lib/json_rules/basic_rules.rb +1 -1
- data/lib/json_rules/cidr_rules.rb +30 -15
- data/lib/json_rules/iam_policy_rules.rb +1 -1
- data/lib/json_rules/iam_user_rules.rb +1 -1
- data/lib/json_rules/port_rules.rb +23 -11
- data/lib/rule.rb +14 -18
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3284f4eba9d2cab9d7790dab39273f9401fbb09
|
4
|
+
data.tar.gz: e715f4800f39198bd95bc1fb6044ce1dd64644d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06b61112e6e6cff426fcd6e07af0c0aa75f85f38914816bc1df48286e4aec04d76d9000d61202c9fae3a138e50d55c9af8ca0da56971f07644bdfde4b80f92a8
|
7
|
+
data.tar.gz: 43147a231964233735ee827ee0f1c9ec4894f6725904e95b137b3e8205181213d5ea5698e75ce704fd695025015e9a31467d65d1e14d0580587d79dfbdc75071
|
data/bin/cfn_nag_rules
ADDED
data/lib/cfn_nag.rb
CHANGED
@@ -4,10 +4,47 @@ require_relative 'custom_rules/user_missing_group'
|
|
4
4
|
require_relative 'model/cfn_model'
|
5
5
|
require_relative 'result_view/simple_stdout_results'
|
6
6
|
require_relative 'result_view/json_results'
|
7
|
+
require 'tempfile'
|
7
8
|
|
8
9
|
class CfnNag
|
9
10
|
include Rule
|
10
11
|
|
12
|
+
def initialize
|
13
|
+
@warning_registry = []
|
14
|
+
@violation_registry = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump_rules
|
18
|
+
dummy_cfn = <<-END
|
19
|
+
{
|
20
|
+
"Resources": {
|
21
|
+
"resource1": {
|
22
|
+
"Type" : "AWS::EC2::DHCPOptions",
|
23
|
+
"Properties": {
|
24
|
+
"DomainNameServers" : [ "10.0.0.1" ]
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
END
|
30
|
+
|
31
|
+
Tempfile.open('tempfile') do |dummy_cfn_template|
|
32
|
+
dummy_cfn_template.write dummy_cfn
|
33
|
+
dummy_cfn_template.rewind
|
34
|
+
audit_file(input_json_path: dummy_cfn_template.path)
|
35
|
+
end
|
36
|
+
|
37
|
+
custom_rule_registry.each do |rule_class|
|
38
|
+
@violation_registry << rule_class.new.rule_text
|
39
|
+
end
|
40
|
+
|
41
|
+
puts 'WARNING VIOLATIONS:'
|
42
|
+
puts @warning_registry.sort
|
43
|
+
puts
|
44
|
+
puts 'FAILING VIOLATIONS:'
|
45
|
+
puts @violation_registry.sort
|
46
|
+
end
|
47
|
+
|
11
48
|
def audit(input_json_path:,
|
12
49
|
output_format:'txt')
|
13
50
|
|
@@ -129,13 +166,16 @@ class CfnNag
|
|
129
166
|
|
130
167
|
def custom_rules(input_json_path)
|
131
168
|
cfn_model = CfnModel.new.parse(IO.read(input_json_path))
|
132
|
-
|
133
|
-
SecurityGroupMissingEgressRule,
|
134
|
-
UserMissingGroupRule
|
135
|
-
]
|
136
|
-
rules.each do |rule_class|
|
169
|
+
custom_rule_registry.each do |rule_class|
|
137
170
|
audit_result = rule_class.new.audit(cfn_model)
|
138
171
|
@violations << audit_result unless audit_result.nil?
|
139
172
|
end
|
140
173
|
end
|
174
|
+
|
175
|
+
def custom_rule_registry
|
176
|
+
[
|
177
|
+
SecurityGroupMissingEgressRule,
|
178
|
+
UserMissingGroupRule
|
179
|
+
]
|
180
|
+
end
|
141
181
|
end
|
@@ -2,6 +2,10 @@ require_relative '../violation'
|
|
2
2
|
|
3
3
|
class SecurityGroupMissingEgressRule
|
4
4
|
|
5
|
+
def rule_text
|
6
|
+
'Missing egress rule means all traffic is allowed outbound. Make this explicit if it is desired configuration'
|
7
|
+
end
|
8
|
+
|
5
9
|
def audit(cfn_model)
|
6
10
|
logical_resource_ids = []
|
7
11
|
cfn_model.security_groups.each do |security_group|
|
@@ -12,7 +16,7 @@ class SecurityGroupMissingEgressRule
|
|
12
16
|
|
13
17
|
if logical_resource_ids.size > 0
|
14
18
|
Violation.new(type: Violation::FAILING_VIOLATION,
|
15
|
-
message:
|
19
|
+
message: rule_text,
|
16
20
|
logical_resource_ids: logical_resource_ids)
|
17
21
|
else
|
18
22
|
nil
|
@@ -2,6 +2,10 @@ require_relative '../violation'
|
|
2
2
|
|
3
3
|
class UserMissingGroupRule
|
4
4
|
|
5
|
+
def rule_text
|
6
|
+
'User is not assigned to a group'
|
7
|
+
end
|
8
|
+
|
5
9
|
def audit(cfn_model)
|
6
10
|
logical_resource_ids = []
|
7
11
|
cfn_model.iam_users.each do |iam_user|
|
@@ -12,7 +16,7 @@ class UserMissingGroupRule
|
|
12
16
|
|
13
17
|
if logical_resource_ids.size > 0
|
14
18
|
Violation.new(type: Violation::FAILING_VIOLATION,
|
15
|
-
message:
|
19
|
+
message: rule_text,
|
16
20
|
logical_resource_ids: logical_resource_ids)
|
17
21
|
else
|
18
22
|
nil
|
@@ -28,19 +28,34 @@ warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | selec
|
|
28
28
|
warning 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
29
|
message: 'Security Group Standalone Ingress cidr found that is not /32'
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
non_32_cidr_jq_expression = <<END
|
32
|
+
[.Resources |
|
33
|
+
with_entries(.value.LogicalResourceId = .key)[] |
|
34
|
+
select(.Type == "AWS::EC2::SecurityGroup") |
|
35
|
+
if (.Properties.SecurityGroupIngress|type == "object")
|
36
|
+
then (
|
37
|
+
select(.Properties.SecurityGroupIngress.CidrIp|type == "string")|
|
38
|
+
select(.Properties.SecurityGroupIngress.CidrIp|test("^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}/(?!32)$"))
|
39
|
+
)
|
40
|
+
else (
|
41
|
+
if (.Properties.SecurityGroupIngress|type == "array")
|
42
|
+
then (
|
43
|
+
select(.Properties.SecurityGroupIngress[].CidrIp|type == "string")|
|
44
|
+
select(.Properties.SecurityGroupIngress[].CidrIp |
|
45
|
+
(
|
46
|
+
if (.|type=="string")
|
47
|
+
then test("^\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}/(?!32)$")
|
48
|
+
else empty
|
49
|
+
end
|
50
|
+
)
|
51
|
+
)
|
52
|
+
)
|
53
|
+
else empty
|
54
|
+
end
|
55
|
+
)
|
56
|
+
end
|
57
|
+
]|map(.LogicalResourceId)
|
58
|
+
END
|
59
|
+
|
60
|
+
warning jq: non_32_cidr_jq_expression,
|
37
61
|
message: 'Security Groups found with cidr that is not /32'
|
38
|
-
|
39
|
-
|
40
|
-
#for inline, this covers it, but with an externalized egress rule... the expression gets real evil
|
41
|
-
#i guess the ideal would be to do a join of ingress and egress rules with the parent sg
|
42
|
-
#but it gets real hairy with FnGetAtt for GroupId and all that.... think it best to
|
43
|
-
#write some imperative code in custom rules to take care of things
|
44
|
-
# violation '.Resources[]|select(.Type == "AWS::EC2::SecurityGroup")|select(.Properties.SecurityGroupEgress == null or .Properties.SecurityGroupEgress.length? == 0)' do |security_groups|
|
45
|
-
# puts "Security Groups found without egress json_rules: #{security_groups}"
|
46
|
-
# end
|
@@ -55,7 +55,7 @@ END
|
|
55
55
|
|
56
56
|
warning jq: allow_not_action_filter +
|
57
57
|
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
58
|
-
message: 'IAM role should not allow Allow+NotAction'
|
58
|
+
message: 'IAM role should not allow Allow+NotAction on trust permissinos'
|
59
59
|
|
60
60
|
|
61
61
|
warning jq: allow_not_action_filter +
|
@@ -8,5 +8,5 @@ violation jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | sel
|
|
8
8
|
|
9
9
|
violation jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::ManagedPolicy")|'\
|
10
10
|
'select(.Properties.Users|length > 0)]|map(.LogicalResourceId) ',
|
11
|
-
message: 'IAM policy should not apply directly to users. Should be on group'
|
11
|
+
message: 'IAM managed policy should not apply directly to users. Should be on group'
|
12
12
|
|
@@ -1,17 +1,29 @@
|
|
1
|
-
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] |
|
1
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | '\
|
2
|
+
'select(.Type == "AWS::EC2::SecurityGroup")| '\
|
3
|
+
'if (.Properties.SecurityGroupIngress|type == "object") '\
|
4
|
+
'then select(.Properties.SecurityGroupIngress.ToPort != .Properties.SecurityGroupIngress.FromPort) '\
|
5
|
+
'else if (.Properties.SecurityGroupIngress|type == "array") '\
|
6
|
+
' then select([.Properties.SecurityGroupIngress[].ToPort] != [.Properties.SecurityGroupIngress[].FromPort]) '\
|
7
|
+
' else empty '\
|
8
|
+
' end '\
|
9
|
+
'end '\
|
10
|
+
']|map(.LogicalResourceId)',
|
2
11
|
message: 'Security Groups found ingress with port range instead of just a single port'
|
3
12
|
|
4
|
-
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::
|
5
|
-
message: 'Security
|
6
|
-
|
7
|
-
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupEgress|type == "object"))|select(.Properties.SecurityGroupEgress.ToPort != .Properties.SecurityGroupEgress.FromPort)]|map(.LogicalResourceId)',
|
8
|
-
message: 'Security Groups found egress with port range instead of just a single port'
|
13
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupIngress")|select(.Properties.ToPort != .Properties.FromPort)]|map(.LogicalResourceId)',
|
14
|
+
message: 'Security Group ingress with port range instead of just a single port'
|
9
15
|
|
10
|
-
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] |
|
16
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | '\
|
17
|
+
'select(.Type == "AWS::EC2::SecurityGroup")| '\
|
18
|
+
'if (.Properties.SecurityGroupEgress|type == "object") '\
|
19
|
+
'then select(.Properties.SecurityGroupEgress.ToPort != .Properties.SecurityGroupEgress.FromPort) '\
|
20
|
+
'else if (.Properties.SecurityGroupEgress|type == "array") '\
|
21
|
+
' then select([.Properties.SecurityGroupEgress[].ToPort] != [.Properties.SecurityGroupEgress[].FromPort]) '\
|
22
|
+
' else empty '\
|
23
|
+
' end '\
|
24
|
+
'end'\
|
25
|
+
']|map(.LogicalResourceId)',
|
11
26
|
message: 'Security Groups found egress with port range instead of just a single port'
|
12
27
|
|
13
|
-
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupIngress")|select(.Properties.ToPort != .Properties.FromPort)]|map(.LogicalResourceId)',
|
14
|
-
message: 'Security Groups found ingress with port range instead of just a single port'
|
15
|
-
|
16
28
|
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupEgress")|select(.Properties.ToPort != .Properties.FromPort)]|map(.LogicalResourceId)',
|
17
|
-
message: 'Security
|
29
|
+
message: 'Security Group egress with port range instead of just a single port'
|
data/lib/rule.rb
CHANGED
@@ -17,13 +17,15 @@ module Rule
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def warning(jq:, message:)
|
20
|
+
@warning_registry << message
|
21
|
+
|
20
22
|
return if @stop_processing
|
21
23
|
|
22
24
|
Logging.logger['log'].debug jq
|
23
25
|
|
24
26
|
stdout = jq_command(@input_json_path, jq)
|
25
27
|
result = $?.exitstatus
|
26
|
-
scrape_jq_output_for_error(stdout)
|
28
|
+
scrape_jq_output_for_error(jq, stdout)
|
27
29
|
|
28
30
|
resource_ids = parse_logical_resource_ids(stdout)
|
29
31
|
new_warnings = resource_ids.size
|
@@ -39,7 +41,6 @@ module Rule
|
|
39
41
|
fail_if_found: false,
|
40
42
|
fatal: true,
|
41
43
|
message: message,
|
42
|
-
message_type: Violation::FAILING_VIOLATION,
|
43
44
|
raw: true)
|
44
45
|
end
|
45
46
|
|
@@ -47,8 +48,7 @@ module Rule
|
|
47
48
|
failing_rule(jq_expression: jq,
|
48
49
|
fail_if_found: false,
|
49
50
|
fatal: true,
|
50
|
-
message: message
|
51
|
-
message_type: Violation::FAILING_VIOLATION)
|
51
|
+
message: message)
|
52
52
|
end
|
53
53
|
|
54
54
|
def raw_fatal_violation(jq:, message:)
|
@@ -56,7 +56,6 @@ module Rule
|
|
56
56
|
fail_if_found: true,
|
57
57
|
fatal: true,
|
58
58
|
message: message,
|
59
|
-
message_type: Violation::FAILING_VIOLATION,
|
60
59
|
raw: true)
|
61
60
|
end
|
62
61
|
|
@@ -64,22 +63,19 @@ module Rule
|
|
64
63
|
failing_rule(jq_expression: jq,
|
65
64
|
fail_if_found: true,
|
66
65
|
fatal: true,
|
67
|
-
message: message
|
68
|
-
message_type: Violation::FAILING_VIOLATION)
|
66
|
+
message: message)
|
69
67
|
end
|
70
68
|
|
71
69
|
def violation(jq:, message:)
|
72
70
|
failing_rule(jq_expression: jq,
|
73
71
|
fail_if_found: true,
|
74
|
-
message: message
|
75
|
-
message_type: Violation::FAILING_VIOLATION)
|
72
|
+
message: message)
|
76
73
|
end
|
77
74
|
|
78
75
|
def assertion(jq:, message:)
|
79
76
|
failing_rule(jq_expression: jq,
|
80
77
|
fail_if_found: false,
|
81
|
-
message: message
|
82
|
-
message_type: Violation::FAILING_VIOLATION)
|
78
|
+
message: message)
|
83
79
|
end
|
84
80
|
|
85
81
|
def self.empty?(array)
|
@@ -129,8 +125,8 @@ module Rule
|
|
129
125
|
JSON.load(stdout)
|
130
126
|
end
|
131
127
|
|
132
|
-
def scrape_jq_output_for_error(stdout)
|
133
|
-
fail
|
128
|
+
def scrape_jq_output_for_error(command, stdout)
|
129
|
+
fail "jq rule is likely not correct: #{command}\n\n#{stdout}" if stdout.include? 'jq: error'
|
134
130
|
end
|
135
131
|
|
136
132
|
# fail_if_found: this is false for an assertion, true for a violation. either way this rule ups the "failure" count
|
@@ -142,21 +138,21 @@ module Rule
|
|
142
138
|
def failing_rule(jq_expression:,
|
143
139
|
fail_if_found:,
|
144
140
|
message:,
|
145
|
-
message_type:,
|
146
141
|
fatal: false,
|
147
142
|
raw: false)
|
143
|
+
@violation_registry << message
|
148
144
|
return if @stop_processing
|
149
145
|
|
150
146
|
Logging.logger['log'].debug jq_expression
|
151
147
|
|
152
148
|
stdout = jq_command(@input_json_path, jq_expression)
|
153
149
|
result = $?.exitstatus
|
154
|
-
scrape_jq_output_for_error(stdout)
|
150
|
+
scrape_jq_output_for_error(jq_expression, stdout)
|
155
151
|
if (fail_if_found and result == 0) or
|
156
152
|
(not fail_if_found and result != 0)
|
157
153
|
|
158
154
|
if raw
|
159
|
-
add_violation(type:
|
155
|
+
add_violation(type: Violation::FAILING_VIOLATION,
|
160
156
|
message: message,
|
161
157
|
violating_code: stdout)
|
162
158
|
|
@@ -167,7 +163,7 @@ module Rule
|
|
167
163
|
resource_ids = parse_logical_resource_ids(stdout)
|
168
164
|
|
169
165
|
if resource_ids.size > 0
|
170
|
-
add_violation(type:
|
166
|
+
add_violation(type: Violation::FAILING_VIOLATION,
|
171
167
|
message: message,
|
172
168
|
logical_resource_ids: resource_ids)
|
173
169
|
|
@@ -181,7 +177,7 @@ module Rule
|
|
181
177
|
|
182
178
|
# the -e will return an exit code
|
183
179
|
def jq_command(input_json_path, jq_expression)
|
184
|
-
command = "cat #{input_json_path} | jq '#{jq_expression}' -e"
|
180
|
+
command = "cat #{input_json_path} | jq '#{jq_expression}' -e 2>&1"
|
185
181
|
|
186
182
|
Logging.logger['log'].debug command
|
187
183
|
|
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.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- someguy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: logging
|
@@ -42,10 +42,12 @@ description: Auditing tool for Cloudformation templates
|
|
42
42
|
email:
|
43
43
|
executables:
|
44
44
|
- cfn_nag
|
45
|
+
- cfn_nag_rules
|
45
46
|
extensions: []
|
46
47
|
extra_rdoc_files: []
|
47
48
|
files:
|
48
49
|
- bin/cfn_nag
|
50
|
+
- bin/cfn_nag_rules
|
49
51
|
- lib/cfn_nag.rb
|
50
52
|
- lib/custom_rules/security_group_missing_egress.rb
|
51
53
|
- lib/custom_rules/user_missing_group.rb
|