cfn-nag 0.5.61 → 0.6.0

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
  SHA256:
3
- metadata.gz: 9730b5f935f661789abf3aa45239a0f7715c35c95842644aeec25f98a7c3c6ce
4
- data.tar.gz: 4a2f9453cc6d9074f4e3ea19b78514fddcd5fc5d8d9269babe8187bebd549482
3
+ metadata.gz: ce61954c2ef415db38dfd9173ab93fb92145a7a2cf5778901bad880d3aabc762
4
+ data.tar.gz: 8296a1bea671a836e464db5f9a4573d7b64ba87be90e841ce2ef5e823b61b49a
5
5
  SHA512:
6
- metadata.gz: 6dc61c128936866f2e0bf34e7e442e4a71366fa0f9b9c41fe972142aafffd51f7155e3aec488b5742ae5b019b484245a1fa7c6555979de3b709e68ab30081553
7
- data.tar.gz: 54431e5bc771b2ca296dfb94ddcb1202da16d7ced5f1c88c28a00bd2d9d7f3b36600edb6449f210244d6c55a22addfa6a293a6a0dc9ad2c5318eb047d74e03e1
6
+ metadata.gz: 4d07a9749c20b7083b9f01b479e20ffdcc327be635d9285e3f7175f8be39c87d7d66fc5fd5d1a7d728d399d9d5fc96a92d7ea42ac90014b79528bcdf2a4786e7
7
+ data.tar.gz: 227ef815606ef8eb763b1523de73ba0f4a0e8c6791afd809699057e41f37694ead17bd1714725838328516c45a369118852e5fadb76e334c75dde009ba459ed2
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'cfn-nag/iam_complexity_metric/spcm'
5
+ require 'cfn-nag/iam_complexity_metric/html_results_renderer'
6
+ require 'optimist'
7
+ require 'json'
8
+
9
+ # rubocop:disable Metrics/BlockLength
10
+ opts = Optimist.options do
11
+ opt :parameter_values_path,
12
+ 'Path to a JSON file to pull Parameter values from',
13
+ type: :string,
14
+ required: false,
15
+ default: nil
16
+ opt :condition_values_path,
17
+ 'Path to a JSON file to pull Condition values from',
18
+ type: :string,
19
+ required: false,
20
+ default: nil
21
+ opt :input_path,
22
+ 'CloudFormation template to measure SPCM on or directory of templates.',
23
+ type: :string,
24
+ required: true
25
+ opt :template_pattern,
26
+ 'Within the --input-path, match files to scan against this regular expression',
27
+ type: :string,
28
+ required: false,
29
+ default: '..*\.json|..*\.yaml|..*\.yml|..*\.template'
30
+ opt :ignore_templates_without_iam,
31
+ 'Within the --input-path ignore files without IAM role/policy resources',
32
+ type: :boolean,
33
+ required: false,
34
+ default: true
35
+ opt :output_format,
36
+ 'Format of results: [json, html]',
37
+ type: :string,
38
+ default: 'json'
39
+ end
40
+ # rubocop:enable Metrics/BlockLength
41
+
42
+ def read_conditionally(path)
43
+ unless path.nil?
44
+ IO.read(path)
45
+ end
46
+ end
47
+
48
+ parameter_values_string = read_conditionally(opts[:parameter_values_path])
49
+
50
+ condition_values_string = read_conditionally(opts[:condition_values_path])
51
+
52
+ metrics = SPCM.new.aggregate_metrics(
53
+ input_path: opts[:input_path],
54
+ parameter_values_path: parameter_values_string,
55
+ condition_values_path: condition_values_string,
56
+ template_pattern: opts[:template_pattern]
57
+ )
58
+
59
+ if opts[:ignore_templates_without_iam]
60
+ metrics = metrics.select do |metric|
61
+ metric[:file_results]['AWS::IAM::Role'] != {} || metric[:file_results]['AWS::IAM::Policy'] != {}
62
+ end
63
+ end
64
+
65
+ if opts[:output_format] == 'json'
66
+ puts JSON.generate(metrics)
67
+ else
68
+ puts HtmlRenderer.new.render(results: metrics)
69
+ end
@@ -87,6 +87,7 @@ class CfnNag
87
87
  parameter_values_string,
88
88
  true,
89
89
  condition_values_string
90
+ CustomRuleLoader.rule_arguments = @config.rule_arguments
90
91
  violations += @config.custom_rule_loader.execute_custom_rules(
91
92
  cfn_model,
92
93
  @config.custom_rule_loader.rule_definitions
@@ -9,7 +9,8 @@ class CfnNagConfig
9
9
  print_suppression: false,
10
10
  isolate_custom_rule_exceptions: false,
11
11
  fail_on_warnings: false,
12
- rule_repository_definitions: [])
12
+ rule_repository_definitions: [],
13
+ rule_arguments: {})
13
14
  @rule_directory = rule_directory
14
15
  @custom_rule_loader = CustomRuleLoader.new(
15
16
  rule_directory: rule_directory,
@@ -22,9 +23,11 @@ class CfnNagConfig
22
23
  @blacklist_definition = blacklist_definition
23
24
  @fail_on_warnings = fail_on_warnings
24
25
  @rule_repositories = rule_repositories
26
+ @rule_arguments = rule_arguments
25
27
  end
26
28
  # rubocop:enable Metrics/ParameterLists
27
29
 
30
+ attr_reader :rule_arguments
28
31
  attr_reader :rule_directory
29
32
  attr_reader :custom_rule_loader
30
33
  attr_reader :profile_definition
@@ -11,6 +11,7 @@ class CfnNagExecutor
11
11
  @parameter_values_string = nil
12
12
  @condition_values_string = nil
13
13
  @rule_repository_definitions = []
14
+ @rule_arguments_string = nil
14
15
  end
15
16
 
16
17
  def scan(options_type:)
@@ -77,6 +78,12 @@ class CfnNagExecutor
77
78
  Optimist.die(:output_format,
78
79
  'Must be colortxt, txt, or json')
79
80
  end
81
+
82
+ opts[:rule_arguments]&.each do |rule_argument|
83
+ unless rule_argument.include?(':')
84
+ Optimist.die(:rule_arguments, 'Must be of form name:value')
85
+ end
86
+ end
80
87
  end
81
88
 
82
89
  def execute_io_options(opts)
@@ -88,6 +95,8 @@ class CfnNagExecutor
88
95
 
89
96
  @condition_values_string = read_conditionally(opts[:condition_values_path])
90
97
 
98
+ @rule_arguments_string = read_conditionally(opts[:rule_arguments_path])
99
+
91
100
  opts[:rule_repository]&.each do |rule_repository|
92
101
  @rule_repository_definitions << IO.read(rule_repository)
93
102
  end
@@ -99,6 +108,17 @@ class CfnNagExecutor
99
108
  end
100
109
  end
101
110
 
111
+ def merge_rule_arguments(opts)
112
+ rule_arguments = {}
113
+ rule_arguments = JSON.parse(@rule_arguments_string) if @rule_arguments_string
114
+ opts[:rule_arguments]&.each do |rule_argument|
115
+ name = rule_argument.split(':')[0]
116
+ value = rule_argument.split(':')[1]
117
+ rule_arguments[name] = value
118
+ end
119
+ rule_arguments
120
+ end
121
+
102
122
  def cfn_nag_config(opts)
103
123
  CfnNagConfig.new(
104
124
  profile_definition: @profile_definition,
@@ -108,7 +128,8 @@ class CfnNagExecutor
108
128
  print_suppression: opts[:print_suppression],
109
129
  isolate_custom_rule_exceptions: opts[:isolate_custom_rule_exceptions],
110
130
  fail_on_warnings: opts[:fail_on_warnings],
111
- rule_repository_definitions: @rule_repository_definitions
131
+ rule_repository_definitions: @rule_repository_definitions,
132
+ rule_arguments: merge_rule_arguments(opts)
112
133
  )
113
134
  end
114
135
 
@@ -91,6 +91,15 @@ class Options
91
91
  'Path(s) to a rule repository to include in rule discovery',
92
92
  type: :strings,
93
93
  required: false
94
+ opt :rule_arguments,
95
+ 'Rule arguments to inject into interested rules',
96
+ type: :strings,
97
+ required: false
98
+ opt :rule_arguments_path,
99
+ 'Path to a rule arguments to inject into interested rules',
100
+ type: :string,
101
+ required: false,
102
+ default: nil
94
103
  end
95
104
  end
96
105
 
@@ -175,6 +184,15 @@ class Options
175
184
  'Path(s)s to rule repository to include in rule discovery',
176
185
  type: :strings,
177
186
  required: false
187
+ opt :rule_arguments,
188
+ 'Rule arguments to inject into interested rules',
189
+ type: :strings,
190
+ required: false
191
+ opt :rule_arguments_path,
192
+ 'Path to a rule arguments to inject into interested rules',
193
+ type: :string,
194
+ required: false,
195
+ default: nil
178
196
  end
179
197
  end
180
198
  # rubocop:enable Metrics/BlockLength
@@ -7,12 +7,22 @@ require_relative 'rule_repos/file_based_rule_repo'
7
7
  require_relative 'rule_repos/gem_based_rule_repo'
8
8
  require_relative 'rule_repos/s3_based_rule_repo'
9
9
  require_relative 'rule_repository_loader'
10
+ require_relative 'metadata'
10
11
 
11
12
  ##
12
13
  # This object can discover the internal and custom user-provided rules and
13
14
  # apply these rules to a CfnModel object
14
15
  #
15
16
  class CustomRuleLoader
17
+ include Metadata
18
+
19
+ # k,v for injection into rules that can respond to k
20
+ @rule_arguments = {}
21
+
22
+ class << self
23
+ attr_accessor :rule_arguments
24
+ end
25
+
16
26
  def initialize(rule_directory: nil,
17
27
  allow_suppression: true,
18
28
  print_suppression: false,
@@ -58,6 +68,14 @@ class CustomRuleLoader
58
68
 
59
69
  private
60
70
 
71
+ def inject_rule_arguments_into_rule(rule)
72
+ self.class.rule_arguments.each do |rule_argument_name, rule_argument_value|
73
+ if rule.respond_to?("#{rule_argument_name}=".to_sym)
74
+ rule.send "#{rule_argument_name}=".to_sym, rule_argument_value
75
+ end
76
+ end
77
+ end
78
+
61
79
  # rubocop:disable Style/RedundantBegin
62
80
  def filter_rule_classes(cfn_model, violations, rules_registry)
63
81
  rules_registry.rule_classes.each do |rule_class|
@@ -65,9 +83,12 @@ class CustomRuleLoader
65
83
  filtered_cfn_model = cfn_model_with_suppressed_resources_removed(
66
84
  cfn_model: cfn_model,
67
85
  rule_id: rule_class.new.rule_id,
68
- allow_suppression: @allow_suppression
86
+ allow_suppression: @allow_suppression,
87
+ print_suppression: @print_suppression
69
88
  )
70
- audit_result = rule_class.new.audit(filtered_cfn_model)
89
+ rule = rule_class.new
90
+ inject_rule_arguments_into_rule(rule)
91
+ audit_result = rule.audit(filtered_cfn_model)
71
92
  violations << audit_result unless audit_result.nil?
72
93
  rescue ScriptError, StandardError => rule_error
73
94
  raise rule_error unless @isolate_custom_rule_exceptions
@@ -77,74 +98,4 @@ class CustomRuleLoader
77
98
  end
78
99
  end
79
100
  # rubocop:enable Style/RedundantBegin
80
-
81
- def rules_to_suppress(resource)
82
- if resource.metadata &&
83
- resource.metadata['cfn_nag'] &&
84
- resource.metadata['cfn_nag']['rules_to_suppress']
85
-
86
- resource.metadata['cfn_nag']['rules_to_suppress']
87
- end
88
- end
89
-
90
- def collect_mangled_metadata(cfn_model)
91
- mangled_metadatas = []
92
- cfn_model.resources.each do |logical_resource_id, resource|
93
- resource_rules_to_suppress = rules_to_suppress resource
94
- next if resource_rules_to_suppress.nil?
95
-
96
- mangled_rules = resource_rules_to_suppress.select do |rule_to_suppress|
97
- rule_to_suppress['id'].nil?
98
- end
99
- unless mangled_rules.empty?
100
- mangled_metadatas << [logical_resource_id, mangled_rules]
101
- end
102
- end
103
- mangled_metadatas
104
- end
105
-
106
- # XXX given mangled_metadatas is never used or returned,
107
- # STDERR emit can be moved to unless block
108
- def validate_cfn_nag_metadata(cfn_model)
109
- mangled_metadatas = collect_mangled_metadata(cfn_model)
110
- mangled_metadatas.each do |mangled_metadata|
111
- logical_resource_id = mangled_metadata.first
112
- mangled_rules = mangled_metadata[1]
113
-
114
- STDERR.puts "#{logical_resource_id} has missing cfn_nag suppression " \
115
- "rule id: #{mangled_rules}"
116
- end
117
- end
118
-
119
- def suppress_resource?(rules_to_suppress, rule_id, logical_resource_id)
120
- found_suppression_rule = rules_to_suppress.find do |rule_to_suppress|
121
- next if rule_to_suppress['id'].nil?
122
-
123
- rule_to_suppress['id'] == rule_id
124
- end
125
- if found_suppression_rule && @print_suppression
126
- message = "Suppressing #{rule_id} on #{logical_resource_id} for " \
127
- "reason: #{found_suppression_rule['reason']}"
128
- STDERR.puts message
129
- end
130
- !found_suppression_rule.nil?
131
- end
132
-
133
- def cfn_model_with_suppressed_resources_removed(cfn_model:,
134
- rule_id:,
135
- allow_suppression:)
136
- return cfn_model unless allow_suppression
137
-
138
- cfn_model = cfn_model.copy
139
-
140
- cfn_model.resources.delete_if do |logical_resource_id, resource|
141
- rules_to_suppress = rules_to_suppress resource
142
- if rules_to_suppress.nil?
143
- false
144
- else
145
- suppress_resource?(rules_to_suppress, rule_id, logical_resource_id)
146
- end
147
- end
148
- cfn_model
149
- end
150
101
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cfn-nag/violation'
4
+ require 'cfn-nag/iam_complexity_metric/spcm'
5
+ require_relative 'base'
6
+
7
+ class SPCMRule < BaseRule
8
+ attr_accessor :spcm_threshold
9
+ DEFAULT_THRESHOLD = 25
10
+
11
+ def rule_text
12
+ "SPCM for IAM policy document is higher than #{spcm_threshold || DEFAULT_THRESHOLD}"
13
+ end
14
+
15
+ def rule_type
16
+ Violation::WARNING
17
+ end
18
+
19
+ def rule_id
20
+ 'W76'
21
+ end
22
+
23
+ def audit_impl(cfn_model)
24
+ logical_resource_ids = []
25
+ begin
26
+ policy_documents = SPCM.new.metric_impl(cfn_model)
27
+ rescue StandardError => catch_all_exception
28
+ puts "Experimental SPCM rule is failing. Please report #{catch_all_exception} with the violating template"
29
+ policy_documents = {}
30
+ end
31
+
32
+ threshold = spcm_threshold.nil? ? DEFAULT_THRESHOLD : spcm_threshold.to_i
33
+ logical_resource_ids += violating_policy_resources(policy_documents, threshold)
34
+ logical_resource_ids += violating_role_resources(policy_documents, threshold)
35
+
36
+ logical_resource_ids
37
+ end
38
+
39
+ private
40
+
41
+ def violating_role_resources(policy_documents, threshold)
42
+ logical_resource_ids = []
43
+
44
+ # unfortunately the line numbers will break if we don't return
45
+ # the logical resource id - so there isn't a good way to communicate
46
+ # the specific policy within the role that is offending
47
+ policy_documents['AWS::IAM::Role'].each do |logical_resource_id, policies|
48
+ policies.each do |_, metric|
49
+ if metric >= threshold
50
+ logical_resource_ids << logical_resource_id
51
+ end
52
+ end
53
+ end
54
+ logical_resource_ids
55
+ end
56
+
57
+ def violating_policy_resources(policy_documents, threshold)
58
+ logical_resource_ids = []
59
+ policy_documents['AWS::IAM::Policy'].each do |logical_resource_id, metric|
60
+ if metric >= threshold
61
+ logical_resource_ids << logical_resource_id
62
+ end
63
+ end
64
+ logical_resource_ids
65
+ end
66
+ end
@@ -5,15 +5,16 @@ require_relative 'boolean_base_rule'
5
5
 
6
6
  class SecretsManagerSecretKmsKeyIdRule < BooleanBaseRule
7
7
  def rule_text
8
- 'Secrets Manager Secret should explicitly specify KmsKeyId'
8
+ 'Secrets Manager Secret should explicitly specify KmsKeyId.' \
9
+ ' Besides control of the key this will allow the secret to be shared cross-account'
9
10
  end
10
11
 
11
12
  def rule_type
12
- Violation::FAILING_VIOLATION
13
+ Violation::WARNING
13
14
  end
14
15
 
15
16
  def rule_id
16
- 'F81'
17
+ 'W77'
17
18
  end
18
19
 
19
20
  def resource_type
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'weights'
4
+ require 'set'
5
+
6
+ class ConditionMetric
7
+ include Weights
8
+
9
+ # rubocop:disable Metrics/AbcSize
10
+ def metric(statement)
11
+ return 0 if statement.condition.nil?
12
+
13
+ aggregate = 0
14
+ aggregate += statement.condition.size * weights[:Condition]
15
+ aggregate += confusing_value_operators(statement.condition)
16
+ aggregate += if_exists_operators(statement.condition)
17
+ aggregate += weights[:Null] if null_operator?(statement.condition)
18
+ aggregate += values_with_policy_tags(statement.condition)
19
+ aggregate
20
+ end
21
+ # rubocop:enable Metrics/AbcSize
22
+
23
+ private
24
+
25
+ def values_with_policy_tags(conditions)
26
+ all_values(conditions).reduce(0) do |aggregate, value|
27
+ aggregate + (contains_policy_tag?(value) ? weights[:PolicyVariables] : 0)
28
+ end
29
+ end
30
+
31
+ def contains_policy_tag?(value)
32
+ strip_special_characters(value).match(/.*\$\{.+\}.*/)
33
+ end
34
+
35
+ def strip_special_characters(value)
36
+ special_characters.each do |special_character|
37
+ value = value.gsub("${#{special_character}}", '')
38
+ end
39
+ value
40
+ end
41
+
42
+ def all_values(conditions)
43
+ result = []
44
+ conditions.each do |_, expression|
45
+ expression.each do |_, value|
46
+ if value.is_a? String
47
+ result << value
48
+ elsif value.is_a? Array
49
+ result += value
50
+ end
51
+ end
52
+ end
53
+ result
54
+ end
55
+
56
+ def special_characters
57
+ %w[$ * ?]
58
+ end
59
+
60
+ def null_operator?(conditions)
61
+ conditions.find { |operator, _| operator == 'Null' }
62
+ end
63
+
64
+ def if_exists_operators(conditions)
65
+ conditions.reduce(0) do |aggregate, condition|
66
+ operator = condition[0]
67
+ aggregate + (if_exists_operator?(operator) ? weights[:IfExists] : 0)
68
+ end
69
+ end
70
+
71
+ def if_exists_operator?(operator)
72
+ operator.end_with? 'IfExists'
73
+ end
74
+
75
+ def confusing_value_operators(conditions)
76
+ conditions.reduce(0) do |aggregate, condition|
77
+ operator = condition[0]
78
+ aggregate + (confusing_value_operator?(operator) ? weights[:Condition] : 0)
79
+ end
80
+ end
81
+
82
+ def confusing_value_operator?(operator)
83
+ %w[ForAllValues ForAnyValues].find { |prefix| operator.start_with? prefix }
84
+ end
85
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HtmlRenderer
4
+ def render(results:)
5
+ output = '<html><body><table>'
6
+ results.each do |result|
7
+ output += '<tr><td><table><tr><td>'
8
+ output += result[:filename]
9
+ output += '</td></tr><tr><td>'
10
+ output += render_policy(result)
11
+ output += render_role(result)
12
+ output += '</td></tr></table></td></tr>'
13
+ end
14
+ output += '</table></body></html>'
15
+ output
16
+ end
17
+
18
+ private
19
+
20
+ def render_policy(result)
21
+ output = ''
22
+ if result[:file_results]['AWS::IAM::Policy'] != {}
23
+ output += '<ul>'
24
+ result[:file_results]['AWS::IAM::Policy'].each do |k, v|
25
+ output += "<li>#{k}=#{v}</li>"
26
+ end
27
+ output += '</ul>'
28
+ end
29
+ output
30
+ end
31
+
32
+ def render_role(result)
33
+ output = ''
34
+ if result[:file_results]['AWS::IAM::Role'] != {}
35
+ output += '<ul>'
36
+ result[:file_results]['AWS::IAM::Role'].each do |role_id, policy_map|
37
+ policy_map.each do |policy_name, metric|
38
+ output += "<li>#{role_id}/#{policy_name}=#{metric}</li>"
39
+ end
40
+ end
41
+ output += '</ul>'
42
+ end
43
+ output
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'statement_metric'
4
+
5
+ class PolicyDocumentMetric
6
+ def metric(policy_document)
7
+ policy_document.statements.reduce(0) do |aggregate, statement|
8
+ aggregate + StatementMetric.new.metric(statement)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cfn-nag/template_discovery'
4
+ require 'cfn-model'
5
+ require_relative 'policy_document_metric'
6
+
7
+ class SPCM
8
+ DEFAULT_TEMPLATE_PATTERN = '..*\.json$|..*\.yaml$|..*\.yml$|..*\.template$'
9
+
10
+ def aggregate_metrics(input_path:,
11
+ parameter_values_path: nil,
12
+ condition_values_path: nil,
13
+ template_pattern: DEFAULT_TEMPLATE_PATTERN)
14
+ parameter_values_string = parameter_values_path.nil? ? nil : IO.read(parameter_values_path)
15
+ condition_values_string = condition_values_path.nil? ? nil : IO.read(condition_values_path)
16
+
17
+ templates = TemplateDiscovery.new.discover_templates(input_json_path: input_path,
18
+ template_pattern: template_pattern)
19
+ aggregate_results = []
20
+ templates.each do |template|
21
+ aggregate_results << {
22
+ filename: template,
23
+ file_results: metric(
24
+ cloudformation_string: IO.read(template),
25
+ parameter_values_string: parameter_values_string,
26
+ condition_values_string: condition_values_string
27
+ )
28
+ }
29
+ end
30
+ aggregate_results
31
+ end
32
+
33
+ def metric(cloudformation_string:, parameter_values_string: nil, condition_values_string: nil)
34
+ cfn_model = CfnParser.new.parse cloudformation_string,
35
+ parameter_values_string,
36
+ false,
37
+ condition_values_string
38
+
39
+ metric_impl(cfn_model)
40
+ end
41
+
42
+ def metric_impl(cfn_model)
43
+ policy_documents = {
44
+ 'AWS::IAM::Policy' => {},
45
+ 'AWS::IAM::Role' => {}
46
+ }
47
+
48
+ cfn_model.resources_by_type('AWS::IAM::Policy').each do |policy|
49
+ update_policy_metric(policy_documents, policy)
50
+ end
51
+
52
+ cfn_model.resources_by_type('AWS::IAM::Role').each do |role|
53
+ role.policy_objects.each do |policy|
54
+ update_role_policy_metric(policy_documents, role, policy)
55
+ end
56
+ end
57
+
58
+ policy_documents
59
+ end
60
+
61
+ private
62
+
63
+ def update_policy_metric(policy_documents, policy)
64
+ metric = PolicyDocumentMetric.new.metric(policy.policy_document)
65
+ policy_documents['AWS::IAM::Policy'][policy.logical_resource_id] = metric
66
+ end
67
+
68
+ def update_role_policy_metric(policy_documents, role, policy)
69
+ metric = PolicyDocumentMetric.new.metric(policy.policy_document)
70
+
71
+ if policy_documents['AWS::IAM::Role'][role.logical_resource_id]
72
+ policy_documents['AWS::IAM::Role'][role.logical_resource_id][policy.policy_name.to_s] = metric
73
+ else
74
+ policy_documents['AWS::IAM::Role'][role.logical_resource_id] = {
75
+ policy.policy_name.to_s => metric
76
+ }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'weights'
4
+ require_relative 'condition_metric'
5
+ require 'set'
6
+
7
+ class StatementMetric
8
+ include Weights
9
+
10
+ # rubocop:disable Metrics/AbcSize
11
+ def metric(statement)
12
+ aggregate = weights[:Base_Statement]
13
+
14
+ aggregate += effect_metrics(statement)
15
+ aggregate += inversion_metrics(statement)
16
+ aggregate += extra_service_count(statement) * weights[:Extra_Service]
17
+ aggregate += misaligned_resource_action_count(statement) * weights[:Resource_Action_NotAligned]
18
+ aggregate += mixed_wildcard(statement) * weights[:Mixed_Wildcard]
19
+
20
+ aggregate += ConditionMetric.new.metric(statement) unless statement.condition.nil?
21
+
22
+ aggregate
23
+ end
24
+ # rubocop:enable Metrics/AbcSize
25
+
26
+ private
27
+
28
+ def effect_metrics(statement)
29
+ aggregate = 0
30
+ aggregate += weights[:Deny] if statement.effect == 'Deny'
31
+ aggregate += weights[:Allow] if statement.effect == 'Allow'
32
+ aggregate
33
+ end
34
+
35
+ def inversion_metrics(statement)
36
+ aggregate = 0
37
+ aggregate += weights[:NotAction] unless statement.not_actions.empty?
38
+ aggregate += weights[:NotResource] unless statement.not_resources.empty?
39
+ aggregate
40
+ end
41
+
42
+ def mixed_wildcard(statement)
43
+ count = 0
44
+ count += 1 if action_service_names(statement).include?('*') && action_service_names(statement).size > 1
45
+ count += 1 if resource_service_names(statement).include?('*') && resource_service_names(statement).size > 1
46
+ count
47
+ end
48
+
49
+ def misaligned_resource_action_count(statement)
50
+ return 0 if resource(statement) == ['*'] || action(statement) == ['*']
51
+
52
+ resource_service_names = resource(statement).map { |resource_arn| resource_service_name(resource_arn) }
53
+ action_service_names = action(statement).map { |action| action_service_name(action) }
54
+
55
+ (set_without_wildcard(resource_service_names) - set_without_wildcard(action_service_names)).size
56
+ end
57
+
58
+ # rubocop:disable Naming/AccessorMethodName
59
+ def set_without_wildcard(array)
60
+ Set.new(array).delete('*')
61
+ end
62
+ # rubocop:enable Naming/AccessorMethodName
63
+
64
+ def extra_service_count(statement)
65
+ service_names = Set.new(action_service_names(statement) + resource_service_names(statement)).delete('*')
66
+ [service_names.size - 1, 0].max
67
+ end
68
+
69
+ def action_service_names(statement)
70
+ action(statement).map { |action| action_service_name(action) }
71
+ end
72
+
73
+ def resource_service_names(statement)
74
+ resource(statement).map { |resource_arn| resource_service_name(resource_arn) }
75
+ end
76
+
77
+ def action_service_name(action)
78
+ return '*' if action == '*'
79
+
80
+ return action unless action.is_a?(String)
81
+
82
+ action.split(':')[0]
83
+ end
84
+
85
+ def resource_service_name(resource_arn)
86
+ return '*' if resource_arn == '*'
87
+
88
+ return resource_arn unless resource_arn.is_a?(String)
89
+
90
+ resource_arn.split(':')[2]
91
+ end
92
+
93
+ def action(statement)
94
+ return statement.actions unless statement.actions.empty?
95
+
96
+ statement.not_actions
97
+ end
98
+
99
+ def resource(statement)
100
+ return statement.resources unless statement.resources.empty?
101
+
102
+ statement.not_resources
103
+ end
104
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Weights
4
+ def weights
5
+ {
6
+ Base_Statement: 1,
7
+ Allow: 0,
8
+ Deny: 1,
9
+ NotAction: 1,
10
+ NotResource: 1,
11
+ Mixed_Wildcard: 1,
12
+
13
+ Extra_Service: 2,
14
+ Resource_Action_NotAligned: 1,
15
+
16
+ Condition: 2,
17
+ IfExists: 1,
18
+ Null: 1,
19
+ PolicyVariables: 1
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cfn-model'
4
+
5
+ ##
6
+ # Mix-in with metadata handling routines for the CustomRuleLoader
7
+ module Metadata
8
+ # XXX given mangled_metadatas is never used or returned,
9
+ # STDERR emit can be moved to unless block
10
+ def validate_cfn_nag_metadata(cfn_model)
11
+ mangled_metadatas = collect_mangled_metadata(cfn_model)
12
+ mangled_metadatas.each do |mangled_metadata|
13
+ logical_resource_id = mangled_metadata.first
14
+ mangled_rules = mangled_metadata[1]
15
+
16
+ STDERR.puts "#{logical_resource_id} has missing cfn_nag suppression rule id: #{mangled_rules}"
17
+ end
18
+ end
19
+
20
+ def cfn_model_with_suppressed_resources_removed(cfn_model:,
21
+ rule_id:,
22
+ allow_suppression:,
23
+ print_suppression:)
24
+ return cfn_model unless allow_suppression
25
+
26
+ cfn_model = cfn_model.copy
27
+
28
+ cfn_model.resources.delete_if do |logical_resource_id, resource|
29
+ rules_to_suppress = rules_to_suppress resource
30
+ if rules_to_suppress.nil?
31
+ false
32
+ else
33
+ suppress_resource?(rules_to_suppress, rule_id, logical_resource_id, print_suppression)
34
+ end
35
+ end
36
+ cfn_model
37
+ end
38
+
39
+ private
40
+
41
+ def suppress_resource?(rules_to_suppress, rule_id, logical_resource_id, print_suppression)
42
+ found_suppression_rule = rules_to_suppress.find do |rule_to_suppress|
43
+ next if rule_to_suppress['id'].nil?
44
+
45
+ rule_to_suppress['id'] == rule_id
46
+ end
47
+ if found_suppression_rule && print_suppression
48
+ message = "Suppressing #{rule_id} on #{logical_resource_id} for reason: #{found_suppression_rule['reason']}"
49
+ STDERR.puts message
50
+ end
51
+ !found_suppression_rule.nil?
52
+ end
53
+
54
+ def rules_to_suppress(resource)
55
+ if resource.metadata &&
56
+ resource.metadata['cfn_nag'] &&
57
+ resource.metadata['cfn_nag']['rules_to_suppress']
58
+
59
+ resource.metadata['cfn_nag']['rules_to_suppress']
60
+ end
61
+ end
62
+
63
+ def collect_mangled_metadata(cfn_model)
64
+ mangled_metadatas = []
65
+ cfn_model.resources.each do |logical_resource_id, resource|
66
+ resource_rules_to_suppress = rules_to_suppress resource
67
+ next if resource_rules_to_suppress.nil?
68
+
69
+ mangled_rules = resource_rules_to_suppress.select do |rule_to_suppress|
70
+ rule_to_suppress['id'].nil?
71
+ end
72
+ unless mangled_rules.empty?
73
+ mangled_metadatas << [logical_resource_id, mangled_rules]
74
+ end
75
+ end
76
+ mangled_metadatas
77
+ end
78
+ end
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.5.61
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Kascic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-06 00:00:00.000000000 Z
11
+ date: 2020-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -156,12 +156,14 @@ executables:
156
156
  - cfn_nag
157
157
  - cfn_nag_rules
158
158
  - cfn_nag_scan
159
+ - spcm_scan
159
160
  extensions: []
160
161
  extra_rdoc_files: []
161
162
  files:
162
163
  - bin/cfn_nag
163
164
  - bin/cfn_nag_rules
164
165
  - bin/cfn_nag_scan
166
+ - bin/spcm_scan
165
167
  - lib/cfn-nag.rb
166
168
  - lib/cfn-nag/base_rule.rb
167
169
  - lib/cfn-nag/blacklist_loader.rb
@@ -295,6 +297,7 @@ files:
295
297
  - lib/cfn-nag/custom_rules/S3BucketPolicyWildcardPrincipalRule.rb
296
298
  - lib/cfn-nag/custom_rules/S3BucketPublicReadAclRule.rb
297
299
  - lib/cfn-nag/custom_rules/S3BucketPublicReadWriteAclRule.rb
300
+ - lib/cfn-nag/custom_rules/SPCMRule.rb
298
301
  - lib/cfn-nag/custom_rules/SageMakerEndpointConfigKmsKeyIdRule.rb
299
302
  - lib/cfn-nag/custom_rules/SageMakerNotebookInstanceKmsKeyIdRule.rb
300
303
  - lib/cfn-nag/custom_rules/SecretsManagerSecretKmsKeyIdRule.rb
@@ -327,7 +330,14 @@ files:
327
330
  - lib/cfn-nag/custom_rules/password_base_rule.rb
328
331
  - lib/cfn-nag/custom_rules/resource_base_rule.rb
329
332
  - lib/cfn-nag/custom_rules/sub_property_with_list_password_base_rule.rb
333
+ - lib/cfn-nag/iam_complexity_metric/condition_metric.rb
334
+ - lib/cfn-nag/iam_complexity_metric/html_results_renderer.rb
335
+ - lib/cfn-nag/iam_complexity_metric/policy_document_metric.rb
336
+ - lib/cfn-nag/iam_complexity_metric/spcm.rb
337
+ - lib/cfn-nag/iam_complexity_metric/statement_metric.rb
338
+ - lib/cfn-nag/iam_complexity_metric/weights.rb
330
339
  - lib/cfn-nag/ip_addr.rb
340
+ - lib/cfn-nag/metadata.rb
331
341
  - lib/cfn-nag/profile_loader.rb
332
342
  - lib/cfn-nag/result_view/colored_stdout_results.rb
333
343
  - lib/cfn-nag/result_view/json_results.rb