class HeimdallTools::NessusMapper

Public Class Methods

new(nessus_xml) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 40
def initialize(nessus_xml)
  @nessus_xml = nessus_xml
  read_cci_xml
  begin
    @cwe_nist_mapping = parse_mapper
    @data = xml_to_hash(nessus_xml)
    @reports = extract_report
    @scaninfo = extract_scaninfo
  rescue StandardError => e
    raise "Invalid Nessus XML file provided Exception: #{e}"
  end
end

Public Instance Methods

cci_nist_tag(cci_refs) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 121
def cci_nist_tag(cci_refs)
  nist_tags = []
  cci_refs.each do |cci_ref|
    item_node = @cci_xml.xpath("//cci_list/cci_items/cci_item[@id='#{cci_ref}']")[0] unless @cci_xml.nil?
    unless item_node.nil?
      nist_ref = item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@index').text
    end
    nist_tags << nist_ref
  end
  nist_tags
end
collapse_duplicates(controls) click to toggle source

Nessus report could have multiple issue entries for multiple findings of same issue type. The meta data is identical across entries method collapse_duplicates return unique controls with applicable findings collapsed into it.

# File lib/heimdall_tools/nessus_mapper.rb, line 172
def collapse_duplicates(controls)
  unique_controls = []

  controls.map { |x| x['id'] }.uniq.each do |id|
    collapsed_results = controls.select { |x| x['id'].eql?(id) }.map { |x| x['results'] }
    unique_control = controls.find { |x| x['id'].eql?(id) }
    unique_control['results'] = collapsed_results.flatten
    unique_controls << unique_control
  end
  unique_controls
end
desc_tags(data, label) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 165
def desc_tags(data, label)
  { data: data || NA_STRING, label: label || NA_STRING }
end
extract_report() click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 53
def extract_report
  # When there are multiple hosts in the nessus report ReportHost field is an array
  # When there is only one host in the nessus report ReportHost field is a hash
  # Array() converts ReportHost to array in case there is only one host
  reports = @data['NessusClientData_v2']['Report']['ReportHost']
  reports.is_a?(Array) ? reports : [reports]
rescue StandardError => e
  raise "Invalid Nessus XML file provided Exception: #{e}"
end
extract_scaninfo() click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 67
def extract_scaninfo
  policy = @data['NessusClientData_v2']['Policy']
  info = {}

  info['policyName'] = policy['policyName']
  scanner_version = policy['Preferences']['ServerPreferences']['preference'].select { |x| x['name'].eql? 'sc_version' }
  info['version'] = scanner_version.empty? ? NA_STRING : scanner_version.first['value']
  info
rescue StandardError => e
  raise "Invalid Nessus XML file provided Exception: #{e}"
end
extract_timestamp(report) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 79
def extract_timestamp(report)
  report['HostProperties']['tag'].select { |x| x['name'].eql? 'HOST_START' }.first['text']
rescue StandardError => e
  raise "Invalid Nessus XML file provided Exception: #{e}"
end
finding(issue, timestamp) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 93
def finding(issue, timestamp)
  finding = {}
  # if compliance-result field, this is a policy compliance result entry
  # nessus policy compliance result provides a pass/fail data
  # For non policy compliance  results are defaulted to failed
  if issue['compliance-result']
    finding['status'] = issue['compliance-result'].eql?('PASSED') ? 'passed' : 'failed'
  else
    finding['status'] = 'failed'
  end

  if issue['description']
    finding['code_desc'] = issue['description'].to_s || NA_PLUGIN_OUTPUT
  else
    finding['code_desc'] = issue['plugin_output'] || NA_PLUGIN_OUTPUT
  end
  finding['run_time'] = NA_FLOAT
  finding['start_time'] = timestamp
  [finding]
end
format_desc(issue) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 85
def format_desc(issue)
  desc = ''
  desc += "Plugin Family: #{issue['pluginFamily']}; "
  desc += "Port: #{issue['port']}; "
  desc += "Protocol: #{issue['protocol']};"
  desc
end
impact(severity) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 139
def impact(severity)
  # Map CAT levels and Plugin severity to HDF impact levels
  case severity
  when '0'
    IMPACT_MAPPING[:Info]
  when '1', 'III'
    IMPACT_MAPPING[:Low]
  when '2', 'II'
    IMPACT_MAPPING[:Medium]
  when '3', 'I'
    IMPACT_MAPPING[:High]
  when '4'
    IMPACT_MAPPING[:Critical]
  else
    -1
  end
end
parse_mapper() click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 157
def parse_mapper
  csv_data = CSV.read(NESSUS_PLUGINS_NIST_MAPPING_FILE, { encoding: 'UTF-8',
                                               headers: true,
                                               header_converters: :symbol,
                                               converters: :all })
  csv_data.map(&:to_hash)
end
parse_refs(refs, key) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 63
def parse_refs(refs, key)
  refs.split(',').map { |x| x.split('|')[1] if x.include?(key) }.compact
end
plugin_nist_tag(pluginfamily, pluginid) click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 133
def plugin_nist_tag(pluginfamily, pluginid)
  entries = @cwe_nist_mapping.select { |x| (x[:pluginfamily].eql?(pluginfamily) && (x[:pluginid].eql?('*') || x[:pluginid].eql?(pluginid.to_i))) && !x[:nistid].nil? }
  tags = entries.map { |x| [x[:nistid].split('|'), "Rev_#{x[:rev]}"] }
  tags.empty? ? DEFAULT_NIST_TAG : tags.flatten.uniq
end
read_cci_xml() click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 114
def read_cci_xml
  @cci_xml = Nokogiri::XML(File.open(U_CCI_LIST))
  @cci_xml.remove_namespaces!
rescue StandardError => e
  puts "Exception: #{e.message}"
end
to_hdf() click to toggle source
# File lib/heimdall_tools/nessus_mapper.rb, line 184
def to_hdf
  host_results = {}
  @reports.each do |report|
    controls = []
    report['ReportItem'].each do |item|
      printf("\rProcessing: %s", $spinner.next)
      @item = {}
      @item['tags']               = {}
      @item['descriptions']       = []
      @item['refs']               = NA_ARRAY
      @item['source_location']    = NA_HASH

      # Nessus results field set are different for 'Policy Compliance' plug-in family vs other plug-in families
      # Following if conditions capture compliance* if it exists else it will default to plugin* fields
      # Current version covers STIG based 'Policy Compliance' results
      # TODO Cover cases for 'Policy Compliance' results based on CIS
      if item['compliance-reference']
        @item['id']                 = parse_refs(item['compliance-reference'], 'Vuln-ID').join.to_s
      else
        @item['id']                 = item['pluginID'].to_s
      end
      if item['compliance-check-name']
        @item['title']              = item['compliance-check-name'].to_s
      else
        @item['title']              = item['pluginName'].to_s
      end
      if item['compliance-info']
        @item['desc']              = item['compliance-info'].to_s
      else
        @item['desc']              = format_desc(item).to_s
      end
      if item['compliance-reference']
        @item['impact']            = impact(parse_refs(item['compliance-reference'], 'CAT').join.to_s)
      else
        @item['impact']            = impact(item['severity'])
      end
      if item['compliance-reference']
        @item['tags']['nist']     = cci_nist_tag(parse_refs(item['compliance-reference'], 'CCI'))
        @item['tags']['cci']      = parse_refs(item['compliance-reference'], 'CCI')
        @item['tags']['rid']      = parse_refs(item['compliance-reference'], 'Rule-ID').join(',')
        @item['tags']['stig_id']  = parse_refs(item['compliance-reference'], 'STIG-ID').join(',')
      else
        @item['tags']['nist']     = plugin_nist_tag(item['pluginFamily'], item['pluginID'])
        @item['tags']['rid']      = item['pluginID'].to_s
      end
      if item['compliance-solution']
        @item['descriptions']       << desc_tags(item['compliance-solution'], 'check')
      end

      @item['code']               = ''
      @item['results']            = finding(item, extract_timestamp(report))
      controls << @item
    end
    controls = collapse_duplicates(controls)
    results = HeimdallDataFormat.new(profile_name: "Nessus #{@scaninfo['policyName']}",
                                     version: @scaninfo['version'],
                                     title: "Nessus #{@scaninfo['policyName']}",
                                     summary: "Nessus #{@scaninfo['policyName']}",
                                     controls: controls,
                                     target_id: report['name'])
    host_results[report['name']] = results.to_hdf
  end
  host_results
end