cfn-nag 0.0.4
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 +7 -0
- data/bin/cfn_nag +12 -0
- data/lib/cfn_nag.rb +71 -0
- data/lib/custom_rules/security_group_missing_egress.rb +26 -0
- data/lib/custom_rules/user_missing_group.rb +26 -0
- data/lib/json_rules/basic_rules.rb +42 -0
- data/lib/json_rules/cidr_rules.rb +46 -0
- data/lib/json_rules/iam_policy_rules.rb +110 -0
- data/lib/json_rules/iam_user_rules.rb +12 -0
- data/lib/json_rules/port_rules.rb +17 -0
- data/lib/model/cfn_model.rb +187 -0
- data/lib/model/iam_user_parser.rb +34 -0
- data/lib/model/security_group_parser.rb +59 -0
- data/lib/rule.rb +168 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 17e63b5f6090fdcd198d81ab01f4ad793f54e71e
|
4
|
+
data.tar.gz: 484a60530c4bf5efb6410c7b5eb5a8f46ba9f869
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff360162ddf6f1378777065b8a6c1079c54bcac2f532edd6246f3c80af51f37d232889014897164b60970e2e2ac4f51db699dbf4a1b60bf2b660a378ea81629b
|
7
|
+
data.tar.gz: c5f4df2a906d9a7c65910d1a917997b8792449763ca7aa0da551c6a854728605b2bab8a8e13ea62893d180e97c536b5d0fb14c4ed6d5e54294514e71e51c2323
|
data/bin/cfn_nag
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'trollop'
|
3
|
+
require 'cfn_nag'
|
4
|
+
require 'logging'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
opt :input_json, 'Cloudformation template to nag on', type: :string, required: true
|
8
|
+
opt :debug, 'Enable debug output', type: :boolean, required: false, default: false
|
9
|
+
end
|
10
|
+
|
11
|
+
CfnNag::configure_logging(opts)
|
12
|
+
exit CfnNag.new.audit(opts[:input_json])
|
data/lib/cfn_nag.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative 'rule'
|
2
|
+
require_relative 'custom_rules/security_group_missing_egress'
|
3
|
+
require_relative 'custom_rules/user_missing_group'
|
4
|
+
require_relative 'model/cfn_model'
|
5
|
+
|
6
|
+
class CfnNag
|
7
|
+
include Rule
|
8
|
+
|
9
|
+
def audit(input_json_path)
|
10
|
+
fail 'not even legit JSON' unless legal_json?(input_json_path)
|
11
|
+
|
12
|
+
@violation_count = 0
|
13
|
+
@warning_count = 0
|
14
|
+
|
15
|
+
generic_json_rules input_json_path
|
16
|
+
|
17
|
+
custom_rules input_json_path
|
18
|
+
|
19
|
+
puts "Violations count: #{@violation_count}"
|
20
|
+
puts "Warnings count: #{@warning_count}"
|
21
|
+
@violation_count
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.configure_logging(opts)
|
25
|
+
logger = Logging.logger['log']
|
26
|
+
if opts[:debug]
|
27
|
+
logger.level = :debug
|
28
|
+
else
|
29
|
+
logger.level = :info
|
30
|
+
end
|
31
|
+
|
32
|
+
logger.add_appenders Logging.appenders.stdout
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def legal_json?(input_json_path)
|
38
|
+
begin
|
39
|
+
JSON.parse(IO.read(input_json_path))
|
40
|
+
true
|
41
|
+
rescue JSON::ParserError
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def command?(command)
|
47
|
+
system("which #{command} > /dev/null 2>&1")
|
48
|
+
end
|
49
|
+
|
50
|
+
def generic_json_rules(input_json_path)
|
51
|
+
unless command? 'jq'
|
52
|
+
fail 'jq executable must be available in PATH'
|
53
|
+
end
|
54
|
+
|
55
|
+
Dir[File.join(__dir__, 'json_rules', '*.rb')].sort.each do |rule_file|
|
56
|
+
@input_json_path = input_json_path
|
57
|
+
eval IO.read(rule_file)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def custom_rules(input_json_path)
|
62
|
+
cfn_model = CfnModel.new.parse(IO.read(input_json_path))
|
63
|
+
rules = [
|
64
|
+
SecurityGroupMissingEgressRule,
|
65
|
+
UserMissingGroupRule
|
66
|
+
]
|
67
|
+
rules.each do |rule_class|
|
68
|
+
@violation_count += rule_class.new.audit(cfn_model)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative '../rule'
|
2
|
+
|
3
|
+
class SecurityGroupMissingEgressRule
|
4
|
+
include Rule
|
5
|
+
|
6
|
+
def audit(cfn_model)
|
7
|
+
violation_count = 0
|
8
|
+
|
9
|
+
logical_resource_ids = []
|
10
|
+
violating_security_groups = []
|
11
|
+
cfn_model.security_groups.each do |security_group|
|
12
|
+
if security_group.egress_rules.size == 0
|
13
|
+
logical_resource_ids << security_group.logical_resource_id
|
14
|
+
violating_security_groups << security_group
|
15
|
+
violation_count += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if violation_count > 0
|
20
|
+
message message_type: 'violation',
|
21
|
+
message: 'Missing egress rule means all traffic is allowed outbound. Make this explicit if it is desired configuration',
|
22
|
+
logical_resource_ids: logical_resource_ids
|
23
|
+
end
|
24
|
+
violation_count
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative '../rule'
|
2
|
+
|
3
|
+
class UserMissingGroupRule
|
4
|
+
include Rule
|
5
|
+
|
6
|
+
def audit(cfn_model)
|
7
|
+
violation_count = 0
|
8
|
+
|
9
|
+
logical_resource_ids = []
|
10
|
+
violating_iam_users = []
|
11
|
+
cfn_model.iam_users.each do |iam_user|
|
12
|
+
if iam_user.groups.size == 0
|
13
|
+
logical_resource_ids << iam_user.logical_resource_id
|
14
|
+
violating_iam_users << iam_user
|
15
|
+
violation_count += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if violation_count > 0
|
20
|
+
message message_type: 'violation',
|
21
|
+
message: 'User is not assigned to a group',
|
22
|
+
logical_resource_ids: logical_resource_ids
|
23
|
+
end
|
24
|
+
violation_count
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
raw_fatal_assertion jq: '.Resources|length > 0',
|
2
|
+
message: 'Must have at least 1 resource'
|
3
|
+
|
4
|
+
|
5
|
+
%w(
|
6
|
+
AWS::IAM::Role
|
7
|
+
AWS::IAM::Policy
|
8
|
+
AWS::IAM::ManagedPolicy
|
9
|
+
AWS::IAM::UserToGroupAddition
|
10
|
+
AWS::EC2::SecurityGroup
|
11
|
+
AWS::EC2::SecurityGroupIngress
|
12
|
+
AWS::EC2::SecurityGroupEgress
|
13
|
+
).each do |resource_must_have_properties|
|
14
|
+
fatal_violation jq: "[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == \"#{resource_must_have_properties}\" and .Properties == null)]|map(.LogicalResourceId)",
|
15
|
+
message: "#{resource_must_have_properties} must have Properties"
|
16
|
+
end
|
17
|
+
|
18
|
+
missing_reference_jq = <<END
|
19
|
+
(
|
20
|
+
(
|
21
|
+
([..|.Ref?]|map(select(. != null)) + [..|."Fn::GetAtt"?[0]]|map(select(. != null)))
|
22
|
+
)
|
23
|
+
-
|
24
|
+
(
|
25
|
+
["AWS::AccountId","AWS::StackName","AWS::Region","AWS::StackId","AWS::NoValue"] +
|
26
|
+
([.Resources|keys]|flatten) +
|
27
|
+
(if .Parameters? then ([.Parameters|keys]|flatten) else [] end)
|
28
|
+
)
|
29
|
+
)|if length==0 then false else . end
|
30
|
+
END
|
31
|
+
|
32
|
+
raw_fatal_violation jq: missing_reference_jq,
|
33
|
+
message: 'All Ref and Fn::GetAtt must reference existing logical resource ids'
|
34
|
+
|
35
|
+
|
36
|
+
%w(
|
37
|
+
AWS::EC2::SecurityGroupIngress
|
38
|
+
AWS::EC2::SecurityGroupEgress
|
39
|
+
).each do |xgress|
|
40
|
+
fatal_violation jq: "[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == \"#{xgress}\" and .Properties.GroupName != null)]|map(.LogicalResourceId)",
|
41
|
+
message: "#{xgress} must not have GroupName - EC2 classic is a no-go!"
|
42
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
##### inline ingress
|
2
|
+
warning 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
|
+
message: 'Security Groups found with cidr open to world on ingress. This should never be true on instance. Permissible on ELB'
|
4
|
+
|
5
|
+
warning 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
|
+
message: 'Security Groups found with cidr open to world on ingress array. This should never be true on instance. Permissible on ELB'
|
7
|
+
|
8
|
+
##### external ingress
|
9
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupIngress")|select(.Properties.CidrIp? == "0.0.0.0/0")]|map(.LogicalResourceId)',
|
10
|
+
message: 'Security Group Standalone Ingress found with cidr open to world. This should never be true on instance. Permissible on ELB'
|
11
|
+
|
12
|
+
|
13
|
+
###### inline egress
|
14
|
+
warning 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
|
+
message: 'Security Groups found with cidr open to world on egress'
|
16
|
+
|
17
|
+
|
18
|
+
warning 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
|
+
message: 'Security Groups found with cidr open to world on egress array'
|
20
|
+
|
21
|
+
|
22
|
+
##### external egress
|
23
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupEgress")|select(.Properties.CidrIp? == "0.0.0.0/0")]|map(.LogicalResourceId)',
|
24
|
+
message: 'Security Group Standalone Egress found with cidr open to world.'
|
25
|
+
|
26
|
+
|
27
|
+
# BEWARE with escapes \d -> \\\d because of how the escapes get munged from ruby through to shell
|
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
|
+
message: 'Security Group Standalone Ingress cidr found that is not /32'
|
30
|
+
|
31
|
+
|
32
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupIngress|type == "object"))|select(.Properties.SecurityGroupIngress.CidrIp|type == "string")|select(.Properties.SecurityGroupIngress.CidrIp | test("^\\\d{1,3}\\\.\\\d{1,3}\\\.\\\d{1,3}\\\.\\\d{1,3}/(?!32)$") )]|map(.LogicalResourceId)',
|
33
|
+
message: 'Security Groups found with cidr that is not /32'
|
34
|
+
|
35
|
+
|
36
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupIngress|type == "array"))|select(.Properties.SecurityGroupIngress[].CidrIp|type == "string")|select(.Properties.SecurityGroupIngress[].CidrIp | if .|type=="string" then test("^\\\d{1,3}\\\.\\\d{1,3}\\\.\\\d{1,3}\\\.\\\d{1,3}/(?!32)$") else false end)]|map(.LogicalResourceId)',
|
37
|
+
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
|
@@ -0,0 +1,110 @@
|
|
1
|
+
wildcard_action_filter = <<END
|
2
|
+
def wildcard_action:
|
3
|
+
if .Statement|type == "object"
|
4
|
+
then select(.Statement.Effect == "Allow" and .Statement|if .Action|type=="string" then select(.Action == "*") else select(.Action|index("*")) end)
|
5
|
+
else select(.Statement[]|if .Action|type=="string" then select(.Effect == "Allow" and .Action == "*") else select(.Effect == "Allow" and (.Action|index("*"))) end)
|
6
|
+
end;
|
7
|
+
END
|
8
|
+
|
9
|
+
violation jq: wildcard_action_filter +
|
10
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|wildcard_action)]|map(.LogicalResourceId) ",
|
11
|
+
message: 'IAM role should not allow * action on its trust policy'
|
12
|
+
|
13
|
+
violation jq: wildcard_action_filter +
|
14
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|wildcard_action)]|map(.LogicalResourceId)",
|
15
|
+
message: 'IAM role should not allow * action on its permissions policy'
|
16
|
+
|
17
|
+
|
18
|
+
violation jq: wildcard_action_filter +
|
19
|
+
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|wildcard_action)]|map(.LogicalResourceId) ",
|
20
|
+
message: 'IAM policy should not allow * action'
|
21
|
+
|
22
|
+
|
23
|
+
violation jq: wildcard_action_filter +
|
24
|
+
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|wildcard_action)]|map(.LogicalResourceId) ",
|
25
|
+
message: 'IAM managed policy should not allow * action'
|
26
|
+
|
27
|
+
|
28
|
+
wildcard_resource_filter = <<END
|
29
|
+
def wildcard_resource:
|
30
|
+
if .Statement|type == "object"
|
31
|
+
then select(.Statement.Effect == "Allow" and .Statement|if .Resource|type=="string" then select(.Resource == "*") else select(.Resource|index("*")) end)
|
32
|
+
else select(.Statement[]|if .Resource|type=="string" then select(.Effect == "Allow" and .Resource == "*") else (if .Resource|type=="array" then select(.Effect == "Allow" and (.Resource|index("*"))) else false end) end)
|
33
|
+
end;
|
34
|
+
END
|
35
|
+
|
36
|
+
warning jq: wildcard_resource_filter +
|
37
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|wildcard_resource)]|map(.LogicalResourceId)",
|
38
|
+
message: 'IAM role should not allow * resource on its permissions policy'
|
39
|
+
|
40
|
+
|
41
|
+
warning jq: wildcard_resource_filter +
|
42
|
+
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|wildcard_resource)]|map(.LogicalResourceId)",
|
43
|
+
message: 'IAM policy should not allow * resource'
|
44
|
+
|
45
|
+
warning jq: wildcard_resource_filter +
|
46
|
+
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|wildcard_resource)]|map(.LogicalResourceId)",
|
47
|
+
message: 'IAM managed policy should not allow * resource'
|
48
|
+
|
49
|
+
allow_not_action_filter = <<END
|
50
|
+
def allow_not_action:
|
51
|
+
if .Statement|type == "object"
|
52
|
+
then select(.Statement.Effect == "Allow" and .Statement.NotAction != null)
|
53
|
+
else select(.Statement[]|select(.NotAction != null and .Effect == "Allow"))
|
54
|
+
end;
|
55
|
+
END
|
56
|
+
|
57
|
+
warning jq: allow_not_action_filter +
|
58
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
59
|
+
message: 'IAM role should not allow Allow+NotAction'
|
60
|
+
|
61
|
+
|
62
|
+
warning jq: allow_not_action_filter +
|
63
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
64
|
+
message: 'IAM role should not allow Allow+NotAction'
|
65
|
+
|
66
|
+
|
67
|
+
warning jq: allow_not_action_filter +
|
68
|
+
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
69
|
+
message: 'IAM policy should not allow Allow+NotAction'
|
70
|
+
|
71
|
+
|
72
|
+
warning jq: allow_not_action_filter +
|
73
|
+
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|allow_not_action)]|map(.LogicalResourceId)",
|
74
|
+
message: 'IAM managed policy should not allow Allow+NotAction'
|
75
|
+
|
76
|
+
|
77
|
+
allow_not_resource_filter = <<END
|
78
|
+
def allow_not_resource:
|
79
|
+
if .Statement|type == "object"
|
80
|
+
then select(.Statement.Effect == "Allow" and .Statement.NotResource != null)
|
81
|
+
else select(.Statement[]|select(.NotResource != null and .Effect == "Allow"))
|
82
|
+
end;
|
83
|
+
END
|
84
|
+
|
85
|
+
warning jq: allow_not_resource_filter +
|
86
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.Policies !=null)|select(.Properties.Policies[].PolicyDocument|allow_not_resource)]|map(.LogicalResourceId)",
|
87
|
+
message: 'IAM role should not allow Allow+NotResource'
|
88
|
+
|
89
|
+
|
90
|
+
warning jq: allow_not_resource_filter +
|
91
|
+
"[#{resources_by_type('AWS::IAM::Policy')}|select(.Properties.PolicyDocument|allow_not_resource)]|map(.LogicalResourceId)",
|
92
|
+
message: 'IAM policy should not allow Allow+NotResource'
|
93
|
+
|
94
|
+
|
95
|
+
warning jq: allow_not_resource_filter +
|
96
|
+
"[#{resources_by_type('AWS::IAM::ManagedPolicy')}|select(.Properties.PolicyDocument|allow_not_resource)]|map(.LogicalResourceId)",
|
97
|
+
message: 'IAM managed policy should not allow Allow+NotResource'
|
98
|
+
|
99
|
+
|
100
|
+
allow_not_principal_filter = <<END
|
101
|
+
def allow_not_principal:
|
102
|
+
if .Statement|type == "object"
|
103
|
+
then select(.Statement.Effect == "Allow" and .Statement.NotPrincipal != null)
|
104
|
+
else select(.Statement[]|select(.NotPrincipal != null and .Effect == "Allow"))
|
105
|
+
end;
|
106
|
+
END
|
107
|
+
|
108
|
+
violation jq: allow_not_principal_filter +
|
109
|
+
"[#{resources_by_type('AWS::IAM::Role')}|select(.Properties.AssumeRolePolicyDocument|allow_not_principal)]|map(.LogicalResourceId)",
|
110
|
+
message: 'IAM role should not allow Allow+NotPrincipal in its trust policy'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
violation jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::User")|'\
|
2
|
+
'select(.Properties.Policies|length > 0)]|map(.LogicalResourceId) ',
|
3
|
+
message: 'IAM user should not have any directly attached policies. Should be on group'
|
4
|
+
|
5
|
+
violation jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::Policy")|'\
|
6
|
+
'select(.Properties.Users|length > 0)]|map(.LogicalResourceId) ',
|
7
|
+
message: 'IAM policy should not apply directly to users. Should be on group'
|
8
|
+
|
9
|
+
violation jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::IAM::ManagedPolicy")|'\
|
10
|
+
'select(.Properties.Users|length > 0)]|map(.LogicalResourceId) ',
|
11
|
+
message: 'IAM policy should not apply directly to users. Should be on group'
|
12
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupIngress|type == "object"))|select(.Properties.SecurityGroupIngress.ToPort != .Properties.SecurityGroupIngress.FromPort)]|map(.LogicalResourceId)',
|
2
|
+
message: 'Security Groups found ingress with port range instead of just a single port'
|
3
|
+
|
4
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupIngress|type == "array"))|select([.Properties.SecurityGroupIngress[].ToPort] != [.Properties.SecurityGroupIngress[].FromPort])]|map(.LogicalResourceId)',
|
5
|
+
message: 'Security Groups found ingress with port range instead of just a single port'
|
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'
|
9
|
+
|
10
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroup" and (.Properties.SecurityGroupEgress|type == "array"))|select([.Properties.SecurityGroupEgress[].ToPort] != [.Properties.SecurityGroupEgress[].FromPort])]|map(.LogicalResourceId)',
|
11
|
+
message: 'Security Groups found egress with port range instead of just a single port'
|
12
|
+
|
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
|
+
warning jq: '[.Resources|with_entries(.value.LogicalResourceId = .key)[] | select(.Type == "AWS::EC2::SecurityGroupEgress")|select(.Properties.ToPort != .Properties.FromPort)]|map(.LogicalResourceId)',
|
17
|
+
message: 'Security Groups found egress with port range instead of just a single port'
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'security_group_parser'
|
3
|
+
require_relative 'iam_user_parser'
|
4
|
+
|
5
|
+
# consider a canonical form for template too...
|
6
|
+
# always transform optional things into more general forms....
|
7
|
+
# although referencing violations becomes tricky then (a la c preprocessor)
|
8
|
+
|
9
|
+
class CfnModel
|
10
|
+
def initialize
|
11
|
+
@parser_registry = {
|
12
|
+
'AWS::EC2::SecurityGroup' => SecurityGroupParser,
|
13
|
+
'AWS::EC2::SecurityGroupIngress' => SecurityGroupXgressParser,
|
14
|
+
'AWS::EC2::SecurityGroupEgress' => SecurityGroupXgressParser,
|
15
|
+
'AWS::IAM::User' => IamUserParser,
|
16
|
+
'AWS::IAM::UserToGroupAddition' => IamUserToGroupAdditionParser
|
17
|
+
}
|
18
|
+
@dangling_ingress_or_egress_rules = []
|
19
|
+
@dangler = Object.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse(cfn_json_string)
|
23
|
+
@json_hash = JSON.load cfn_json_string
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def security_groups
|
28
|
+
fail 'must call parse first' unless @json_hash
|
29
|
+
security_groups_hash = resources_by_type('AWS::EC2::SecurityGroup')
|
30
|
+
wire_ingress_rules_to_security_groups(security_groups_hash)
|
31
|
+
wire_egress_rules_to_security_groups(security_groups_hash)
|
32
|
+
security_groups_hash.values
|
33
|
+
end
|
34
|
+
|
35
|
+
def dangling_ingress_or_egress_rules
|
36
|
+
fail 'must call parse first' unless @json_hash
|
37
|
+
@dangling_ingress_or_egress_rules
|
38
|
+
end
|
39
|
+
|
40
|
+
def iam_users
|
41
|
+
fail 'must call parse first' unless @json_hash
|
42
|
+
iam_users_hash = resources_by_type('AWS::IAM::User')
|
43
|
+
wire_user_to_group_additions_to_users(iam_users_hash)
|
44
|
+
iam_users_hash.values
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
#
|
49
|
+
# def is_get_att(object)
|
50
|
+
# not object['Fn::GetAtt'].nil?
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# def is_reference(object)
|
54
|
+
# not object['Ref'].nil?
|
55
|
+
# end
|
56
|
+
|
57
|
+
def resolve_user_logical_resource_id(user)
|
58
|
+
if not user['Ref'].nil?
|
59
|
+
user['Ref']
|
60
|
+
elsif not user['Fn::GetAtt'].nil?
|
61
|
+
fail 'Arn not legal for user to group addition'
|
62
|
+
else
|
63
|
+
@dangler
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def resolve_group_id(group_id)
|
68
|
+
if not group_id['Ref'].nil?
|
69
|
+
group_id['Ref']
|
70
|
+
elsif not group_id['Fn::GetAtt'].nil?
|
71
|
+
fail 'GroupId only legal att on security group resource' unless group_id['Fn::GetAtt'][1] == 'GroupId'
|
72
|
+
group_id['Fn::GetAtt'][0]
|
73
|
+
else
|
74
|
+
@dangling_ingress_or_egress_rules << group_id
|
75
|
+
@dangler
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def wire_user_to_group_additions_to_users(iam_users_hash)
|
80
|
+
resources_by_type('AWS::IAM::UserToGroupAddition').each do |resource_name, user_to_group_addition|
|
81
|
+
user_to_group_addition['Users'].each do |user|
|
82
|
+
unless resolve_user_logical_resource_id(user) == @dangler
|
83
|
+
iam_users_hash[resolve_user_logical_resource_id(user)].add_group user_to_group_addition['GroupName']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
iam_users_hash
|
88
|
+
end
|
89
|
+
|
90
|
+
def wire_ingress_rules_to_security_groups(security_groups_hash)
|
91
|
+
resources_by_type('AWS::EC2::SecurityGroupIngress').each do |resource_name, ingress_rule|
|
92
|
+
if not ingress_rule['GroupId'].nil?
|
93
|
+
group_id = resolve_group_id(ingress_rule['GroupId'])
|
94
|
+
|
95
|
+
unless group_id == @dangler
|
96
|
+
security_groups_hash[group_id].add_ingress_rule ingress_rule
|
97
|
+
end
|
98
|
+
else
|
99
|
+
fail "GroupId must be set: #{ingress_rule}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
security_groups_hash
|
103
|
+
end
|
104
|
+
|
105
|
+
def wire_egress_rules_to_security_groups(security_groups_hash)
|
106
|
+
resources_by_type('AWS::EC2::SecurityGroupEgress').each do |resource_name, egress_rule|
|
107
|
+
if not egress_rule['GroupId'].nil?
|
108
|
+
group_id = resolve_group_id(egress_rule['GroupId'])
|
109
|
+
|
110
|
+
unless group_id == @dangler
|
111
|
+
security_groups_hash[group_id].add_egress_rule egress_rule
|
112
|
+
end
|
113
|
+
else
|
114
|
+
fail "GroupId must be set: #{egress_rule}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
security_groups_hash
|
118
|
+
end
|
119
|
+
|
120
|
+
def resources
|
121
|
+
@json_hash['Resources']
|
122
|
+
end
|
123
|
+
|
124
|
+
def resources_by_type(resource_type)
|
125
|
+
resources_map = {}
|
126
|
+
resources.each do |resource_name, resource|
|
127
|
+
if resource['Type'] == resource_type
|
128
|
+
resource_parser = @parser_registry[resource_type].new
|
129
|
+
resources_map[resource_name] = resource_parser.parse(resource_name, resource)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
resources_map
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class SecurityGroup
|
137
|
+
attr_accessor :group_description, :vpc_id, :logical_resource_id
|
138
|
+
attr_reader :ingress_rules, :egress_rules
|
139
|
+
|
140
|
+
def initialize
|
141
|
+
@ingress_rules = []
|
142
|
+
@egress_rules = []
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_ingress_rule(ingress_rule)
|
146
|
+
@ingress_rules << ingress_rule
|
147
|
+
end
|
148
|
+
|
149
|
+
def add_egress_rule(egress_rule)
|
150
|
+
@egress_rules << egress_rule
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s
|
154
|
+
<<-END
|
155
|
+
{
|
156
|
+
logical_resource_id: #{@logical_resource_id}
|
157
|
+
group_description: #{@group_description}
|
158
|
+
vpc_id: #{@vpc_id}
|
159
|
+
ingress_rules: #{@ingress_rules}
|
160
|
+
egress_rules: #{@egress_rules}
|
161
|
+
}
|
162
|
+
END
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class IamUser
|
167
|
+
attr_accessor :logical_resource_id
|
168
|
+
attr_reader :groups
|
169
|
+
|
170
|
+
def initialize
|
171
|
+
@groups = []
|
172
|
+
end
|
173
|
+
|
174
|
+
def add_group(group)
|
175
|
+
@groups << group
|
176
|
+
end
|
177
|
+
|
178
|
+
def to_s
|
179
|
+
<<-END
|
180
|
+
{
|
181
|
+
logical_resource_id: #{@logical_resource_id}
|
182
|
+
groups: #{@groups}
|
183
|
+
}
|
184
|
+
END
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'cfn_model'
|
2
|
+
|
3
|
+
|
4
|
+
class IamUserParser
|
5
|
+
|
6
|
+
def parse(resource_name, resource_json)
|
7
|
+
properties = resource_json['Properties']
|
8
|
+
iam_user = IamUser.new
|
9
|
+
|
10
|
+
iam_user.logical_resource_id = resource_name
|
11
|
+
|
12
|
+
unless properties.nil?
|
13
|
+
unless properties['Groups'].nil?
|
14
|
+
properties['Groups'].each do |group|
|
15
|
+
iam_user.add_group group
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
iam_user
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class IamUserToGroupAdditionParser
|
25
|
+
|
26
|
+
def parse(resource_name, resource_json)
|
27
|
+
user_to_group_addition = {}
|
28
|
+
user_to_group_addition['logical_resource_id'] = resource_name
|
29
|
+
resource_json['Properties'].each_pair do |key, value|
|
30
|
+
user_to_group_addition[key] = value
|
31
|
+
end
|
32
|
+
user_to_group_addition
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'cfn_model'
|
2
|
+
|
3
|
+
class SecurityGroupParser
|
4
|
+
|
5
|
+
# precondition: properties are actually there... other validator takes care
|
6
|
+
def parse(resource_name, resource_json)
|
7
|
+
properties = resource_json['Properties']
|
8
|
+
security_group = SecurityGroup.new
|
9
|
+
|
10
|
+
parse_ingress_rules(security_group, properties)
|
11
|
+
|
12
|
+
parse_egress_rules(security_group, properties)
|
13
|
+
|
14
|
+
security_group.vpc_id = properties['VpcId']
|
15
|
+
security_group.group_description = properties['GroupDescription']
|
16
|
+
security_group.logical_resource_id = resource_name
|
17
|
+
|
18
|
+
security_group
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_ingress_rules(security_group, properties)
|
24
|
+
unless properties['SecurityGroupIngress'].nil?
|
25
|
+
if properties['SecurityGroupIngress'].is_a? Array
|
26
|
+
properties['SecurityGroupIngress'].each do |ingress_json|
|
27
|
+
security_group.add_ingress_rule ingress_json
|
28
|
+
end
|
29
|
+
elsif properties['SecurityGroupIngress'].is_a? Hash
|
30
|
+
security_group.add_ingress_rule properties['SecurityGroupIngress']
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_egress_rules(security_group, properties)
|
36
|
+
unless properties['SecurityGroupEgress'].nil?
|
37
|
+
if properties['SecurityGroupEgress'].is_a? Array
|
38
|
+
properties['SecurityGroupEgress'].each do |egress_json|
|
39
|
+
security_group.add_egress_rule egress_json
|
40
|
+
end
|
41
|
+
elsif properties['SecurityGroupEgress'].is_a? Hash
|
42
|
+
security_group.add_egress_rule properties['SecurityGroupEgress']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class SecurityGroupXgressParser
|
50
|
+
|
51
|
+
def parse(resource_name, resource_json)
|
52
|
+
xgress = {}
|
53
|
+
xgress['logical_resource_id'] = resource_name
|
54
|
+
resource_json['Properties'].each_pair do |key, value|
|
55
|
+
xgress[key] = value
|
56
|
+
end
|
57
|
+
xgress
|
58
|
+
end
|
59
|
+
end
|
data/lib/rule.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
module Rule
|
4
|
+
attr_accessor :input_json_path, :failure_count
|
5
|
+
|
6
|
+
def resources
|
7
|
+
'.Resources|with_entries(.value.LogicalResourceId = .key)[]'
|
8
|
+
end
|
9
|
+
|
10
|
+
def resources_by_type(resource)
|
11
|
+
"#{resources}| select(.Type == \"#{resource}\")"
|
12
|
+
end
|
13
|
+
|
14
|
+
def warning(jq:, message:)
|
15
|
+
Logging.logger['log'].debug jq
|
16
|
+
|
17
|
+
stdout = jq_command(@input_json_path, jq)
|
18
|
+
result = $?.exitstatus
|
19
|
+
scrape_jq_output_for_error(stdout)
|
20
|
+
|
21
|
+
resource_ids = parse_logical_resource_ids(stdout)
|
22
|
+
new_warnings = resource_ids.size
|
23
|
+
if result == 0 and new_warnings > 0
|
24
|
+
@warning_count ||= 0
|
25
|
+
@warning_count += new_warnings
|
26
|
+
|
27
|
+
message(message_type: 'warning',
|
28
|
+
message: message,
|
29
|
+
logical_resource_ids: resource_ids)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def raw_fatal_assertion(jq:, message:)
|
34
|
+
failing_rule(jq_expression: jq,
|
35
|
+
fail_if_found: false,
|
36
|
+
fatal: true,
|
37
|
+
message: message,
|
38
|
+
message_type: 'fatal assertion',
|
39
|
+
raw: true)
|
40
|
+
end
|
41
|
+
|
42
|
+
def fatal_assertion(jq:, message:)
|
43
|
+
failing_rule(jq_expression: jq,
|
44
|
+
fail_if_found: false,
|
45
|
+
fatal: true,
|
46
|
+
message: message,
|
47
|
+
message_type: 'fatal assertion')
|
48
|
+
end
|
49
|
+
|
50
|
+
def raw_fatal_violation(jq:, message:)
|
51
|
+
failing_rule(jq_expression: jq,
|
52
|
+
fail_if_found: true,
|
53
|
+
fatal: true,
|
54
|
+
message: message,
|
55
|
+
message_type: 'fatal violation',
|
56
|
+
raw: true)
|
57
|
+
end
|
58
|
+
|
59
|
+
def fatal_violation(jq:, message:)
|
60
|
+
failing_rule(jq_expression: jq,
|
61
|
+
fail_if_found: true,
|
62
|
+
fatal: true,
|
63
|
+
message: message,
|
64
|
+
message_type: 'fatal violation')
|
65
|
+
end
|
66
|
+
|
67
|
+
def violation(jq:, message:)
|
68
|
+
failing_rule(jq_expression: jq,
|
69
|
+
fail_if_found: true,
|
70
|
+
message: message,
|
71
|
+
message_type: 'violation')
|
72
|
+
end
|
73
|
+
|
74
|
+
def assertion(jq:, message:)
|
75
|
+
failing_rule(jq_expression: jq,
|
76
|
+
fail_if_found: false,
|
77
|
+
message: message,
|
78
|
+
message_type: 'assertion')
|
79
|
+
end
|
80
|
+
|
81
|
+
def message(message_type:,
|
82
|
+
message:,
|
83
|
+
logical_resource_ids: nil,
|
84
|
+
violating_code: nil)
|
85
|
+
|
86
|
+
if logical_resource_ids == []
|
87
|
+
logical_resource_ids = nil
|
88
|
+
end
|
89
|
+
|
90
|
+
(1..60).each { print '-' }
|
91
|
+
puts
|
92
|
+
puts "| #{message_type.upcase}"
|
93
|
+
puts '|'
|
94
|
+
puts "| Resources: #{logical_resource_ids}" unless logical_resource_ids.nil?
|
95
|
+
puts '|' unless logical_resource_ids.nil?
|
96
|
+
puts "| #{message}"
|
97
|
+
|
98
|
+
unless violating_code.nil?
|
99
|
+
puts '|'
|
100
|
+
puts indent_multiline_string_with_prefix('|', violating_code.to_s)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def parse_logical_resource_ids(stdout)
|
107
|
+
JSON.load(stdout)
|
108
|
+
end
|
109
|
+
|
110
|
+
def scrape_jq_output_for_error(stdout)
|
111
|
+
fail 'json rule is likely not complete' if stdout.match /jq: error/
|
112
|
+
end
|
113
|
+
|
114
|
+
def indent_multiline_string_with_prefix(prefix, multiline_string)
|
115
|
+
prefix + ' ' + multiline_string.gsub(/\n/, "\n#{prefix} ")
|
116
|
+
end
|
117
|
+
|
118
|
+
def failing_rule(jq_expression:,
|
119
|
+
fail_if_found:,
|
120
|
+
message:,
|
121
|
+
message_type:,
|
122
|
+
fatal: false,
|
123
|
+
raw: false)
|
124
|
+
Logging.logger['log'].debug jq_expression
|
125
|
+
|
126
|
+
stdout = jq_command(@input_json_path, jq_expression)
|
127
|
+
result = $?.exitstatus
|
128
|
+
scrape_jq_output_for_error(stdout)
|
129
|
+
if (fail_if_found and result == 0) or
|
130
|
+
(not fail_if_found and result != 0)
|
131
|
+
@violation_count ||= 0
|
132
|
+
|
133
|
+
if raw
|
134
|
+
@violation_count += 1
|
135
|
+
|
136
|
+
message(message_type: message_type,
|
137
|
+
message: message,
|
138
|
+
violating_code: stdout)
|
139
|
+
|
140
|
+
if fatal
|
141
|
+
exit 1
|
142
|
+
end
|
143
|
+
else
|
144
|
+
resource_ids = parse_logical_resource_ids(stdout)
|
145
|
+
@violation_count += resource_ids.size
|
146
|
+
|
147
|
+
if resource_ids.size > 0
|
148
|
+
message(message_type: message_type,
|
149
|
+
message: message,
|
150
|
+
logical_resource_ids: resource_ids)
|
151
|
+
|
152
|
+
if fatal
|
153
|
+
exit 1
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def jq_command(input_json_path, jq_expression)
|
161
|
+
command = "cat #{input_json_path} | jq '#{jq_expression}' -e"
|
162
|
+
|
163
|
+
Logging.logger['log'].debug command
|
164
|
+
|
165
|
+
`#{command}`
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cfn-nag
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- someguy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: logging
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: trollop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.1.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.1.2
|
41
|
+
description: Auditing tool for Cloudformation templates
|
42
|
+
email:
|
43
|
+
executables:
|
44
|
+
- cfn_nag
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- bin/cfn_nag
|
49
|
+
- lib/cfn_nag.rb
|
50
|
+
- lib/custom_rules/security_group_missing_egress.rb
|
51
|
+
- lib/custom_rules/user_missing_group.rb
|
52
|
+
- lib/json_rules/basic_rules.rb
|
53
|
+
- lib/json_rules/cidr_rules.rb
|
54
|
+
- lib/json_rules/iam_policy_rules.rb
|
55
|
+
- lib/json_rules/iam_user_rules.rb
|
56
|
+
- lib/json_rules/port_rules.rb
|
57
|
+
- lib/model/cfn_model.rb
|
58
|
+
- lib/model/iam_user_parser.rb
|
59
|
+
- lib/model/security_group_parser.rb
|
60
|
+
- lib/rule.rb
|
61
|
+
homepage:
|
62
|
+
licenses: []
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 2.5.1
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: cfn-nag
|
85
|
+
test_files: []
|