cfn-nag 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []