class Sourcefire::Rapid7SourceFireConnector

Public Instance Methods

batch_single_ip(data_sets, asset, header, footer, max_data_size) click to toggle source
# File lib/sourcefire_connector.rb, line 175
def batch_single_ip(data_sets, asset, header, footer, max_data_size)
  split_asset = nil
  initial = true
  update_footer = "ScanUpdate"

  asset.each_line do |line|
    if split_asset.nil?
      split_asset = header.dup + line
    elsif (split_asset + line + footer).bytesize < max_data_size
      split_asset += line
    else
      data_sets << split_asset + (initial ? footer : update_footer)
      initial = false
      split_asset = header.dup
      redo
    end
  end
  data_sets << split_asset + update_footer
  data_sets << header.dup
end
connect_to_sourcefire() click to toggle source
# File lib/sourcefire_connector.rb, line 221
def connect_to_sourcefire()
  @log.log_message('Establishing connection to SourceFire...')
  p12_utils = Sourcefire::PkcsOps.new
  p12_utils.extract_pkcs_12(@config[:options][:p12_location], @config[:sourcefire_pkcs12_password])

  ctx = OpenSSL::SSL::SSLContext.new()
  ctx.cert = p12_utils.cert
  ctx.key = p12_utils.key

  @log.log_message('Parsed cert and key from pkcs file. Creating socket...')
  socket = TCPSocket.new(@config[:sourcefire_address],@config[:sourcefire_port])
  @log.log_message('Socket connection established. Initiating SSL handshake...')
  ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
  ssl_socket.sync_close = true
  ssl_socket.sync = true
  ssl_socket.connect

  @log.log_message('SSL connection established! Returning socket.')

  ssl_socket
end
generate_vuln_id(vuln_title) click to toggle source
# File lib/sourcefire_connector.rb, line 208
def generate_vuln_id(vuln_title)
  vuln_id = ''
  md5_title = Digest::MD5::hexdigest(vuln_title)
  md5_title[0..7].chars.map { |ch|
    if (ch != '0') && (ch.to_i == 0)
      vuln_id += (ch.ord - 'a'.ord + 1).to_s
    else
      vuln_id +=ch
    end
  }
  vuln_id
end
get_assets(report_file) click to toggle source
# File lib/sourcefire_connector.rb, line 75
def get_assets(report_file)
  assets = []
  current_asset = nil
  current_ip = nil

  CSV.foreach(report_file, headers: true) do |row|
    if current_asset.nil?
      current_asset = "AddHost,#{row['ip_address']}\n"
      current_asset << "SetOS,#{row['ip_address']},#{row['vendor']},#{row['name']},#{row['version']}\n"
      current_ip = row['ip_address']
    end

    if row['ip_address'] == current_ip
      sf_csv = ""
      sf_csv << "AddScanResult,#{row['ip_address']},\"NeXpose\",#{generate_vuln_id(row['nexpose_id'])},"
      (row['port'].to_i == -1) ? sf_csv << ',' : sf_csv << "#{row['port']},"
      (row['protocol_id'].to_i == -1) ? sf_csv << ',' : sf_csv << "#{row['protocol_id']},"
      sf_csv << "\"#{row['title'].tr('"', "'")}\","
      sf_csv << "\"NeXpose ID: #{row['nexpose_id'].tr('"', "'")}; References: #{row['references'] ? row['references'].scan(/<(.*?:.*?)>/).join(' ').downcase.tr('"', "'") : row['references']}; Severity: #{row['severity_score']}; PCI Severity: #{row['pci_severity_score']}; CVSS Score: #{row['cvss_score']}; CVSS Vector: (#{row['cvss_vector']})\","
      row['references'].nil? ?
        sf_csv << "\"cve_ids: \"," :
        sf_csv << "\"cve_ids: #{row['references'].scan(/<CVE:(.*?)>/).join(' ').tr('"', "'")}\","
      sf_csv << "\"bugtraq_ids: \"\n"
      
      current_asset += sf_csv
      next
    end   

    #Next asset
    assets << current_asset
    current_asset = nil
    current_asset_id = nil
    redo
  end
  
  assets << current_asset unless current_asset.nil?
  @log.log_message("Total of #{assets.count} assets")
  assets
end
process_nexpose_data(report_file) click to toggle source
# File lib/sourcefire_connector.rb, line 115
def process_nexpose_data(report_file)
  #Originally using the value returned from SourceFire as the max data size.
  #However, this caused issues with some ticket creation / batching, and the
  #reduced value was hard-coded below.
  #max_data_size = 524288
  max_data_size = 450000

  @log.log_message('Creating data sets')
  header = "SetSource,NeXpose Scan Report\n"
  footer = "ScanFlush"

  puts 'Processing vulnerability list.'
  assets = get_assets(report_file)

  ssl_socket = connect_to_sourcefire
  ssl_socket.write([2,4].pack('NN'))
  ssl_socket.write([1].pack('N'))
  msg_details = read_from_socket(ssl_socket)
  if msg_details.kind_of?(Array)
    @log.log_message("Got a message of type <#{msg_details[0]}> and size <#{msg_details[1]}>")
    max_size = read_from_socket(ssl_socket, msg_details[1], msg_details[0])
    @log.log_message("Max message length is <#{max_size}>")
    #max_data_size = max_size.first
  end
  ssl_socket.close

  data_sets = []
  current_data_set = nil

  assets.each do |asset|
    if data_sets[-1].nil?
      if (asset + footer).bytesize < max_data_size
        data_sets << header.dup + asset
      else
        batch_single_ip(data_sets, asset, header, footer, max_data_size)
      end
    elsif (data_sets[-1].to_s + asset + footer).bytesize < max_data_size
      data_sets[-1] += asset
    elsif (header.dup + asset + footer).bytesize < max_data_size
      data_sets[-1] += footer
      data_sets << header.dup + asset
    else
      data_sets[-1] += footer
      batch_single_ip(data_sets, asset, header, footer, max_data_size)
    end
  end

  if data_sets.count == 0
    @log.log_message("No data found. Returning <0> assets.")
    return []
  end

  #mark the overall scan finish
  data_sets[-1] = data_sets.last + footer

  @log.log_message("Number of batches to submit: #{data_sets.count}")
  puts 'Nexpose report processing complete.'
  data_sets
end
process_nexpose_data_alt(report_file) click to toggle source
# File lib/sourcefire_connector.rb, line 196
def process_nexpose_data_alt(report_file)
  max_data_size = 524288

  assets = get_assets(report_file)
  header = "SetSource,NeXpose Scan Report\n"
  footer = "ScanFlush"
  assets[0] = header + assets.first
  assets[-1] = assets.last + footer

  assets
end
read_from_socket(ssl_socket, read_size=nil, msg_type = nil) click to toggle source
# File lib/sourcefire_connector.rb, line 305
def read_from_socket(ssl_socket, read_size=nil, msg_type = nil)
  readable = IO.select([ssl_socket], nil, nil, 10)
  if readable.nil?
    @log.log_message('No response from server.') 
    return 
  end
  readable[0].each do |socket|
    next unless socket == ssl_socket

    if read_size.nil?
      data = ssl_socket.read_nonblock(10_000)
      type = data[0..3].unpack('N')
      read_size = data[4..7].unpack('N')
      return [type[0],read_size[0]]
    end

    begin
      data = ssl_socket.read_nonblock(read_size)
      return data.unpack('N') if msg_type == 1
      return data
    rescue IO::WaitReadable
      @log.log_message("Waiting for data of length <#{read_size}>")
      IO.select([ssl_socket], nil, nil, 10)
      retry
    end
  end
end
send_processed_data(data_sets, ssl_socket) click to toggle source
# File lib/sourcefire_connector.rb, line 243
def send_processed_data(data_sets, ssl_socket)
  overall_data_size = 0

  file = File.open("update_sets.csv", 'w')
  data_sets.each { |data| file.puts(data) }
  file.close

  @log.log_message('Starting to transmit data to SourceFire...')

  #Send the data
  number_of_commands = 0
  count = 0
  response = ''
  data_sets.each do |data|         
    count += 1
    
    #Inform SourceFire of the type of data.
    ssl_socket.write([2,4].pack('NN'))
    ssl_socket.write([1].pack('N'))

    #Send the data type and size
    ssl_socket.write([3,data.bytesize].pack('NN'))

    progress = "[#{((count-1)*100/Float(data_sets.count)).round(2)}%]"
    message_log = "Sending #{count.to_s.rjust(3, ' ')}/#{data_sets.count} #{progress}: Sending #{data.bytesize} bytes to socket."
    @log.log_message(message_log)
    print "\r#{message_log}"
    print 
    
    ssl_socket.write(data)
    ssl_socket.flush
    msg_details = read_from_socket(ssl_socket)
    
    if msg_details.kind_of?(Array)
      @log.log_message("Got a message of type <#{msg_details[0]}> and size <#{msg_details[1]}>")
      response = read_from_socket(ssl_socket, msg_details[1], msg_details[0])
      @log.log_message("Message is <#{response}>")
    end

    #get the response
    msg_details = read_from_socket(ssl_socket)
    response = ''
    if msg_details.kind_of?(Array)
      @log.log_message("Got a message of type <#{msg_details[0]}> and size <#{msg_details[1]}>")
      response = read_from_socket(ssl_socket, msg_details[1], msg_details[0])
      @log.log_message("Message is <#{response}>")
      response = response.kind_of?(Array) ? response.first : response.to_s
      current_number_of_commands = response.to_s.scan(/\d+/).first.to_s.to_i
      number_of_commands += current_number_of_commands
      if current_number_of_commands > 0
        @log.log_message("Sent #{current_number_of_commands} commands for latest batch to Sourcefire console.")
      end
    end
  end
  
  print "\rSent #{data_sets.count.to_s.rjust(3, ' ')}/#{data_sets.count} [100%]#{' '*40}"
  @log.log_message("Sent #{number_of_commands} commands total to Sourcefire console.")
  @log.log_message('Data transmission complete.')
  puts "\nProcessing complete."
  response
end
set_variables(options) click to toggle source
# File lib/sourcefire_connector.rb, line 66
def set_variables(options)
  options.each_key do |key|
    value = ENV[key.to_s.upcase]
    value ||= options[key]
    @log.log_message('No configuration value found for #{key}') if value.nil?
    @config[key] = value
  end
end
setup(config_options) click to toggle source
# File lib/sourcefire_connector.rb, line 11
def setup(config_options)
  @config = {}
  @log = Sourcefire::NxLogger.instance
  set_variables(config_options[:nexpose_options])
  set_variables(config_options[:sourcefire_options])
  @config[:options] = config_options[:options]

  if @config[:nexpose_address].nil? || @config[:nexpose_username].nil? || 
     @config[:nexpose_password].nil?
    raise 'Must configure Nexpose settings before starting'
  end

  if @config[:sourcefire_address].nil? || @config[:sourcefire_port].nil? || 
     @config[:sourcefire_pkcs12_password].nil?
    raise 'Must configure SourceFire settings before starting'
  end
end
start() click to toggle source
# File lib/sourcefire_connector.rb, line 29
def start
  puts "Nexpose Report Processing Starting"

  # Create a new Nexpose connection
  nxro = Sourcefire::ReportOps.new
  nxro.login(@config[:nexpose_address],@config[:nexpose_username],
             @config[:nexpose_password], @config[:options][:timeout],
             @config[:nexpose_port])
  
  #Generate the required data from Nexpose
  time = Time.now.to_i
  report_file = File.open("nexpose_report_#{time}.csv", 'w')
  puts "Site ID: #{@config[:options][:sites].join(', ')}"
  puts 'Generating report.'
  nxro.generate_sourcefire_nexpose_report(report_file, 
                                          @config[:options][:sites])
  puts 'Report generation complete.'

  #Process the Nexpose results.report("name:") { TESTS.times {  } }ort into SourceFire format
  data_sets = process_nexpose_data(report_file)

  #Establish connection with Sourcefire
  puts "Connecting to Sourcefire: #{@config[:sourcefire_address]}"
  ssl_socket = connect_to_sourcefire

  #Send the Data to SourceFire
  send_processed_data(data_sets, ssl_socket)

  #Cleanup
  ssl_socket.close
  if(@config[:options][:keep_csv] == 'N')
    File.delete(processed_report_file)
    File.delete(report_file)
  end
  @log.log_message('Finished processing. Exiting...')
end