cfn-nag 0.0.16 → 0.0.17
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_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
|