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