cfn-nag 0.4.82 → 0.5.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/cfn_nag_rules +14 -3
- data/lib/cfn-nag/blacklist_loader.rb +1 -1
- data/lib/cfn-nag/cfn_nag.rb +6 -3
- data/lib/cfn-nag/cfn_nag_config.rb +6 -2
- data/lib/cfn-nag/cfn_nag_executor.rb +18 -14
- data/lib/cfn-nag/cli_options.rb +11 -3
- data/lib/cfn-nag/custom_rule_loader.rb +25 -94
- data/lib/cfn-nag/rule_dumper.rb +7 -3
- data/lib/cfn-nag/rule_registry.rb +40 -13
- data/lib/cfn-nag/rule_repo_exception.rb +7 -0
- data/lib/cfn-nag/rule_repos/file_based_rule_repo.rb +64 -0
- data/lib/cfn-nag/rule_repos/gem_based_rule_repo.rb +63 -0
- data/lib/cfn-nag/rule_repos/s3_based_rule_repo.rb +118 -0
- data/lib/cfn-nag/rule_repository_loader.rb +48 -0
- data/lib/cfn-nag/violation.rb +1 -1
- metadata +33 -16
- data/lib/cfn-nag/jmes_path_discovery.rb +0 -19
- data/lib/cfn-nag/jmes_path_evaluator.rb +0 -56
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3d07f5b4eb55109314e6ce4d6361da04baa6d194e5ff4ec7eb8b39f55a345da
|
4
|
+
data.tar.gz: 3761581983506be2911e8cdaf504590f68420f09ae9b368789db83979a161bbb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c678f4fc50b3c4419aab7d66956212d573413be0745469d3bf9325241d2f04b0a43b76013f830806863a7ac3b0683e5c54aa352dd2b66fe93cd64bf8fdb2ad2e
|
7
|
+
data.tar.gz: 00ff6c2bf813e325897ec761a365e334f9d8267892eccf82d1d0118261ab42226c7cde3e6857ff082b3cfab1ace3e3b0fd80f5d087b40718b5b13bddb8f92be2
|
data/bin/cfn_nag_rules
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require '
|
4
|
+
require 'optimist'
|
5
5
|
require 'cfn-nag'
|
6
6
|
require 'rubygems/specification'
|
7
7
|
|
8
|
-
opts =
|
8
|
+
opts = Optimist.options do
|
9
9
|
version Gem::Specification.find_by_name('cfn-nag').version
|
10
10
|
|
11
11
|
opt :rule_directory, 'Extra rule directories', type: :io,
|
@@ -18,6 +18,11 @@ opts = Trollop.options do
|
|
18
18
|
'Format of results: [csv, json, txt]',
|
19
19
|
type: :string,
|
20
20
|
default: 'txt'
|
21
|
+
|
22
|
+
opt :rule_repository,
|
23
|
+
'Path(s)s to rule repository to include in rule discovery',
|
24
|
+
type: :strings,
|
25
|
+
required: false
|
21
26
|
end
|
22
27
|
|
23
28
|
profile_definition = nil
|
@@ -25,8 +30,14 @@ unless opts[:profile_path].nil?
|
|
25
30
|
profile_definition = IO.read(opts[:profile_path])
|
26
31
|
end
|
27
32
|
|
33
|
+
rule_repository_definitions = []
|
34
|
+
opts[:rule_repository]&.each do |rule_repository|
|
35
|
+
rule_repository_definitions << IO.read(rule_repository)
|
36
|
+
end
|
37
|
+
|
28
38
|
rule_dumper = CfnNagRuleDumper.new(profile_definition: profile_definition,
|
29
39
|
rule_directory: opts[:rule_directory],
|
30
|
-
output_format: opts[:output_format]
|
40
|
+
output_format: opts[:output_format],
|
41
|
+
rule_repository_definitions: rule_repository_definitions)
|
31
42
|
|
32
43
|
rule_dumper.dump_rules
|
@@ -38,6 +38,6 @@ class BlackListLoader
|
|
38
38
|
def check_valid_rule_id(rule_id)
|
39
39
|
return true unless @rules_registry.by_id(rule_id).nil?
|
40
40
|
|
41
|
-
raise "#{rule_id} is not a legal rule identifier from: #{
|
41
|
+
raise "#{rule_id} is not a legal rule identifier from: #{@rules_registry.ids}"
|
42
42
|
end
|
43
43
|
end
|
data/lib/cfn-nag/cfn_nag.rb
CHANGED
@@ -87,12 +87,15 @@ class CfnNag
|
|
87
87
|
parameter_values_string,
|
88
88
|
true,
|
89
89
|
condition_values_string
|
90
|
-
violations += @config.custom_rule_loader.execute_custom_rules(
|
90
|
+
violations += @config.custom_rule_loader.execute_custom_rules(
|
91
|
+
cfn_model,
|
92
|
+
@config.custom_rule_loader.rule_definitions
|
93
|
+
)
|
91
94
|
|
92
95
|
violations = filter_violations_by_blacklist_and_profile(violations)
|
93
96
|
violations = mark_line_numbers(violations, cfn_model)
|
94
|
-
rescue Psych::SyntaxError, ParserError =>
|
95
|
-
violations << fatal_violation(
|
97
|
+
rescue RuleRepoException, Psych::SyntaxError, ParserError => fatal_error
|
98
|
+
violations << fatal_violation(fatal_error.to_s)
|
96
99
|
rescue JSON::ParserError => json_parameters_error
|
97
100
|
error = "JSON Parameter values parse error: #{json_parameters_error}"
|
98
101
|
violations << fatal_violation(error)
|
@@ -8,17 +8,20 @@ class CfnNagConfig
|
|
8
8
|
allow_suppression: true,
|
9
9
|
print_suppression: false,
|
10
10
|
isolate_custom_rule_exceptions: false,
|
11
|
-
fail_on_warnings: false
|
11
|
+
fail_on_warnings: false,
|
12
|
+
rule_repository_definitions: [])
|
12
13
|
@rule_directory = rule_directory
|
13
14
|
@custom_rule_loader = CustomRuleLoader.new(
|
14
15
|
rule_directory: rule_directory,
|
15
16
|
allow_suppression: allow_suppression,
|
16
17
|
print_suppression: print_suppression,
|
17
|
-
isolate_custom_rule_exceptions: isolate_custom_rule_exceptions
|
18
|
+
isolate_custom_rule_exceptions: isolate_custom_rule_exceptions,
|
19
|
+
rule_repository_definitions: rule_repository_definitions
|
18
20
|
)
|
19
21
|
@profile_definition = profile_definition
|
20
22
|
@blacklist_definition = blacklist_definition
|
21
23
|
@fail_on_warnings = fail_on_warnings
|
24
|
+
@rule_repositories = rule_repositories
|
22
25
|
end
|
23
26
|
# rubocop:enable Metrics/ParameterLists
|
24
27
|
|
@@ -27,4 +30,5 @@ class CfnNagConfig
|
|
27
30
|
attr_reader :profile_definition
|
28
31
|
attr_reader :blacklist_definition
|
29
32
|
attr_reader :fail_on_warnings
|
33
|
+
attr_reader :rule_repositories
|
30
34
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'optimist'
|
4
4
|
require 'cfn-nag/cli_options'
|
5
5
|
require 'cfn-nag/cfn_nag_config'
|
6
6
|
|
@@ -10,6 +10,7 @@ class CfnNagExecutor
|
|
10
10
|
@blacklist_definition = nil
|
11
11
|
@parameter_values_string = nil
|
12
12
|
@condition_values_string = nil
|
13
|
+
@rule_repository_definitions = []
|
13
14
|
end
|
14
15
|
|
15
16
|
def scan(options_type:)
|
@@ -73,26 +74,28 @@ class CfnNagExecutor
|
|
73
74
|
|
74
75
|
def validate_options(opts)
|
75
76
|
unless opts[:output_format].nil? || %w[colortxt txt json].include?(opts[:output_format])
|
76
|
-
|
77
|
-
|
77
|
+
Optimist.die(:output_format,
|
78
|
+
'Must be colortxt, txt, or json')
|
78
79
|
end
|
79
80
|
end
|
80
81
|
|
81
82
|
def execute_io_options(opts)
|
82
|
-
|
83
|
-
@profile_definition = IO.read(opts[:profile_path])
|
84
|
-
end
|
83
|
+
@profile_definition = read_conditionally(opts[:profile_path])
|
85
84
|
|
86
|
-
|
87
|
-
|
88
|
-
|
85
|
+
@blacklist_definition = read_conditionally(opts[:blacklist_path])
|
86
|
+
|
87
|
+
@parameter_values_string = read_conditionally(opts[:parameter_values_path])
|
89
88
|
|
90
|
-
|
91
|
-
|
89
|
+
@condition_values_string = read_conditionally(opts[:condition_values_path])
|
90
|
+
|
91
|
+
opts[:rule_repository]&.each do |rule_repository|
|
92
|
+
@rule_repository_definitions << IO.read(rule_repository)
|
92
93
|
end
|
94
|
+
end
|
93
95
|
|
94
|
-
|
95
|
-
|
96
|
+
def read_conditionally(path)
|
97
|
+
unless path.nil?
|
98
|
+
IO.read(path)
|
96
99
|
end
|
97
100
|
end
|
98
101
|
|
@@ -104,7 +107,8 @@ class CfnNagExecutor
|
|
104
107
|
allow_suppression: opts[:allow_suppression],
|
105
108
|
print_suppression: opts[:print_suppression],
|
106
109
|
isolate_custom_rule_exceptions: opts[:isolate_custom_rule_exceptions],
|
107
|
-
fail_on_warnings: opts[:fail_on_warnings]
|
110
|
+
fail_on_warnings: opts[:fail_on_warnings],
|
111
|
+
rule_repository_definitions: @rule_repository_definitions
|
108
112
|
)
|
109
113
|
end
|
110
114
|
|
data/lib/cfn-nag/cli_options.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'optimist'
|
4
4
|
|
5
5
|
# rubocop:disable Metrics/ClassLength
|
6
6
|
class Options
|
@@ -29,7 +29,7 @@ class Options
|
|
29
29
|
custom_rule_exceptions_message = @custom_rule_exceptions_message
|
30
30
|
version = @version
|
31
31
|
|
32
|
-
|
32
|
+
Optimist.options do
|
33
33
|
usage options_message
|
34
34
|
version version
|
35
35
|
|
@@ -87,6 +87,10 @@ class Options
|
|
87
87
|
'Format of results: [txt, json, colortxt]',
|
88
88
|
type: :string,
|
89
89
|
default: 'colortxt'
|
90
|
+
opt :rule_repository,
|
91
|
+
'Path(s) to a rule repository to include in rule discovery',
|
92
|
+
type: :strings,
|
93
|
+
required: false
|
90
94
|
end
|
91
95
|
end
|
92
96
|
|
@@ -102,7 +106,7 @@ class Options
|
|
102
106
|
custom_rule_exceptions_message = @custom_rule_exceptions_message
|
103
107
|
version = @version
|
104
108
|
|
105
|
-
|
109
|
+
Optimist.options do
|
106
110
|
version version
|
107
111
|
opt :input_path,
|
108
112
|
input_path_message,
|
@@ -167,6 +171,10 @@ class Options
|
|
167
171
|
type: :boolean,
|
168
172
|
required: false,
|
169
173
|
default: false
|
174
|
+
opt :rule_repository,
|
175
|
+
'Path(s)s to rule repository to include in rule discovery',
|
176
|
+
type: :strings,
|
177
|
+
required: false
|
170
178
|
end
|
171
179
|
end
|
172
180
|
# rubocop:enable Metrics/BlockLength
|
@@ -3,47 +3,46 @@
|
|
3
3
|
require 'cfn-model'
|
4
4
|
require 'logging'
|
5
5
|
require_relative 'rule_registry'
|
6
|
-
|
7
|
-
|
6
|
+
require_relative 'rule_repos/file_based_rule_repo'
|
7
|
+
require_relative 'rule_repos/gem_based_rule_repo'
|
8
|
+
require_relative 'rule_repos/s3_based_rule_repo'
|
9
|
+
require_relative 'rule_repository_loader'
|
8
10
|
|
9
11
|
##
|
10
12
|
# This object can discover the internal and custom user-provided rules and
|
11
13
|
# apply these rules to a CfnModel object
|
12
14
|
#
|
13
|
-
# rubocop:disable Metrics/ClassLength
|
14
15
|
class CustomRuleLoader
|
15
16
|
def initialize(rule_directory: nil,
|
16
17
|
allow_suppression: true,
|
17
18
|
print_suppression: false,
|
18
|
-
isolate_custom_rule_exceptions: false
|
19
|
+
isolate_custom_rule_exceptions: false,
|
20
|
+
rule_repository_definitions: [])
|
19
21
|
@rule_directory = rule_directory
|
20
22
|
@allow_suppression = allow_suppression
|
21
23
|
@print_suppression = print_suppression
|
22
24
|
@isolate_custom_rule_exceptions = isolate_custom_rule_exceptions
|
23
|
-
|
25
|
+
@rule_repository_definitions = rule_repository_definitions
|
26
|
+
@registry = nil
|
24
27
|
end
|
25
28
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
eval IO.read jmespath_file
|
39
|
-
end
|
29
|
+
##
|
30
|
+
# the first time this runs, it's "expensive". the core rules, the gem-based rules will load, and
|
31
|
+
# any other repos like "s3" will go the expensive route. after that, it's cached so you can
|
32
|
+
# call it as many times as you like unless you force_refresh
|
33
|
+
#
|
34
|
+
def rule_definitions(force_refresh: false)
|
35
|
+
if @registry.nil? || force_refresh
|
36
|
+
@registry = FileBasedRuleRepo.new(@rule_directory).discover_rules
|
37
|
+
@registry.merge! GemBasedRuleRepo.new.discover_rules
|
38
|
+
|
39
|
+
@registry = RuleRepositoryLoader.new.merge(@registry, @rule_repository_definitions)
|
40
|
+
@registry
|
40
41
|
end
|
41
|
-
|
42
|
-
rule_registry
|
42
|
+
@registry
|
43
43
|
end
|
44
|
-
# rubocop:enable Security/Eval
|
45
44
|
|
46
|
-
def execute_custom_rules(cfn_model)
|
45
|
+
def execute_custom_rules(cfn_model, rules_registry)
|
47
46
|
if Logging.logger['log'].debug?
|
48
47
|
Logging.logger['log'].debug "cfn_model: #{cfn_model}"
|
49
48
|
end
|
@@ -52,37 +51,16 @@ class CustomRuleLoader
|
|
52
51
|
|
53
52
|
validate_cfn_nag_metadata(cfn_model)
|
54
53
|
|
55
|
-
filter_rule_classes cfn_model, violations
|
56
|
-
|
57
|
-
filter_jmespath_filenames cfn_model, violations
|
54
|
+
filter_rule_classes cfn_model, violations, rules_registry
|
58
55
|
|
59
56
|
violations
|
60
57
|
end
|
61
58
|
|
62
59
|
private
|
63
60
|
|
64
|
-
def rule_registry_from_rule_class(rule_class)
|
65
|
-
rule = rule_class.new
|
66
|
-
{ id: rule.rule_id,
|
67
|
-
type: rule.rule_type,
|
68
|
-
message: rule.rule_text }
|
69
|
-
end
|
70
|
-
|
71
|
-
# rubocop:disable Security/Eval
|
72
|
-
def filter_jmespath_filenames(cfn_model, violations)
|
73
|
-
discover_jmespath_filenames(@rule_directory).each do |jmespath_file|
|
74
|
-
evaluator = JmesPathEvaluator.new cfn_model
|
75
|
-
evaluator.instance_eval do
|
76
|
-
eval IO.read jmespath_file
|
77
|
-
end
|
78
|
-
violations += evaluator.violations
|
79
|
-
end
|
80
|
-
end
|
81
|
-
# rubocop:enable Security/Eval
|
82
|
-
|
83
61
|
# rubocop:disable Style/RedundantBegin
|
84
|
-
def filter_rule_classes(cfn_model, violations)
|
85
|
-
|
62
|
+
def filter_rule_classes(cfn_model, violations, rules_registry)
|
63
|
+
rules_registry.rule_classes.each do |rule_class|
|
86
64
|
begin
|
87
65
|
filtered_cfn_model = cfn_model_with_suppressed_resources_removed(
|
88
66
|
cfn_model: cfn_model,
|
@@ -169,51 +147,4 @@ class CustomRuleLoader
|
|
169
147
|
end
|
170
148
|
cfn_model
|
171
149
|
end
|
172
|
-
|
173
|
-
def validate_extra_rule_directory(rule_directory)
|
174
|
-
return true if rule_directory.nil? || File.directory?(rule_directory)
|
175
|
-
|
176
|
-
raise "Not a real directory #{rule_directory}"
|
177
|
-
end
|
178
|
-
|
179
|
-
def discover_rule_filenames(rule_directory)
|
180
|
-
rule_filenames = []
|
181
|
-
unless rule_directory.nil?
|
182
|
-
rule_filenames += Dir[File.join(rule_directory, '*Rule.rb')].sort
|
183
|
-
end
|
184
|
-
rule_filenames += Dir[File.join(__dir__, 'custom_rules', '*Rule.rb')].sort
|
185
|
-
# Windows fix when running ruby from Command Prompt and not bash
|
186
|
-
rule_filenames.reject! { |filename| filename =~ /_rule.rb$/ }
|
187
|
-
Logging.logger['log'].debug "rule_filenames: #{rule_filenames}"
|
188
|
-
rule_filenames
|
189
|
-
end
|
190
|
-
|
191
|
-
def discover_rule_classes(rule_directory)
|
192
|
-
rule_classes = []
|
193
|
-
|
194
|
-
rule_filenames = discover_rule_filenames(rule_directory)
|
195
|
-
|
196
|
-
rule_filenames.each do |rule_filename|
|
197
|
-
require(rule_filename)
|
198
|
-
rule_classname = File.basename(rule_filename, '.rb')
|
199
|
-
|
200
|
-
rule_classes << Object.const_get(rule_classname)
|
201
|
-
end
|
202
|
-
Logging.logger['log'].debug "rule_classes: #{rule_classes}"
|
203
|
-
|
204
|
-
rule_classes
|
205
|
-
end
|
206
|
-
|
207
|
-
def discover_jmespath_filenames(rule_directory)
|
208
|
-
rule_filenames = []
|
209
|
-
unless rule_directory.nil?
|
210
|
-
rule_filenames += Dir[File.join(rule_directory, '*jmespath.rb')].sort
|
211
|
-
end
|
212
|
-
rule_filenames += Dir[File.join(__dir__,
|
213
|
-
'custom_rules',
|
214
|
-
'*jmespath.rb')].sort
|
215
|
-
Logging.logger['log'].debug "jmespath_filenames: #{rule_filenames}"
|
216
|
-
rule_filenames
|
217
|
-
end
|
218
150
|
end
|
219
|
-
# rubocop:enable Metrics/ClassLength
|
data/lib/cfn-nag/rule_dumper.rb
CHANGED
@@ -3,19 +3,23 @@
|
|
3
3
|
require_relative 'custom_rule_loader'
|
4
4
|
require_relative 'profile_loader'
|
5
5
|
require_relative 'result_view/rules_view'
|
6
|
+
require_relative 'rule_repository_loader'
|
6
7
|
|
7
8
|
class CfnNagRuleDumper
|
8
9
|
def initialize(profile_definition: nil,
|
9
10
|
rule_directory: nil,
|
10
|
-
output_format: nil
|
11
|
+
output_format: nil,
|
12
|
+
rule_repository_definitions: [])
|
11
13
|
@rule_directory = rule_directory
|
12
14
|
@profile_definition = profile_definition
|
13
15
|
@output_format = output_format
|
16
|
+
@rule_repository_definitions = rule_repository_definitions
|
14
17
|
end
|
15
18
|
|
16
19
|
def dump_rules
|
17
|
-
|
18
|
-
rule_registry
|
20
|
+
rule_registry = FileBasedRuleRepo.new(@rule_directory).discover_rules
|
21
|
+
rule_registry.merge! GemBasedRuleRepo.new.discover_rules
|
22
|
+
rule_registry = RuleRepositoryLoader.new.merge(rule_registry, @rule_repository_definitions)
|
19
23
|
|
20
24
|
profile = nil
|
21
25
|
unless @profile_definition.nil?
|
@@ -1,33 +1,56 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
3
4
|
require_relative 'rule_definition'
|
4
5
|
|
6
|
+
##
|
7
|
+
# This registry contains all the discovered rule classes that are to be used
|
8
|
+
# for inspection.
|
9
|
+
#
|
10
|
+
# Historically this just kept the metadata around the rules that percolated into
|
11
|
+
# the violation reports. This is no longer true as the rule discovery logic is being
|
12
|
+
# split out to support multiple discover algorithms (i.e. rule repositories).
|
13
|
+
# The CustomRuleLoader asks the discovery delegate to the rule classes, and that
|
14
|
+
# discovery returns this object with the rule definitions, and the class itself
|
15
|
+
# that can be invoked to do an inspection
|
16
|
+
#
|
5
17
|
class RuleRegistry
|
6
|
-
attr_reader :rules, :duplicate_ids
|
18
|
+
attr_reader :rules, :duplicate_ids, :rule_classes
|
7
19
|
|
8
20
|
def initialize
|
9
21
|
@rules = []
|
10
22
|
@duplicate_ids = []
|
23
|
+
@rule_classes = Set.new
|
11
24
|
end
|
12
25
|
|
13
26
|
def duplicate_ids?
|
14
27
|
@duplicate_ids.count.positive?
|
15
28
|
end
|
16
29
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
30
|
+
def merge!(other_rule_registry)
|
31
|
+
@rules += other_rule_registry.rules
|
32
|
+
@duplicate_ids += other_rule_registry.duplicate_ids
|
33
|
+
@rule_classes += other_rule_registry.rule_classes
|
34
|
+
end
|
35
|
+
|
36
|
+
def definition(rule_class)
|
37
|
+
@rule_classes.add rule_class
|
38
|
+
|
39
|
+
rule = rule_class.new
|
40
|
+
|
41
|
+
existing_def = by_id rule.rule_id
|
24
42
|
|
25
43
|
if existing_def.nil?
|
44
|
+
rule_definition = RuleDefinition.new(
|
45
|
+
id: rule.rule_id,
|
46
|
+
type: rule.rule_type,
|
47
|
+
message: rule.rule_text
|
48
|
+
)
|
26
49
|
add_rule rule_definition
|
27
50
|
else
|
28
51
|
@duplicate_ids << {
|
29
|
-
id:
|
30
|
-
new_message:
|
52
|
+
id: rule.rule_id,
|
53
|
+
new_message: rule.rule_text,
|
31
54
|
registered_message: existing_def.message
|
32
55
|
}
|
33
56
|
end
|
@@ -37,6 +60,10 @@ class RuleRegistry
|
|
37
60
|
@rules.find { |rule| rule.id == id }
|
38
61
|
end
|
39
62
|
|
63
|
+
def ids
|
64
|
+
@rules.map(&:id)
|
65
|
+
end
|
66
|
+
|
40
67
|
def warnings
|
41
68
|
@rules.select { |rule| rule.type == RuleDefinition::WARNING }
|
42
69
|
end
|
@@ -47,8 +74,8 @@ class RuleRegistry
|
|
47
74
|
|
48
75
|
private
|
49
76
|
|
50
|
-
def add_rule(
|
51
|
-
@rules <<
|
52
|
-
|
77
|
+
def add_rule(rule_definition)
|
78
|
+
@rules << rule_definition
|
79
|
+
rule_definition
|
53
80
|
end
|
54
81
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logging'
|
4
|
+
|
5
|
+
##
|
6
|
+
# This is really the traditional implementation for CustomRuleLoader
|
7
|
+
# that looks in cfn-nag/custom_rules and an optional directory of a
|
8
|
+
# client's choosing
|
9
|
+
#
|
10
|
+
class FileBasedRuleRepo
|
11
|
+
def initialize(rule_directory)
|
12
|
+
@rule_directory = rule_directory
|
13
|
+
validate_extra_rule_directory rule_directory
|
14
|
+
end
|
15
|
+
|
16
|
+
def discover_rules
|
17
|
+
rule_registry = RuleRegistry.new
|
18
|
+
|
19
|
+
# we look on the file system, and we load from the file system into a Class
|
20
|
+
# that the runtime can refer back to later from the registry which is effectively
|
21
|
+
# just a set of rule definitons
|
22
|
+
discover_rule_classes(@rule_directory).each do |rule_class|
|
23
|
+
rule_registry.definition(rule_class)
|
24
|
+
end
|
25
|
+
|
26
|
+
rule_registry
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_extra_rule_directory(rule_directory)
|
32
|
+
return true if rule_directory.nil? || File.directory?(rule_directory)
|
33
|
+
|
34
|
+
raise "Not a real directory #{rule_directory}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def discover_rule_filenames(rule_directory)
|
38
|
+
rule_filenames = []
|
39
|
+
unless rule_directory.nil?
|
40
|
+
rule_filenames += Dir[File.join(rule_directory, '*Rule.rb')].sort
|
41
|
+
end
|
42
|
+
rule_filenames += Dir[File.join(__dir__, '..', 'custom_rules', '*Rule.rb')].sort
|
43
|
+
|
44
|
+
# Windows fix when running ruby from Command Prompt and not bash
|
45
|
+
rule_filenames.reject! { |filename| filename =~ /_rule.rb$/ }
|
46
|
+
Logging.logger['log'].debug "rule_filenames: #{rule_filenames}"
|
47
|
+
rule_filenames
|
48
|
+
end
|
49
|
+
|
50
|
+
def discover_rule_classes(rule_directory)
|
51
|
+
rule_classes = []
|
52
|
+
|
53
|
+
rule_filenames = discover_rule_filenames(rule_directory)
|
54
|
+
rule_filenames.each do |rule_filename|
|
55
|
+
require(rule_filename)
|
56
|
+
rule_classname = File.basename(rule_filename, '.rb')
|
57
|
+
|
58
|
+
rule_classes << Object.const_get(rule_classname)
|
59
|
+
end
|
60
|
+
Logging.logger['log'].debug "rule_classes: #{rule_classes}"
|
61
|
+
|
62
|
+
rule_classes
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cfn-nag/base_rule'
|
4
|
+
require 'rubygems'
|
5
|
+
|
6
|
+
class GemBasedRuleRepo
|
7
|
+
def discover_rules
|
8
|
+
rule_registry = RuleRegistry.new
|
9
|
+
|
10
|
+
rule_gem_names.each do |rule_gem_name|
|
11
|
+
next unless load_gem_entrypoint(rule_gem_name)
|
12
|
+
|
13
|
+
gem_path = Gem.loaded_specs[rule_gem_name].full_gem_path
|
14
|
+
require_all_rb_files_in_gem gem_path, rule_gem_name
|
15
|
+
end
|
16
|
+
|
17
|
+
unless rule_gem_names.empty?
|
18
|
+
ObjectSpace.each_object do |object|
|
19
|
+
if derives_from_base_rule?(object) || (object.respond_to?(:cfn_nag_rule?) && object.cfn_nag_rule?)
|
20
|
+
rule_registry.definition(object)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
rule_registry
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def require_all_rb_files_in_gem(gem_path, rule_gem_name)
|
31
|
+
Dir.glob("#{gem_path}/lib/#{rule_gem_name}/**/*.rb").sort.each do |rule_file|
|
32
|
+
require rule_file
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def rule_gem_names
|
37
|
+
rule_gem_specs.map(&:name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def rule_gem_specs
|
41
|
+
specs = []
|
42
|
+
|
43
|
+
# https://github.com/rvm/rubygems-bundler can interfere here
|
44
|
+
# if you happen to have a gemspec in the current working directory?
|
45
|
+
# Bundle.setup runs and replaces a SpecSet in here instead of collection of Specification?
|
46
|
+
Gem::Specification.each do |spec|
|
47
|
+
specs << spec if spec.metadata && spec.metadata['cfn_nag_rules'] == 'true'
|
48
|
+
end
|
49
|
+
specs
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_gem_entrypoint(rule_gem_name)
|
53
|
+
require rule_gem_name
|
54
|
+
true
|
55
|
+
rescue LoadError
|
56
|
+
STDERR.puts "Could not require #{rule_gem_name} - does the rule gem have a top level entry point?"
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def derives_from_base_rule?(object)
|
61
|
+
object.respond_to?(:superclass) && object.superclass == CfnNag::BaseRule
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-s3'
|
4
|
+
require 'lightly'
|
5
|
+
require 'json'
|
6
|
+
require_relative '../rule_registry'
|
7
|
+
require_relative '../rule_repo_exception'
|
8
|
+
|
9
|
+
class Object
|
10
|
+
##
|
11
|
+
# This is meta-magic evil. eval apparently has lexical scope so... opening up Object to
|
12
|
+
# evaluate ruby code that contains top-level Class definitions
|
13
|
+
#
|
14
|
+
# Without this, the class ends up "under" the scope of the class which in this case would be
|
15
|
+
# S3BucketBasedRuleRepo
|
16
|
+
#
|
17
|
+
# rubocop:disable Security/Eval
|
18
|
+
def eval_code_in_object_scope(code)
|
19
|
+
eval code
|
20
|
+
end
|
21
|
+
# rubocop:enable Security/Eval
|
22
|
+
end
|
23
|
+
|
24
|
+
class S3BucketBasedRuleRepo
|
25
|
+
def initialize(s3_bucket_name:, prefix:, index_lifetime: '1h', aws_profile: nil)
|
26
|
+
@s3_bucket_name = s3_bucket_name
|
27
|
+
@prefix = prefix
|
28
|
+
@index_cache = Lightly.new(
|
29
|
+
dir: cache_path('cfn_nag_s3_index_cache', s3_bucket_name),
|
30
|
+
life: index_lifetime,
|
31
|
+
hash: true
|
32
|
+
)
|
33
|
+
|
34
|
+
# except in dev mode, rules are immutable so once we have it don't worry about it changing
|
35
|
+
@rule_cache = Lightly.new(
|
36
|
+
dir: cache_path('cfn_nag_s3_rule_cache', s3_bucket_name),
|
37
|
+
life: '1000d',
|
38
|
+
hash: true
|
39
|
+
)
|
40
|
+
@aws_profile = aws_profile
|
41
|
+
@s3_resource = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def discover_rules
|
45
|
+
rule_registry = RuleRegistry.new
|
46
|
+
|
47
|
+
index = index(@s3_bucket_name, @prefix)
|
48
|
+
index.each do |rule_object_key|
|
49
|
+
rule_code = @rule_cache.get(rule_object_key) do
|
50
|
+
cache_miss(rule_object_key)
|
51
|
+
end
|
52
|
+
|
53
|
+
rule_class_name = select_class_name_from_object_key(rule_object_key)
|
54
|
+
|
55
|
+
eval_code_in_object_scope rule_code
|
56
|
+
|
57
|
+
rule_registry.definition(Object.const_get(rule_class_name))
|
58
|
+
end
|
59
|
+
|
60
|
+
rule_registry
|
61
|
+
end
|
62
|
+
|
63
|
+
def nuke_cache
|
64
|
+
cached_dirs = [
|
65
|
+
cache_path('cfn_nag_s3_index_cache', @s3_bucket_name),
|
66
|
+
cache_path('cfn_nag_s3_rule_cache', @s3_bucket_name)
|
67
|
+
]
|
68
|
+
FileUtils.rm_rf(cached_dirs)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def cache_miss(key)
|
74
|
+
rule_code_record = s3_object_content(@s3_bucket_name, key)
|
75
|
+
rule_code_record.body.read
|
76
|
+
end
|
77
|
+
|
78
|
+
def cache_path(cache_name, s3_bucket_name)
|
79
|
+
"/tmp/#{cache_name}/#{s3_bucket_name}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def s3_resource
|
83
|
+
return @s3_resource unless @s3_resource.nil?
|
84
|
+
|
85
|
+
@s3_resource = @aws_profile ? Aws::S3::Resource.new(profile: @aws_profile) : Aws::S3::Resource.new
|
86
|
+
end
|
87
|
+
|
88
|
+
def select_class_name_from_object_key(key)
|
89
|
+
key.split('/')[-1].gsub('.rb', '')
|
90
|
+
end
|
91
|
+
|
92
|
+
def index(s3_bucket_name, prefix)
|
93
|
+
index_json_str = @index_cache.get('index_cfn_nag') do
|
94
|
+
discover_rule_s3_objects(s3_bucket_name, prefix).to_json
|
95
|
+
end
|
96
|
+
JSON.parse(index_json_str)
|
97
|
+
end
|
98
|
+
|
99
|
+
def s3_object(s3_bucket_name, key)
|
100
|
+
rule_bucket = s3_resource.bucket(s3_bucket_name)
|
101
|
+
rule_bucket.object key
|
102
|
+
end
|
103
|
+
|
104
|
+
def s3_object_content(s3_bucket_name, key)
|
105
|
+
s3_object(s3_bucket_name, key).get
|
106
|
+
end
|
107
|
+
|
108
|
+
def discover_rule_s3_objects(s3_bucket_name, prefix)
|
109
|
+
rule_bucket = s3_resource.bucket(s3_bucket_name)
|
110
|
+
objects = rule_bucket.objects(prefix: prefix)
|
111
|
+
rule_objects = objects.select do |object|
|
112
|
+
object.key.match(/.*Rule\.rb/)
|
113
|
+
end
|
114
|
+
rule_objects.map(&:key)
|
115
|
+
rescue Aws::S3::Errors::NoSuchBucket
|
116
|
+
raise RuleRepoException.new(msg: "Rule bucket not found: #{s3_bucket_name}")
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require_relative 'rule_repo_exception'
|
5
|
+
|
6
|
+
##
|
7
|
+
# This captures logic for instantiating the RuleRepo implementations
|
8
|
+
# and merging their rule registries. This was baked into CustomRuleLoader
|
9
|
+
# but broken out for cfn_nag_rules to use as well
|
10
|
+
#
|
11
|
+
class RuleRepositoryLoader
|
12
|
+
def merge(rule_registry, rule_repository_definitions)
|
13
|
+
rule_repository_definitions.each do |rule_repository_definition|
|
14
|
+
rule_registry.merge! rule_repository(rule_repository_definition).discover_rules
|
15
|
+
end
|
16
|
+
rule_registry
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def to_sym_keys(hash)
|
22
|
+
sym_hash = {}
|
23
|
+
hash.each do |k, v|
|
24
|
+
sym_hash[k.to_sym] = v
|
25
|
+
end
|
26
|
+
sym_hash
|
27
|
+
end
|
28
|
+
|
29
|
+
def rule_repository(rule_repository_definition_str)
|
30
|
+
rule_repository_definition = YAML.safe_load rule_repository_definition_str
|
31
|
+
unless rule_repository_definition['repo_class_name']
|
32
|
+
raise RuleRepoException.new(msg: 'Malformed repo definition: missing repo_class_name')
|
33
|
+
end
|
34
|
+
|
35
|
+
repo_class = class_from_name(rule_repository_definition['repo_class_name'])
|
36
|
+
if rule_repository_definition['repo_arguments']&.is_a?(Hash)
|
37
|
+
repo_class.new(**to_sym_keys(rule_repository_definition['repo_arguments']))
|
38
|
+
else
|
39
|
+
repo_class.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def class_from_name(name)
|
44
|
+
Object.const_get name
|
45
|
+
rescue NameError
|
46
|
+
raise RuleRepoException.new(msg: "Malformed repo definition: repo_class_name: #{name} not loaded")
|
47
|
+
end
|
48
|
+
end
|
data/lib/cfn-nag/violation.rb
CHANGED
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.5.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-02-
|
11
|
+
date: 2020-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -81,61 +81,75 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.4.14
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: logging
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: 2.2.2
|
90
90
|
type: :runtime
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 2.2.2
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: netaddr
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 2.
|
103
|
+
version: 2.0.4
|
104
104
|
type: :runtime
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 2.
|
110
|
+
version: 2.0.4
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: optimist
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version:
|
117
|
+
version: 3.0.0
|
118
118
|
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version:
|
124
|
+
version: 3.0.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: aws-sdk-s3
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 1.60.1
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 1.60.1
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
140
|
+
name: lightly
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
128
142
|
requirements:
|
129
143
|
- - "~>"
|
130
144
|
- !ruby/object:Gem::Version
|
131
|
-
version:
|
145
|
+
version: 0.3.2
|
132
146
|
type: :runtime
|
133
147
|
prerelease: false
|
134
148
|
version_requirements: !ruby/object:Gem::Requirement
|
135
149
|
requirements:
|
136
150
|
- - "~>"
|
137
151
|
- !ruby/object:Gem::Version
|
138
|
-
version:
|
152
|
+
version: 0.3.2
|
139
153
|
description: Auditing tool for CloudFormation templates
|
140
154
|
email:
|
141
155
|
executables:
|
@@ -287,8 +301,6 @@ files:
|
|
287
301
|
- lib/cfn-nag/custom_rules/password_base_rule.rb
|
288
302
|
- lib/cfn-nag/custom_rules/sub_property_with_list_password_base_rule.rb
|
289
303
|
- lib/cfn-nag/ip_addr.rb
|
290
|
-
- lib/cfn-nag/jmes_path_discovery.rb
|
291
|
-
- lib/cfn-nag/jmes_path_evaluator.rb
|
292
304
|
- lib/cfn-nag/profile_loader.rb
|
293
305
|
- lib/cfn-nag/result_view/colored_stdout_results.rb
|
294
306
|
- lib/cfn-nag/result_view/json_results.rb
|
@@ -299,6 +311,11 @@ files:
|
|
299
311
|
- lib/cfn-nag/rule_dumper.rb
|
300
312
|
- lib/cfn-nag/rule_id_set.rb
|
301
313
|
- lib/cfn-nag/rule_registry.rb
|
314
|
+
- lib/cfn-nag/rule_repo_exception.rb
|
315
|
+
- lib/cfn-nag/rule_repos/file_based_rule_repo.rb
|
316
|
+
- lib/cfn-nag/rule_repos/gem_based_rule_repo.rb
|
317
|
+
- lib/cfn-nag/rule_repos/s3_based_rule_repo.rb
|
318
|
+
- lib/cfn-nag/rule_repository_loader.rb
|
302
319
|
- lib/cfn-nag/template_discovery.rb
|
303
320
|
- lib/cfn-nag/util/blank.rb
|
304
321
|
- lib/cfn-nag/util/enforce_reference_parameter.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class JmesPathDiscovery
|
4
|
-
def initialize(rule_registry)
|
5
|
-
@rule_registry = rule_registry
|
6
|
-
end
|
7
|
-
|
8
|
-
def warning(id:, message:)
|
9
|
-
@rule_registry.definition(id: id,
|
10
|
-
type: Violation::WARNING,
|
11
|
-
message: message)
|
12
|
-
end
|
13
|
-
|
14
|
-
def failure(id:, message:)
|
15
|
-
@rule_registry.definition(id: id,
|
16
|
-
type: Violation::FAILING_VIOLATION,
|
17
|
-
message: message)
|
18
|
-
end
|
19
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'jmespath'
|
4
|
-
require 'logging'
|
5
|
-
|
6
|
-
##
|
7
|
-
# THIS DOES NOT RESPECT SUPPRESSIONS!!!!!!
|
8
|
-
##
|
9
|
-
class JmesPathEvaluator
|
10
|
-
def initialize(cfn_model)
|
11
|
-
@cfn_model = cfn_model
|
12
|
-
@warnings = []
|
13
|
-
@failures = []
|
14
|
-
end
|
15
|
-
|
16
|
-
def warning(id:, jmespath:, message:)
|
17
|
-
violation id: id,
|
18
|
-
jmespath: jmespath,
|
19
|
-
message: message,
|
20
|
-
violation_type: Violation::WARNING
|
21
|
-
end
|
22
|
-
|
23
|
-
def failure(id:, jmespath:, message:)
|
24
|
-
violation id: id,
|
25
|
-
jmespath: jmespath,
|
26
|
-
message: message,
|
27
|
-
violation_type: Violation::FAILING_VIOLATION
|
28
|
-
end
|
29
|
-
|
30
|
-
def violations
|
31
|
-
@warnings + @failures
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def violation(id:, jmespath:, message:, violation_type:)
|
37
|
-
Logging.logger['log'].debug jmespath
|
38
|
-
|
39
|
-
logical_resource_ids = JMESPath.search(jmespath,
|
40
|
-
flatten(@cfn_model.raw_model))
|
41
|
-
|
42
|
-
unless logical_resource_ids.empty?
|
43
|
-
@warnings << Violation.new(id: id,
|
44
|
-
type: violation_type,
|
45
|
-
message: message,
|
46
|
-
logical_resource_ids: logical_resource_ids)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def flatten(hash)
|
51
|
-
hash['Resources'].each do |logical_resource_id, resource|
|
52
|
-
resource['id'] = logical_resource_id
|
53
|
-
end
|
54
|
-
hash
|
55
|
-
end
|
56
|
-
end
|