Skip to content

Commit 97717cb

Browse files
authoredMay 17, 2022
feat(scan): add rule directory recursive (#596)
1 parent 0f8532f commit 97717cb

File tree

6 files changed

+102
-12
lines changed

6 files changed

+102
-12
lines changed
 

‎lib/cfn-nag/cfn_nag_config.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ def initialize(profile_definition: nil,
1111
fail_on_warnings: false,
1212
ignore_fatal: false,
1313
rule_repository_definitions: [],
14-
rule_arguments: {})
14+
rule_arguments: {},
15+
rule_directory_recursive: false)
1516
@rule_directory = rule_directory
1617
@custom_rule_loader = CustomRuleLoader.new(
1718
rule_directory: rule_directory,
1819
allow_suppression: allow_suppression,
1920
print_suppression: print_suppression,
2021
isolate_custom_rule_exceptions: isolate_custom_rule_exceptions,
21-
rule_repository_definitions: rule_repository_definitions
22+
rule_repository_definitions: rule_repository_definitions,
23+
rule_directory_recursive: rule_directory_recursive
2224
)
2325
@profile_definition = profile_definition
2426
@deny_list_definition = deny_list_definition

‎lib/cfn-nag/cfn_nag_executor.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ def cfn_nag_config(opts)
130130
fail_on_warnings: opts[:fail_on_warnings],
131131
rule_repository_definitions: @rule_repository_definitions,
132132
ignore_fatal: opts[:ignore_fatal],
133-
rule_arguments: merge_rule_arguments(opts)
133+
rule_arguments: merge_rule_arguments(opts),
134+
rule_directory_recursive: opts[:rule_directory_recursive]
134135
)
135136
end
136137

‎lib/cfn-nag/cli_options.rb

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ def self.file_options
5454
type: :string,
5555
required: false,
5656
default: nil
57+
opt :rule_directory_recursive,
58+
'Recursively search extra rule directory',
59+
type: :boolean,
60+
required: false,
61+
default: false
5762
opt :profile_path,
5863
'Path to a profile file',
5964
type: :string,

‎lib/cfn-nag/custom_rule_loader.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ def initialize(rule_directory: nil,
2727
allow_suppression: true,
2828
print_suppression: false,
2929
isolate_custom_rule_exceptions: false,
30-
rule_repository_definitions: [])
30+
rule_repository_definitions: [],
31+
rule_directory_recursive: false)
3132
@rule_directory = rule_directory
3233
@allow_suppression = allow_suppression
3334
@print_suppression = print_suppression
3435
@isolate_custom_rule_exceptions = isolate_custom_rule_exceptions
3536
@rule_repository_definitions = rule_repository_definitions
37+
@rule_directory_recursive = rule_directory_recursive
3638
@registry = nil
3739
end
3840

@@ -43,7 +45,8 @@ def initialize(rule_directory: nil,
4345
#
4446
def rule_definitions(force_refresh: false)
4547
if @registry.nil? || force_refresh
46-
@registry = FileBasedRuleRepo.new(@rule_directory).discover_rules
48+
@registry = FileBasedRuleRepo.new(@rule_directory,
49+
rule_directory_recursive: @rule_directory_recursive).discover_rules
4750
@registry.merge! GemBasedRuleRepo.new.discover_rules
4851

4952
@registry = RuleRepositoryLoader.new.merge(@registry, @rule_repository_definitions)

‎lib/cfn-nag/rule_repos/file_based_rule_repo.rb

+18-7
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
# client's choosing
99
#
1010
class FileBasedRuleRepo
11-
def initialize(rule_directory)
11+
def initialize(rule_directory, rule_directory_recursive: false)
1212
@rule_directory = rule_directory
13+
@rule_directory_recursive = rule_directory_recursive
1314
validate_extra_rule_directory rule_directory
1415
end
1516

@@ -19,7 +20,8 @@ def discover_rules
1920
# we look on the file system, and we load from the file system into a Class
2021
# that the runtime can refer back to later from the registry which is effectively
2122
# just a set of rule definitons
22-
discover_rule_classes(@rule_directory).each do |rule_class|
23+
discover_rule_classes(@rule_directory,
24+
rule_directory_recursive: @rule_directory_recursive).each do |rule_class|
2325
rule_registry.definition(rule_class)
2426
end
2527

@@ -34,23 +36,32 @@ def validate_extra_rule_directory(rule_directory)
3436
raise "Not a real directory #{rule_directory}"
3537
end
3638

37-
def discover_rule_filenames(rule_directory)
39+
def locate_rule_files(rule_directory, rule_directory_recursive)
40+
return Dir.glob(File.join(rule_directory, '**/*Rule.rb')).sort if rule_directory_recursive
41+
42+
Dir[File.join(rule_directory, '*Rule.rb')].sort
43+
end
44+
45+
def discover_rule_filenames(rule_directory, rule_directory_recursive: false)
3846
rule_filenames = []
3947
unless rule_directory.nil?
40-
rule_filenames += Dir[File.join(rule_directory, '*Rule.rb')].sort
48+
rule_filenames += locate_rule_files(rule_directory, rule_directory_recursive)
4149
end
42-
rule_filenames += Dir[File.join(__dir__, '..', 'custom_rules', '*Rule.rb')].sort
50+
rule_filenames += locate_rule_files(File.join(__dir__, '..', 'custom_rules'), rule_directory_recursive)
4351

4452
# Windows fix when running ruby from Command Prompt and not bash
4553
rule_filenames.reject! { |filename| filename =~ /_rule.rb$/ }
4654
Logging.logger['log'].debug "rule_filenames: #{rule_filenames}"
4755
rule_filenames
4856
end
4957

50-
def discover_rule_classes(rule_directory)
58+
def discover_rule_classes(rule_directory, rule_directory_recursive: false)
5159
rule_classes = []
5260

53-
rule_filenames = discover_rule_filenames(rule_directory)
61+
rule_filenames = discover_rule_filenames(
62+
rule_directory,
63+
rule_directory_recursive: rule_directory_recursive
64+
)
5465
rule_filenames.each do |rule_filename|
5566
require(File.absolute_path(rule_filename))
5667
rule_classname = File.basename(rule_filename, '.rb')

‎spec/rules_repos/file_base_rule_repo_spec.rb

+68
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,30 @@ def audit_impl(cfn_model)
3131
RULE
3232
end
3333

34+
let(:valid_rule_text2) do
35+
<<~RULE
36+
require 'cfn-nag/custom_rules/base'
37+
require 'cfn-nag/violation'
38+
class ValidCustom2Rule < BaseRule
39+
def rule_text
40+
'this is fake rule text 2'
41+
end
42+
43+
def rule_type
44+
Violation::WARNING
45+
end
46+
47+
def rule_id
48+
'W9934'
49+
end
50+
51+
def audit_impl(cfn_model)
52+
%w(hardwired1 hardwired2)
53+
end
54+
end
55+
RULE
56+
end
57+
3458
it 'includes external rule definition from absolute directories' do
3559
Dir.mktmpdir(%w[custom_rule loader]) do |custom_rule_directory|
3660
# Write out a valid rule
@@ -88,6 +112,50 @@ def audit_impl(cfn_model)
88112
expect(actual_rule_classes.include?(expected_rule_classes)).to be true
89113
end
90114
end
115+
116+
it 'includes external rule definitions from subdirectories' do
117+
Dir.mktmpdir(%w[custom_rule loader], Dir.getwd) do |custom_rule_directory|
118+
# Write out a valid rule
119+
File.write(File.join(custom_rule_directory, 'ValidCustomRule.rb'), valid_rule_text)
120+
# Create a subdirectory for rules
121+
rule_subdir = File.join(custom_rule_directory, 'subdir')
122+
Dir.mkdir(rule_subdir)
123+
# Write a rule in a subdirectory
124+
File.write(File.join(rule_subdir, 'ValidCustom2Rule.rb'), valid_rule_text2)
125+
# Write out a invalid rule
126+
File.write(File.join(custom_rule_directory, 'InvalidRuleNotMatching.rb'), 'fake_rule')
127+
128+
core_rules_registry = FileBasedRuleRepo.new(nil).discover_rules
129+
actual_rule_registry = FileBasedRuleRepo.new(File.basename(custom_rule_directory),
130+
rule_directory_recursive: true).discover_rules
131+
132+
# Expect one additional rule loaded
133+
expect(actual_rule_registry.rules.size).to eq(core_rules_registry.rules.size+2)
134+
135+
# Validate that the rule was loaded by id
136+
expected_rule_definition = RuleDefinition.new id: 'W9933',
137+
name: 'ValidCustomRule',
138+
message: 'this is fake rule text',
139+
type: RuleDefinition::WARNING
140+
141+
actual_rule_definition = actual_rule_registry.by_id 'W9933'
142+
expect(actual_rule_definition).to eq expected_rule_definition
143+
144+
# Validate that the rule was loaded by id
145+
expected_rule_definition = RuleDefinition.new id: 'W9934',
146+
name: 'ValidCustom2Rule',
147+
message: 'this is fake rule text 2',
148+
type: RuleDefinition::WARNING
149+
150+
actual_rule_definition = actual_rule_registry.by_id 'W9934'
151+
expect(actual_rule_definition).to eq expected_rule_definition
152+
153+
# Validate that the rule class was mapped correctly
154+
actual_rule_classes = actual_rule_registry.rule_classes.map { |rule_class| rule_class.name }
155+
expect(actual_rule_classes.include?('ValidCustomRule')).to be true
156+
expect(actual_rule_classes.include?('ValidCustom2Rule')).to be true
157+
end
158+
end
91159
end
92160
end
93161
end

0 commit comments

Comments
 (0)