cfn-nag 0.4.82 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|