cfn-nag 0.5.61 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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