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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da96aa37c7198a301552e939d2013246b2fe285c36719cb5e3cb44cc5e757ddf
4
- data.tar.gz: 4cdca4308db2101e4f7ae1e2403eb4944715912b6403b1e5e21629d392148e5e
3
+ metadata.gz: c3d07f5b4eb55109314e6ce4d6361da04baa6d194e5ff4ec7eb8b39f55a345da
4
+ data.tar.gz: 3761581983506be2911e8cdaf504590f68420f09ae9b368789db83979a161bbb
5
5
  SHA512:
6
- metadata.gz: 38e89b36905cb8573192787400cec3f43255637f1a9285bd4a72e63f55bc978bacdff0c59bbb6ce4d0f5fd222319e95076e1fe95153dcfc23527a8a8e6edaf56
7
- data.tar.gz: 621c466546551f7157be0a0008205d558f3c61094b066b5375b5186829ef59ce964e61190134217dceaa0a46c810554a60bab935fdfe4edc218c96cdb07df43e
6
+ metadata.gz: c678f4fc50b3c4419aab7d66956212d573413be0745469d3bf9325241d2f04b0a43b76013f830806863a7ac3b0683e5c54aa352dd2b66fe93cd64bf8fdb2ad2e
7
+ data.tar.gz: 00ff6c2bf813e325897ec761a365e334f9d8267892eccf82d1d0118261ab42226c7cde3e6857ff082b3cfab1ace3e3b0fd80f5d087b40718b5b13bddb8f92be2
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'trollop'
4
+ require 'optimist'
5
5
  require 'cfn-nag'
6
6
  require 'rubygems/specification'
7
7
 
8
- opts = Trollop.options do
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: #{rules_ids}"
41
+ raise "#{rule_id} is not a legal rule identifier from: #{@rules_registry.ids}"
42
42
  end
43
43
  end
@@ -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(cfn_model)
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 => parser_error
95
- violations << fatal_violation(parser_error.to_s)
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 'trollop'
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
- Trollop.die(:output_format,
77
- 'Must be colortxt, txt, or json')
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
- unless opts[:profile_path].nil?
83
- @profile_definition = IO.read(opts[:profile_path])
84
- end
83
+ @profile_definition = read_conditionally(opts[:profile_path])
85
84
 
86
- unless opts[:blacklist_path].nil?
87
- @blacklist_definition = IO.read(opts[:blacklist_path])
88
- end
85
+ @blacklist_definition = read_conditionally(opts[:blacklist_path])
86
+
87
+ @parameter_values_string = read_conditionally(opts[:parameter_values_path])
89
88
 
90
- unless opts[:parameter_values_path].nil?
91
- @parameter_values_string = IO.read(opts[:parameter_values_path])
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
- unless opts[:condition_values_path].nil?
95
- @condition_values_string = IO.read(opts[:condition_values_path])
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
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'trollop'
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
- Trollop.options do
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
- Trollop.options do
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
- require 'cfn-nag/jmes_path_evaluator'
7
- require 'cfn-nag/jmes_path_discovery'
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
- validate_extra_rule_directory rule_directory
25
+ @rule_repository_definitions = rule_repository_definitions
26
+ @registry = nil
24
27
  end
25
28
 
26
- # rubocop:disable Security/Eval
27
- def rule_definitions
28
- rule_registry = RuleRegistry.new
29
-
30
- discover_rule_classes(@rule_directory).each do |rule_class|
31
- rule_registry
32
- .definition(**rule_registry_from_rule_class(rule_class))
33
- end
34
-
35
- discover_jmespath_filenames(@rule_directory).each do |jmespath_file|
36
- evaluator = JmesPathDiscovery.new rule_registry
37
- evaluator.instance_eval do
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
- discover_rule_classes(@rule_directory).each do |rule_class|
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
@@ -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
- custom_rule_loader = CustomRuleLoader.new(rule_directory: @rule_directory)
18
- rule_registry = custom_rule_loader.rule_definitions
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 definition(id:,
18
- type:,
19
- message:)
20
- rule_definition = RuleDefinition.new(id: id,
21
- type: type,
22
- message: message)
23
- existing_def = by_id id
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: id,
30
- new_message: 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(violation_def)
51
- @rules << violation_def
52
- violation_def
77
+ def add_rule(rule_definition)
78
+ @rules << rule_definition
79
+ rule_definition
53
80
  end
54
81
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RuleRepoException < StandardError
4
+ def initialize(msg: 'Trouble loading a rule-repository')
5
+ super
6
+ end
7
+ 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
@@ -9,7 +9,7 @@ class Violation < RuleDefinition
9
9
  def initialize(id:,
10
10
  type:,
11
11
  message:,
12
- logical_resource_ids: nil,
12
+ logical_resource_ids: [],
13
13
  line_numbers: [])
14
14
  super id: id,
15
15
  type: type,
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.82
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-04 00:00:00.000000000 Z
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: jmespath
84
+ name: logging
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 1.3.1
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: 1.3.1
96
+ version: 2.2.2
97
97
  - !ruby/object:Gem::Dependency
98
- name: logging
98
+ name: netaddr
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 2.2.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.2.2
110
+ version: 2.0.4
111
111
  - !ruby/object:Gem::Dependency
112
- name: netaddr
112
+ name: optimist
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 2.0.4
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: 2.0.4
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: trollop
140
+ name: lightly
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - "~>"
130
144
  - !ruby/object:Gem::Version
131
- version: 2.1.2
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: 2.1.2
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