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 +4 -4
- data/bin/spcm_scan +69 -0
- data/lib/cfn-nag/cfn_nag.rb +1 -0
- data/lib/cfn-nag/cfn_nag_config.rb +4 -1
- data/lib/cfn-nag/cfn_nag_executor.rb +22 -1
- data/lib/cfn-nag/cli_options.rb +18 -0
- data/lib/cfn-nag/custom_rule_loader.rb +23 -72
- data/lib/cfn-nag/custom_rules/SPCMRule.rb +66 -0
- data/lib/cfn-nag/custom_rules/SecretsManagerSecretKmsKeyIdRule.rb +4 -3
- data/lib/cfn-nag/iam_complexity_metric/condition_metric.rb +85 -0
- data/lib/cfn-nag/iam_complexity_metric/html_results_renderer.rb +45 -0
- data/lib/cfn-nag/iam_complexity_metric/policy_document_metric.rb +11 -0
- data/lib/cfn-nag/iam_complexity_metric/spcm.rb +79 -0
- data/lib/cfn-nag/iam_complexity_metric/statement_metric.rb +104 -0
- data/lib/cfn-nag/iam_complexity_metric/weights.rb +22 -0
- data/lib/cfn-nag/metadata.rb +78 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce61954c2ef415db38dfd9173ab93fb92145a7a2cf5778901bad880d3aabc762
|
4
|
+
data.tar.gz: 8296a1bea671a836e464db5f9a4573d7b64ba87be90e841ce2ef5e823b61b49a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d07a9749c20b7083b9f01b479e20ffdcc327be635d9285e3f7175f8be39c87d7d66fc5fd5d1a7d728d399d9d5fc96a92d7ea42ac90014b79528bcdf2a4786e7
|
7
|
+
data.tar.gz: 227ef815606ef8eb763b1523de73ba0f4a0e8c6791afd809699057e41f37694ead17bd1714725838328516c45a369118852e5fadb76e334c75dde009ba459ed2
|
data/bin/spcm_scan
ADDED
@@ -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
|
data/lib/cfn-nag/cfn_nag.rb
CHANGED
@@ -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
|
|
data/lib/cfn-nag/cli_options.rb
CHANGED
@@ -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
|
-
|
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::
|
13
|
+
Violation::WARNING
|
13
14
|
end
|
14
15
|
|
15
16
|
def rule_id
|
16
|
-
'
|
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.
|
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-
|
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
|