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 +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
|