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.
@@ -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
@@ -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])
@@ -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
@@ -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: []