diff --git a/lib/helpers/constants.rb b/lib/helpers/constants.rb index 5986fd0c2..5d80c6491 100644 --- a/lib/helpers/constants.rb +++ b/lib/helpers/constants.rb @@ -1,6 +1,7 @@ # datastore related global variables $datastore = {} $datastore_iterators = {} # keeps track of previous access to datastore elements datastorevariablename => prev_index_accessed +$cybok_coverage = [] # array of XML nodes ## FILE / DIR CONSTANTS ## @@ -55,8 +56,12 @@ SAMP_DBS_DIR = "#{ROOT_DIR}/lib/resources/sample_databases" LOCAL_PUPPET_DIR = "#{MODULES_DIR}build/puppet" SECGEN_FUNCTIONS_PUPPET_DIR = "#{MODULES_DIR}build/puppet/secgen_functions" -# Filename for flags +# Filename for flags, etc FLAGS_FILENAME = "flag_hints.xml" +CYBOK_FILENAME = "cybok.xml" +SPOILER_ADMIN_FILENAME = "spoiler_admin_pass" +IP_ADDRESSES_FILENAME = "IP_addresses.json" + ## PACKER CONSTANTS ## diff --git a/lib/objects/module.rb b/lib/objects/module.rb index 06eb5a4c2..64ae7ee3e 100644 --- a/lib/objects/module.rb +++ b/lib/objects/module.rb @@ -22,6 +22,7 @@ class Module attr_accessor :conflicts attr_accessor :requires + attr_accessor :cybok_coverage # a list of xml doc nodes attr_accessor :puppet_file attr_accessor :puppet_other_path attr_accessor :local_calc_file @@ -34,6 +35,7 @@ class Module self.module_type = module_type self.conflicts = [] self.requires = [] + self.cybok_coverage = [] self.attributes = {} self.output = [] self.write_to_module_with_id = write_output_variable = '' @@ -96,13 +98,17 @@ class Module def attributes_for_scenario_output attr_flattened = {} - attributes.each do |key, array| - unless "#{key}" == 'module_type' || "#{key}" == 'conflict' || "#{key}" == 'default_input' || "#{key}" == 'requires' - # creates a valid regexp that can match the original module - attr_flattened["#{key}"] = Regexp.escape(array.join('~~~')).gsub(/\n\w*/, '.*').gsub(/\\ /, ' ').gsub(/~~~/, '|') - end - end + # this alternative approach populates all the filters in the generated senario, + # but this means that changes to the metadata breaks the selection + # attributes.each do |key, array| + # unless "#{key}" == 'module_type' || "#{key}" == 'conflict' || "#{key}" == 'default_input' || "#{key}" == 'requires' + # # creates a valid regexp that can match the original module + # attr_flattened["#{key}"] = Regexp.escape(array.join('~~~')).gsub(/\n\w*/, '.*').gsub(/\\ /, ' ').gsub(/~~~/, '|') + # end + # end + # just specify modules by their path + attr_flattened["module_path"] = Regexp.escape(attributes["module_path"][0]) attr_flattened end diff --git a/lib/output/project_files_creator.rb b/lib/output/project_files_creator.rb index 91a8a97a9..7bead3a6e 100644 --- a/lib/output/project_files_creator.rb +++ b/lib/output/project_files_creator.rb @@ -2,10 +2,12 @@ require 'erb' require_relative '../helpers/constants.rb' require_relative 'xml_scenario_generator.rb' require_relative 'xml_marker_generator.rb' +require_relative 'xml_cybok_generator.rb' require_relative 'ctfd_generator.rb' require 'fileutils' require 'librarian' require 'zip/zip' +require 'json' class ProjectFilesCreator # Creates project directory, uses .erb files to create a report and the vagrant file that will be used @@ -127,16 +129,25 @@ class ProjectFilesCreator Print.std "Creating scenario definition file: #{xfile}" write_data_to_file(xml, xfile) + write_data_to_file(@systems.to_s, "#{@out_dir}/systems") + write_data_to_file(@scenario.to_s, "#{@out_dir}/scenario") + + # Create the marker xml file x2file = "#{@out_dir}/#{FLAGS_FILENAME}" - xml_marker_generator = XmlMarkerGenerator.new(@systems, @scenario, @time) xml = xml_marker_generator.output Print.std "Creating flags and hints file: #{x2file}" write_data_to_file(xml, x2file) - Print.std "Saving spoiler/admin records..." + # Create the CyBOK xml file + x3file = "#{@out_dir}/#{CYBOK_FILENAME}" + xml_cybok_generator = XmlCybokGenerator.new(@systems, @scenario, @time) + xml = xml_cybok_generator.output + Print.std "Creating flags and hints file: #{x3file}" + write_data_to_file(xml, x3file) + Print.std "Saving spoiler/admin records..." jfile = "#{@out_dir}/datastores" Print.std "Saving datastore records: #{jfile}" json = JSON.generate($datastore) @@ -145,14 +156,14 @@ class ProjectFilesCreator if $datastore.has_key? "IP_addresses" system_names = @systems.map { |system| system.name } system_ips = Hash[system_names.zip($datastore["IP_addresses"])] - jfile = "#{@out_dir}/IP_addresses.json" + jfile = "#{@out_dir}/#{IP_ADDRESSES_FILENAME}" Print.std "Saving IP addresses: #{jfile}" json = JSON.generate(system_ips) write_data_to_file(json, jfile) end if $datastore.has_key? "spoiler_admin_pass" - pfile = "#{@out_dir}/spoiler_admin_pass" + pfile = "#{@out_dir}/#{SPOILER_ADMIN_FILENAME}" Print.std "Saving spoiler/admin passwords: #{pfile}" pass_notes = $datastore["spoiler_admin_pass"].join("\n") write_data_to_file(pass_notes, pfile) diff --git a/lib/output/xml_cybok_generator.rb b/lib/output/xml_cybok_generator.rb new file mode 100644 index 000000000..5d3768078 --- /dev/null +++ b/lib/output/xml_cybok_generator.rb @@ -0,0 +1,30 @@ +require 'nokogiri' +# Convert systems objects into xml +class XmlCybokGenerator + + # @param [Object] systems the list of systems + # @param [Object] scenario the scenario file used to generate + # @param [Object] time the current time as a string + def initialize(systems, scenario, time) + @systems = systems + @scenario = scenario + @time = time + end + + # outputs a XML CyBOK file that can be used to track CyBOK + # even for randomised challenges, where CyBOK is defined per module + # @return [Object] xml string + def output + # $cybok_coverage starts with the cybok from the scenario, and then we also + # add all the cybok from modules that are selected + @systems.each { |system| + system.module_selections.each { |selected_module| + $cybok_coverage.push *selected_module.cybok_coverage + } + } + coverage = "" + $cybok_coverage.map { |c| "\n " + c.to_xml.gsub(/\R/, "\n ").gsub(/\t/, ' ') }.uniq.join("\n") + "\n " + "" + + doc = Nokogiri.XML(coverage) + doc.to_xml + end +end diff --git a/lib/output/xml_marker_generator.rb b/lib/output/xml_marker_generator.rb index a0fc84a88..2d5b6dc64 100644 --- a/lib/output/xml_marker_generator.rb +++ b/lib/output/xml_marker_generator.rb @@ -1,5 +1,5 @@ require 'nokogiri' - +require 'irb' # Convert systems objects into xml class XmlMarkerGenerator @@ -15,6 +15,7 @@ class XmlMarkerGenerator # outputs a XML marker file that can be used to mark flags and provide hints # @return [Object] xml string def output + @processed_hints = [] ns = { 'xmlns' => "http://www.github/cliffe/SecGen/marker", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", @@ -37,10 +38,16 @@ class XmlMarkerGenerator # flag has to be the only thing in the parameter string (not within some text) if output_value.match(/\Aflag{.*\z/) xml.challenge{ - xml.flag(output_value) system.module_selections.each { |search_module| if search_module.unique_id == selected_module.write_to_module_with_id + # special case check for flag that's fed into a parameter that isn't defined within the receiving module + if search_module.attributes["read_fact"].include? selected_module.write_output_variable + xml.flag(output_value) + else + Print.warn "Ignoring flag generated but fed into a fact that the module doesn't read: #{selected_module.write_to_module_with_id}.#{selected_module.write_output_variable} #{output_value}" + Print.warn "This likely isn't an issue, especially if fed into strings_to_pre_leak which doesn't always exist" + end module_hints(search_module, xml, system.module_selections) end } @@ -69,6 +76,10 @@ class XmlMarkerGenerator } end + if search_module.cybok_coverage&.size > 0 + add_cybok(search_module, xml) + end + case search_module.module_type when "vulnerability" case search_module.attributes['access'].first @@ -130,10 +141,22 @@ class XmlMarkerGenerator end def add_hint(hint_text, hint_id, hint_type, xml) - xml.hint { - xml.hint_text(hint_text) - xml.hint_type(hint_type) - xml.hint_id(hint_id) + # due to the nested structure of components a specific hint may lead to + # multiple next steps -- but we just record each hint once to simplify things + # without this condition, the same hint will appear multiple times + unless @processed_hints.include? hint_id + @processed_hints << hint_id + xml.hint { + xml.hint_text(hint_text) + xml.hint_type(hint_type) + } + end +end + +def add_cybok(search_module, xml) + xml.cybok_coverage { + # quick and dirty conversion of saved nodes back to tidy xml + xml << search_module.cybok_coverage.map { |c| "\n " + c.to_xml.gsub(/\R/, "\n ").gsub(/\t/, ' ') }.join("\n") + "\n " } end diff --git a/lib/readers/module_reader.rb b/lib/readers/module_reader.rb index a2acce99f..f70752c76 100644 --- a/lib/readers/module_reader.rb +++ b/lib/readers/module_reader.rb @@ -160,6 +160,11 @@ class ModuleReader < XMLReader new_module.requires.push(require) end + # for each CyBOK in the module -- we just store the xml node for later + doc.xpath("/#{module_type}/CyBOK").each do |cybok_doc| + new_module.cybok_coverage.push(cybok_doc.clone) + end + # for each default input doc.xpath("/#{module_type}/default_input").each do |inputs_doc| inputs_doc.xpath('descendant::vulnerability | descendant::service | descendant::utility | descendant::network | descendant::base | descendant::encoder | descendant::generator').each do |module_node| @@ -219,4 +224,4 @@ class ModuleReader < XMLReader return modules end -end \ No newline at end of file +end diff --git a/lib/readers/system_reader.rb b/lib/readers/system_reader.rb index 4fd0bb64f..16f2d4f8e 100644 --- a/lib/readers/system_reader.rb +++ b/lib/readers/system_reader.rb @@ -16,6 +16,11 @@ class SystemReader < XMLReader # Parse and validate the schema doc = parse_doc(scenario_file, SCENARIO_SCHEMA_FILE, 'scenario') + # for each CyBOK in the module + doc.xpath("/scenario/CyBOK").each do |cybok_doc| + $cybok_coverage.push(cybok_doc.clone) + end + doc.xpath('/scenario/system').each_with_index do |system_node, system_index| module_selectors = []