diff --git a/lib/inspec_delta/commands/profile.rb b/lib/inspec_delta/commands/profile.rb index b14e03d..accc4c3 100644 --- a/lib/inspec_delta/commands/profile.rb +++ b/lib/inspec_delta/commands/profile.rb @@ -13,9 +13,30 @@ class Profile < Thor aliases: %w[-s --st], desc: 'The path to the stig file to apply to the profile.', required: true + method_option :rule_id, + aliases: '-r', + desc: 'Sets inspec_delta to use STIG Rule IDs (SV-XXXXXX) as the primary key for comparison between the benchmark and the profile. Set to false to use the Vuln ID (V-XXXXXX) as the comparator.', + type: :boolean, + default: true + def update prof = InspecDelta::Object::Profile.new(options[:profile_path]) - prof.update(options[:stig_file_path]) + prof.update(options[:stig_file_path], options[:rule_id]) + prof.format + end + + desc 'update_id', 'Relabel the controls in the profile with the updated IDs from the benchmark. Run this first if the profile uses the old-stlye V-XXXXXX IDs and you want to rename the files with the right IDs in a separate commit.' + method_option :profile_path, + aliases: %w[-p --pr], + desc: 'The path to the directory that contains the profile to modify.', + required: true + method_option :stig_file_path, + aliases: %w[-s --st], + desc: 'The path to the stig file to apply to the profile.', + required: true + def update_id + prof = InspecDelta::Object::Profile.new(options[:profile_path]) + prof.update_id(options[:stig_file_path]) prof.format end end diff --git a/lib/inspec_delta/objects/control.rb b/lib/inspec_delta/objects/control.rb index a3d9384..ab1ae3e 100644 --- a/lib/inspec_delta/objects/control.rb +++ b/lib/inspec_delta/objects/control.rb @@ -34,11 +34,28 @@ def initialize # # @return [control_string] String updated string with the changes from other def apply_updates(other) + apply_updates_id(other.id) apply_updates_title(other.title) apply_updates_desc(other.descriptions[:default]) apply_updates_impact(other.impact) apply_updates_tags(other) - @control_string + return { + :control_string => @control_string, + :id => other.id + } + end + + # Updates a string representation of a Control's ID with string substitutions + # Only updates the ID if the Other ID does not match the one in the Control + # + # @param [String] title - The title to be applied to the Control + def apply_updates_id(id) + wrap_length = MAX_LINE_LENGTH - WORD_WRAP_INDENT + + @control_string.sub!( + /(control\s+)(\S+)(\s+do)/, + "control \'#{id}\' do".word_wrap(wrap_length).indent(WORD_WRAP_INDENT) + ) end # Updates a string representation of a Control's title with string substitutions @@ -62,7 +79,6 @@ def apply_updates_desc(desc) return if desc.to_s.empty? wrap_length = MAX_LINE_LENGTH - WORD_WRAP_INDENT - @control_string.sub!( /desc\s+(((").*?(? :stig_id, 'fix_id' => :fix_id, 'cci' => :cci, + 'legacy' => :legacy, 'false_negatives' => :false_negatives, 'false_positives' => :false_positives, 'documentable' => :documentable, @@ -203,6 +235,7 @@ def self.from_ruby(ruby_control_path) # @return [string] severity if no match is found def self.impact(severity) { + 'none' => 0.0, 'low' => 0.3, 'medium' => 0.5, 'high' => 0.7 diff --git a/lib/inspec_delta/objects/profile.rb b/lib/inspec_delta/objects/profile.rb index a76abb0..8a35f41 100644 --- a/lib/inspec_delta/objects/profile.rb +++ b/lib/inspec_delta/objects/profile.rb @@ -31,14 +31,15 @@ def format # # @param [profile_path] String - path to the inspec profile's root directory. # @param [stig_file_path] String - The STIG file to be applied to profile. - def update(stig_file_path) + def update(stig_file_path, rule_id) raise StandardError, "STIG file at #{stig_file_path} not found" unless File.exist?(stig_file_path) - control_dir = "#{@profile_path}/controls" benchmark = InspecDelta::Parser::Benchmark.get_benchmark(stig_file_path) benchmark.each do |control_id, control| benchmark_control = InspecDelta::Object::Control.from_benchmark(control) - profile_control_path = File.join(File.expand_path(control_dir), "#{control_id}.rb") + control_filename = (rule_id || control[:legacy].nil? || control[:legacy].empty?) ? "#{control_id}.rb" : + "#{control[:legacy].select{ |x| x.start_with? ('V-') }.first}.rb" + profile_control_path = File.join(File.expand_path(control_dir), control_filename) if File.file?(profile_control_path) update_existing_control_file(profile_control_path, benchmark_control) else @@ -47,6 +48,34 @@ def update(stig_file_path) end end + # Updates ONLY the filenames of the profile's controls metadata with definitions from a STIG xml file + # + # @param [profile_path] String - path to the inspec profile's root directory. + # @param [stig_file_path] String - The STIG file to be applied to profile. + def update_id(stig_file_path) + raise StandardError, "STIG file at #{stig_file_path} not found" unless File.exist?(stig_file_path) + control_dir = "#{@profile_path}/controls" + benchmark = InspecDelta::Parser::Benchmark.get_benchmark(stig_file_path) + benchmark.each do |control_id, control| unless control[:legacy].nil? || control[:legacy].empty? + benchmark_control = InspecDelta::Object::Control.from_benchmark(control) + control_filename = "#{control[:legacy].select{ |x| x.start_with? ('V-') }.first}.rb" + profile_control_path = File.join(File.expand_path(control_dir), control_filename) + #require 'pry'; binding.pry + if File.file?(profile_control_path) + puts "Updating \"#{control_filename}\" ==> \"#{control[:id]}.rb\"" + updated_path = profile_control_path.sub( + /[^\/\\]+.rb/, + control[:id] + '.rb' + ) + system("cd #{@profile_path} && git mv #{profile_control_path} #{updated_path}") + #require 'pry'; binding.pry + end + end + end + puts "Done updating." + end + + # Updates a control file with the updates from the stig # # @param [profile_control_path] String - The location of the Inspec profile on disk @@ -54,7 +83,16 @@ def update(stig_file_path) def update_existing_control_file(profile_control_path, benchmark_control) profile_control = InspecDelta::Object::Control.from_ruby(profile_control_path) updated_control = profile_control.apply_updates(benchmark_control) - File.open(profile_control_path, 'w') { |f| f.puts updated_control } + updated_path = profile_control_path.sub( + /[^\/\\]+.rb/, + updated_control[:id] + '.rb' + ) + if updated_path != profile_control_path + #require 'pry'; binding.pry + system("cd #{@profile_path} && git mv #{profile_control_path} #{updated_path}") + profile_control_path = updated_path + end + File.open(profile_control_path, 'w') { |f| f.puts updated_control[:control_string] } end # Creates a control file with the string representation of the benchmark control diff --git a/lib/inspec_delta/parsers/benchmark.rb b/lib/inspec_delta/parsers/benchmark.rb index c3e5ef9..1a00cfb 100644 --- a/lib/inspec_delta/parsers/benchmark.rb +++ b/lib/inspec_delta/parsers/benchmark.rb @@ -24,12 +24,12 @@ def self.get_benchmark(file) g[:stig_title] = benchmark_title - g[:id] = b.id g[:gtitle] = b.title g[:description] = b.description g[:gid] = b.id rule = b.rule + g[:id] = rule.id.scan(/SV-[\d]+/).first g[:rid] = rule.id g[:severity] = rule.severity g[:stig_id] = rule.version @@ -62,6 +62,7 @@ def self.get_benchmark(file) g[:dc_type] = reference_group.dc_type g[:cci] = rule.idents.select { |t| t.start_with?('CCI-') } # XCCDFReader + g[:legacy] = rule.idents.select { |t| !t.start_with?('CCI-') } g[:fix] = rule.fixtext g[:fix_id] = rule.fix.id @@ -69,7 +70,7 @@ def self.get_benchmark(file) g[:check] = rule.check.content g[:check_ref_name] = rule.check.content_ref.name g[:check_ref] = rule.check.content_ref.href - [b.id, g] + [g[:id], g] end mapped_benchmark_group.to_h end