cfn-nag 0.0.16 → 0.0.17

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