|
| 1 | +require 'json' |
| 2 | +require 'csv' |
| 3 | +require 'heimdall_tools/hdf' |
| 4 | +require 'utilities/xml_to_hash' |
| 5 | +require 'nokogiri' |
| 6 | + |
| 7 | +RESOURCE_DIR = Pathname.new(__FILE__).join('../../data') |
| 8 | + |
| 9 | +# XCCDF mapping for converting SCAP client (SCC or OpenSCAP) outputs to HDF |
| 10 | +# SCC output from the RHEL7 Lockdown image was used for testing |
| 11 | + |
| 12 | +U_CCI_LIST = File.join(RESOURCE_DIR, 'U_CCI_List.xml') |
| 13 | + |
| 14 | +IMPACT_MAPPING = { |
| 15 | + critical: 0.9, |
| 16 | + high: 0.7, |
| 17 | + medium: 0.5, |
| 18 | + low: 0.3, |
| 19 | + na: 0.0 |
| 20 | +}.freeze |
| 21 | + |
| 22 | +# severity maps to high, medium, low with weights all being 10.0 from the xml |
| 23 | +# it doesn't really look like SCAP or SCC cares about that value, just if its high, med, or low |
| 24 | + |
| 25 | +CWE_REGEX = 'CWE-(\d*):'.freeze |
| 26 | +CCI_REGEX = 'CCI-(\d*)'.freeze |
| 27 | + |
| 28 | +DEFAULT_NIST_TAG = %w{SA-11 RA-5 Rev_4}.freeze |
| 29 | + |
| 30 | +module HeimdallTools |
| 31 | + class XCCDFResultsMapper |
| 32 | + def initialize(scap_xml, _name = nil) |
| 33 | + @scap_xml = scap_xml |
| 34 | + read_cci_xml |
| 35 | + begin |
| 36 | + data = xml_to_hash(scap_xml) |
| 37 | + @results = data['Benchmark']['TestResult'] |
| 38 | + @benchmarks = data['Benchmark'] |
| 39 | + @groups = data['Benchmark']['Group'] |
| 40 | + rescue StandardError => e |
| 41 | + raise "Invalid SCAP Client XCCDF output XML file provided Exception: #{e}" |
| 42 | + end |
| 43 | + end |
| 44 | + |
| 45 | + # change for pass/fail based on output Benchmark.rule |
| 46 | + # Pass/Fail are the only two options included in the output file |
| 47 | + def finding(issue, count) |
| 48 | + finding = {} |
| 49 | + finding['status'] = issue['rule-result'][count]['result'].to_s |
| 50 | + if finding['status'] == 'pass' |
| 51 | + finding['status'] = 'passed' |
| 52 | + end |
| 53 | + if finding['status'] == 'fail' |
| 54 | + finding['status'] = 'failed' |
| 55 | + end |
| 56 | + finding['code_desc'] = NA_STRING |
| 57 | + finding['run_time'] = NA_FLOAT |
| 58 | + finding['start_time'] = issue['start-time'] |
| 59 | + finding['message'] = NA_STRING |
| 60 | + finding['resource_class'] = NA_STRING |
| 61 | + [finding] |
| 62 | + end |
| 63 | + |
| 64 | + def read_cci_xml |
| 65 | + @cci_xml = Nokogiri::XML(File.open(U_CCI_LIST)) |
| 66 | + @cci_xml.remove_namespaces! |
| 67 | + rescue StandardError => e |
| 68 | + puts "Exception: #{e.message}" |
| 69 | + end |
| 70 | + |
| 71 | + def cci_nist_tag(cci_refs) |
| 72 | + nist_tags = [] |
| 73 | + cci_refs.each do |cci_ref| |
| 74 | + item_node = @cci_xml.xpath("//cci_list/cci_items/cci_item[@id='#{cci_ref}']")[0] unless @cci_xml.nil? |
| 75 | + unless item_node.nil? |
| 76 | + nist_ref = item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@index').text |
| 77 | + end |
| 78 | + nist_tags << nist_ref |
| 79 | + end |
| 80 | + nist_tags |
| 81 | + end |
| 82 | + |
| 83 | + def get_impact(severity) |
| 84 | + IMPACT_MAPPING[severity.to_sym] |
| 85 | + end |
| 86 | + |
| 87 | + def parse_refs(refs) |
| 88 | + refs.map { |ref| ref['text'] if ref['text'].match?(CCI_REGEX) }.reject!(&:nil?) |
| 89 | + end |
| 90 | + |
| 91 | + # Clean up output by removing the Satsifies block and the end of the description |
| 92 | + def satisfies_parse(satisf) |
| 93 | + temp_satisf = satisf.match('Satisfies: ([^;]*)<\/VulnDiscussion>') |
| 94 | + return temp_satisf[1].split(',') unless temp_satisf.nil? |
| 95 | + |
| 96 | + NA_ARRAY |
| 97 | + end |
| 98 | + |
| 99 | + def desc_tags(data, label) |
| 100 | + { data: data || NA_STRING, label: label || NA_STRING } |
| 101 | + end |
| 102 | + |
| 103 | + def collapse_duplicates(controls) |
| 104 | + unique_controls = [] |
| 105 | + |
| 106 | + controls.map { |x| x['id'] }.uniq.each do |id| |
| 107 | + collapsed_results = controls.select { |x| x['id'].eql?(id) }.map { |x| x['results'] } |
| 108 | + unique_control = controls.find { |x| x['id'].eql?(id) } |
| 109 | + unique_control['results'] = collapsed_results.flatten |
| 110 | + unique_controls << unique_control |
| 111 | + end |
| 112 | + unique_controls |
| 113 | + end |
| 114 | + |
| 115 | + def to_hdf |
| 116 | + controls = [] |
| 117 | + @groups.each_with_index do |group, i| |
| 118 | + @item = {} |
| 119 | + @item['id'] = group['Rule']['id'].split('.').last.split('_').drop(2).first.split('r').first.split('S')[1] |
| 120 | + @item['title'] = group['Rule']['title'].to_s |
| 121 | + @item['desc'] = group['Rule']['description'].to_s.split('Satisfies').first |
| 122 | + @item['descriptions'] = [] |
| 123 | + @item['descriptions'] << desc_tags(group['Rule']['description'], 'default') |
| 124 | + @item['descriptions'] << desc_tags('NA', 'rationale') |
| 125 | + @item['descriptions'] << desc_tags(group['Rule']['check']['check-content-ref']['name'], 'check') |
| 126 | + @item['descriptions'] << desc_tags(group['Rule']['fixtext']['text'], 'fix') |
| 127 | + @item['impact'] = get_impact(group['Rule']['severity']) |
| 128 | + @item['refs'] = NA_ARRAY |
| 129 | + @item['tags'] = {} |
| 130 | + @item['tags']['severity'] = nil |
| 131 | + @item['tags']['gtitle'] = group['title'] |
| 132 | + @item['tags']['satisfies'] = satisfies_parse(group['Rule']['description']) |
| 133 | + @item['tags']['gid'] = group['Rule']['id'].split('.').last.split('_').drop(2).first.split('r').first |
| 134 | + @item['tags']['legacy_id'] = group['Rule']['ident'][2]['text'] |
| 135 | + @item['tags']['rid'] = group['Rule']['ident'][1]['text'] |
| 136 | + @item['tags']['stig_id'] = @benchmarks['id'] |
| 137 | + @item['tags']['fix_id'] = group['Rule']['fix']['id'] |
| 138 | + @item['tags']['cci'] = parse_refs(group['Rule']['ident']) |
| 139 | + @item['tags']['nist'] = cci_nist_tag(@item['tags']['cci']) |
| 140 | + @item['code'] = NA_STRING |
| 141 | + @item['source_location'] = NA_HASH |
| 142 | + # results were in another location and using the top block "Benchmark" as a starting point caused odd issues. This works for now for the results. |
| 143 | + @item['results'] = finding(@results, i) |
| 144 | + controls << @item |
| 145 | + end |
| 146 | + |
| 147 | + controls = collapse_duplicates(controls) |
| 148 | + results = HeimdallDataFormat.new(profile_name: @benchmarks['id'], |
| 149 | + version: @benchmarks['style'], |
| 150 | + duration: NA_FLOAT, |
| 151 | + title: @benchmarks['title'], |
| 152 | + maintainer: @benchmarks['reference']['publisher'], |
| 153 | + summary: @benchmarks['description'], |
| 154 | + license: @benchmarks['notice']['id'], |
| 155 | + copyright: @benchmarks['metadata']['creator'], |
| 156 | + copyright_email: '[email protected]', |
| 157 | + controls: controls) |
| 158 | + results.to_hdf |
| 159 | + end |
| 160 | + end |
| 161 | +end |
0 commit comments