More cleanup related to vuln schema
parent
42c3bedfad
commit
49b3c9b0e8
|
@ -1,35 +0,0 @@
|
|||
class AddVulnDetails < ActiveRecord::Migration
|
||||
|
||||
def self.up
|
||||
create_table :vuln_details do |t|
|
||||
t.integer :vuln_id # Vuln table reference
|
||||
t.float :cvss_score # 0.0 to 10.0
|
||||
t.string :cvss_vector # Ex: (AV:N/AC:L/Au:N/C:C/I:C/A:C)(AV:N/AC:L/Au:N/C:C/I:C/A:C)
|
||||
t.string :src # Source application ("nexpose")
|
||||
|
||||
t.string :title # Short identifier
|
||||
t.text :description # Plain text or HTML (trusted)
|
||||
t.text :solution # Plain text or HTML (trusted)
|
||||
t.binary :proof # Should be UTF-8, but may not be, sanitize on output
|
||||
# Technically this duplicates vuln.info, but that field
|
||||
# is poorly managed / handled today. Eventually we will
|
||||
# replace vuln.info
|
||||
|
||||
# Nexpose-specific fields
|
||||
t.integer :nx_console_id # NexposeConsole local table reference
|
||||
t.integer :nx_device_id # Reference from the Nexpose side
|
||||
t.string :nx_vuln_id # 'jre-java-update-flaw'
|
||||
t.float :nx_severity # 0-10
|
||||
t.float :nx_pci_severity # 0-10
|
||||
t.timestamp :nx_published # Normalized from "20081205T000000000"
|
||||
t.timestamp :nx_added # Normalized from "20081205T000000000"
|
||||
t.timestamp :nx_modified # Normalized from "20081205T000000000"
|
||||
t.text :nx_tags # Comma separated
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :vuln_details
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
class ExpandDetails < ActiveRecord::Migration
|
||||
|
||||
def self.up
|
||||
add_column :vuln_details, :nx_vuln_status, :text
|
||||
add_column :vuln_details, :nx_proof_key, :text
|
||||
add_column :vuln_details, :src, :string
|
||||
add_column :host_details, :src, :string
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :vuln_details, :nx_vuln_status
|
||||
remove_column :vuln_details, :nx_proof_key
|
||||
remove_column :vuln_details, :src
|
||||
remove_column :host_details, :src
|
||||
end
|
||||
end
|
|
@ -1345,7 +1345,9 @@ class DBManager
|
|||
|
||||
# Identify the associated service
|
||||
service = nil
|
||||
if opts[:port]
|
||||
|
||||
# Treat port zero as no service
|
||||
if opts[:port].to_i > 0
|
||||
proto = nil
|
||||
case opts[:proto].to_s.downcase # Catch incorrect usages, as in report_note
|
||||
when 'tcp','udp'
|
||||
|
@ -1362,18 +1364,22 @@ class DBManager
|
|||
|
||||
# Try to find an existing vulnerability with the same service & references
|
||||
# If there are multiple matches, choose the one with the most matches
|
||||
refs_ids = rids.map{|x| x.id }
|
||||
vuln = service.vulns.find(:all, :include => [:refs], :conditions => { 'refs.id' => refs_ids }).sort { |a,b|
|
||||
( refs_ids - a.refs.map{|x| x.id } ).length <=> ( refs_ids - b.refs.map{|x| x.id } ).length
|
||||
}.first
|
||||
if rids
|
||||
refs_ids = rids.map{|x| x.id }
|
||||
vuln = service.vulns.find(:all, :include => [:refs], :conditions => { 'refs.id' => refs_ids }).sort { |a,b|
|
||||
( refs_ids - a.refs.map{|x| x.id } ).length <=> ( refs_ids - b.refs.map{|x| x.id } ).length
|
||||
}.first
|
||||
end
|
||||
|
||||
else
|
||||
# Try to find an existing vulnerability with the same host & references
|
||||
# If there are multiple matches, choose the one with the most matches
|
||||
refs_ids = rids.map{|x| x.id }
|
||||
vuln = service.vulns.find(:all, :include => [:refs], :conditions => { 'service.id' => nil, 'refs.id' => refs_ids }).sort { |a,b|
|
||||
( refs_ids - a.refs.map{|x| x.id } ).length <=> ( refs_ids - b.refs.map{|x| x.id } ).length
|
||||
}.first
|
||||
if rids
|
||||
refs_ids = rids.map{|x| x.id }
|
||||
vuln = host.vulns.find(:all, :include => [:refs], :conditions => { 'service_id' => nil, 'refs.id' => refs_ids }).sort { |a,b|
|
||||
( refs_ids - a.refs.map{|x| x.id } ).length <=> ( refs_ids - b.refs.map{|x| x.id } ).length
|
||||
}.first
|
||||
end
|
||||
end
|
||||
|
||||
# No matches, so create a new vuln record
|
||||
|
@ -1398,16 +1404,16 @@ class DBManager
|
|||
vuln.refs << (rids - vuln.refs)
|
||||
end
|
||||
|
||||
# Handle vuln_details parameters
|
||||
if details
|
||||
report_vuln_details(vuln, details)
|
||||
end
|
||||
|
||||
# Finalize
|
||||
if vuln.changed?
|
||||
msf_import_timestamps(opts,vuln)
|
||||
vuln.save!
|
||||
end
|
||||
|
||||
# Handle vuln_details parameters
|
||||
report_vuln_details(vuln, details) if details
|
||||
|
||||
vuln
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -1454,35 +1460,44 @@ class DBManager
|
|||
#
|
||||
def report_vuln_details(vuln, details)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
detail = ::Mdm::VulnDetail.where(( details.delete(:key) || {} ).merge(:vuln_id => vuln.id))
|
||||
detail = ::Mdm::VulnDetail.where(( details.delete(:key) || {} ).merge(:vuln_id => vuln.id)).first
|
||||
if detail
|
||||
details.each_pair do |k,v|
|
||||
detail[k] = v
|
||||
end
|
||||
detail.save! if detail.changed?
|
||||
detail
|
||||
else
|
||||
detail = ::Mdm::VulnDetail.create(details.merge(:vuln_id => vuln.id))
|
||||
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Update vuln_details records en-masse based on specific criteria
|
||||
# Note that this *can* update data across workspaces
|
||||
#
|
||||
def update_vuln_details(details)
|
||||
criteria = details.delete(:key) || {}
|
||||
::Mdm::VulnDetail.update(key, details)
|
||||
end
|
||||
|
||||
#
|
||||
# Populate the host_details table with additional
|
||||
# information, matched by a specific criteria
|
||||
#
|
||||
def report_host_details(vuln, details)
|
||||
def report_host_details(host, details)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
|
||||
detail = ::Mdm::HostDetail.where(( details.delete(:key) || {} ).merge(:host_id => host.id))
|
||||
detail = ::Mdm::HostDetail.where(( details.delete(:key) || {} ).merge(:host_id => host.id)).first
|
||||
if detail
|
||||
details.each_pair do |k,v|
|
||||
detail[k] = v
|
||||
end
|
||||
detail.save! if detail.changed?
|
||||
detail
|
||||
else
|
||||
detail = ::Mdm::HostDetail.create(details.merge(:host_id => host.id))
|
||||
|
||||
end
|
||||
}
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require "rex/parser/nokogiri_doc_mixin"
|
||||
require "date"
|
||||
|
||||
module Rex
|
||||
module Parser
|
||||
|
@ -19,7 +20,7 @@ module Rex
|
|||
@state[:current_tag][name] = true
|
||||
case name
|
||||
when "nodes" # There are two main sections, nodes and VulnerabilityDefinitions
|
||||
@tests = []
|
||||
@tests = {}
|
||||
when "node"
|
||||
record_host(attrs)
|
||||
when "name"
|
||||
|
@ -33,6 +34,7 @@ module Rex
|
|||
when "os"
|
||||
record_os_fingerprint(attrs)
|
||||
when "test" # All the vulns tested for
|
||||
@state[:has_text] = true
|
||||
record_host_test(attrs)
|
||||
record_service_test(attrs)
|
||||
when "vulnerability"
|
||||
|
@ -40,6 +42,14 @@ module Rex
|
|||
when "reference"
|
||||
@state[:has_text] = true
|
||||
record_reference(attrs)
|
||||
when "description"
|
||||
@state[:has_text] = true
|
||||
when "solution"
|
||||
@state[:has_text] = true
|
||||
when "tag"
|
||||
@state[:has_text] = true
|
||||
when "tags"
|
||||
@state[:tags] = []
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -58,12 +68,16 @@ module Rex
|
|||
when "name"
|
||||
collect_hostname
|
||||
@state[:has_text] = false
|
||||
@text = nil
|
||||
when "endpoint"
|
||||
collect_service_data
|
||||
@state.delete(:cached_service_object)
|
||||
when "os"
|
||||
collect_os_fingerprints
|
||||
when "test"
|
||||
save_test
|
||||
report_test(&block)
|
||||
@state[:has_text] = false
|
||||
@text = nil
|
||||
when "vulnerability"
|
||||
collect_vuln_info
|
||||
report_vuln(&block)
|
||||
|
@ -72,6 +86,20 @@ module Rex
|
|||
@state[:has_text] = false
|
||||
collect_reference
|
||||
@text = nil
|
||||
when "description"
|
||||
@state[:has_text] = false
|
||||
collect_vuln_description
|
||||
@text = nil
|
||||
when "solution"
|
||||
@state[:has_text] = false
|
||||
collect_vuln_solution
|
||||
@text = nil
|
||||
when "tag"
|
||||
@state[:has_text] = false
|
||||
collect_tag
|
||||
@text = nil
|
||||
when "tags"
|
||||
@state.delete(:tags)
|
||||
end
|
||||
@state[:current_tag].delete name
|
||||
end
|
||||
|
@ -86,6 +114,29 @@ module Rex
|
|||
@state[:ref] = nil
|
||||
end
|
||||
|
||||
def collect_vuln_description
|
||||
return unless in_tag("description")
|
||||
return unless in_tag("vulnerability")
|
||||
return unless @state[:vuln]
|
||||
@report_data[:vuln_description] = @text.to_s.strip
|
||||
end
|
||||
|
||||
def collect_vuln_solution
|
||||
return unless in_tag("solution")
|
||||
return unless in_tag("vulnerability")
|
||||
return unless @state[:vuln]
|
||||
@report_data[:vuln_solution] = @text.to_s.strip
|
||||
end
|
||||
|
||||
def collect_tag
|
||||
return unless in_tag("tag")
|
||||
return unless in_tag("tags")
|
||||
return unless in_tag("vulnerability")
|
||||
return unless @state[:vuln]
|
||||
@report_data[:vuln_tags] ||= []
|
||||
@report_data[:vuln_tags] << @text.to_s.strip
|
||||
end
|
||||
|
||||
def collect_vuln_info
|
||||
return unless in_tag("VulnerabilityDefinitions")
|
||||
return unless in_tag("vulnerability")
|
||||
|
@ -101,49 +152,53 @@ module Rex
|
|||
return unless in_tag("VulnerabilityDefinitions")
|
||||
return unless @report_data[:vuln]
|
||||
return unless @report_data[:vuln][:matches].kind_of? Array
|
||||
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
|
||||
refs = normalize_references(@report_data[:vuln][:refs])
|
||||
refs << "NEXPOSE-#{report_data[:vuln]["id"]}"
|
||||
vuln_instances = @report_data[:vuln][:matches].size
|
||||
db.emit(:vuln, [refs.last,vuln_instances], &block) if block
|
||||
data = {
|
||||
:workspace => @args[:wspace],
|
||||
:name => refs.last,
|
||||
:info => @report_data[:vuln]["title"],
|
||||
:refs => refs.uniq
|
||||
}
|
||||
hosts_keys = {}
|
||||
@report_data[:vuln][:matches].each do |match|
|
||||
host_data = data.dup
|
||||
host_data[:host] = match[:host]
|
||||
host_data[:port] = match[:port] if match[:port]
|
||||
host_data[:proto] = match[:protocol] if match[:protocol]
|
||||
db_report(:vuln, host_data)
|
||||
if match[:key]
|
||||
hosts_keys[host_data[:host]] ||= []
|
||||
hosts_keys[host_data[:host]] << match[:key]
|
||||
end
|
||||
end
|
||||
report_key_note(hosts_keys,data)
|
||||
@report_data[:vuln] = nil
|
||||
end
|
||||
|
||||
def report_key_note(hosts_keys,data)
|
||||
return if hosts_keys.empty?
|
||||
hosts_keys.each do |key_host,key_values|
|
||||
key_note = {
|
||||
:workspace => @args[:wspace],
|
||||
:host => key_host,
|
||||
:type => "host.vuln.nexpose_keys",
|
||||
:data => {},
|
||||
:update => :unique_data
|
||||
}
|
||||
key_values.each do |key_value|
|
||||
key_note[:data][data[:name]] ||= []
|
||||
next if key_note[:data][data[:name]].include? key_value
|
||||
key_note[:data][data[:name]] << key_value
|
||||
vuln_ids = @report_data[:vuln][:matches].map{ |v| v[0] }
|
||||
vdet_ids = @report_data[:vuln][:matches].map{ |v| v[1] }
|
||||
|
||||
refs = refs.uniq.map{|x| db.find_or_create_ref(:name => x) }
|
||||
|
||||
# Assign title and references to all vuln_ids
|
||||
# Mass update fails due to the join table || ::Mdm::Vuln.where(:id => vuln_ids).update_all({ :name => @report_data[:vuln]["title"], :refs => refs } )
|
||||
vuln_ids.each do |vid|
|
||||
vuln = ::Mdm::Vuln.find(vid)
|
||||
next unless vuln
|
||||
vuln.name = @report_data[:vuln]["title"]
|
||||
vuln.refs = refs
|
||||
if vuln.changed?
|
||||
vuln.save!
|
||||
end
|
||||
db_report(:note, key_note)
|
||||
end
|
||||
|
||||
# Mass update vulnerability details across the database based on conditions
|
||||
vdet_info = { :title => @report_data[:vuln]["title"] }
|
||||
vdet_info[:description] = @report_data[:vuln_description] unless @report_data[:vuln_description].to_s.empty?
|
||||
vdet_info[:solution] = @report_data[:vuln_solution] unless @report_data[:vuln_solution].to_s.empty?
|
||||
vdet_info[:nx_tags] = @report_data[:vuln_tags].join(", ") if ( @report_data[:vuln_tags].kind_of?(::Array) and @report_data[:vuln_tags].length > 0 )
|
||||
vdet_info[:nx_severity] = @report_data[:vuln]["severity"].to_f if @report_data[:vuln]["severity"]
|
||||
vdet_info[:nx_pci_severity] = @report_data[:vuln]["pciSeverity"].to_f if @report_data[:vuln]["pciSeverity"]
|
||||
vdet_info[:cvss_score] = @report_data[:vuln]["cvssScore"].to_f if @report_data[:vuln]["cvssScore"]
|
||||
vdet_info[:cvss_vector] = @report_data[:vuln]["cvssVector"] if @report_data[:vuln]["cvssVector"]
|
||||
|
||||
%W{ published added modified }.each do |tf|
|
||||
next if not @report_data[:vuln][tf]
|
||||
ts = DateTime.parse(@report_data[:vuln][tf]) rescue nil
|
||||
next if not ts
|
||||
vdet_info[ "nx_#{tf}".to_sym ] = ts
|
||||
end
|
||||
|
||||
::Mdm::VulnDetail.where(:id => vdet_ids).update_all(vdet_info)
|
||||
|
||||
@report_data[:vuln] = nil
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
def record_reference(attrs)
|
||||
|
@ -155,22 +210,82 @@ module Rex
|
|||
def record_vuln(attrs)
|
||||
return unless in_tag("VulnerabilityDefinitions")
|
||||
vuln = attr_hash(attrs)
|
||||
matching_tests = @tests.select {|x| x[:id] == vuln["id"].downcase}
|
||||
matching_tests = @tests[ vuln["id"].downcase ]
|
||||
return unless matching_tests
|
||||
return if matching_tests.empty?
|
||||
@state[:vuln] = vuln
|
||||
@state[:vuln][:matches] = matching_tests
|
||||
end
|
||||
|
||||
def save_test
|
||||
# XML Export 2.0 includes additional test keys:
|
||||
# <test id="unix-unowned-files-or-dirs" status="vulnerable-exploited" scan-id="6381" vulnerable-since="20120322T124352665" pci-compliance-status="pass">
|
||||
|
||||
def report_test
|
||||
return unless in_tag("nodes")
|
||||
return unless in_tag("node")
|
||||
return unless @state[:test]
|
||||
test = { :id => @state[:test][:id]}
|
||||
test[:host] = @state[:address]
|
||||
test[:port] = @state[:test][:port] if @state[:test][:port]
|
||||
test[:protocol] = @state[:test][:protocol] if @state[:test][:protocol]
|
||||
test[:key] = @state[:test][:key] if @state[:test][:key]
|
||||
@tests << test
|
||||
|
||||
vuln_info = {
|
||||
:workspace => @args[:wspace],
|
||||
# This name will be overwritten during the vuln definition
|
||||
# parsing via mass-update.
|
||||
:name => "NEXPOSE-" + @state[:test][:id].downcase,
|
||||
:host => @state[:cached_host_object] || @state[:address]
|
||||
}
|
||||
|
||||
if @state[:cached_service_object]
|
||||
vuln_info[:service] = @state[:cached_service_object]
|
||||
else
|
||||
vuln_info[:port] = @state[:test][:port] if @state[:test][:port]
|
||||
vuln_info[:protocol] = @state[:test][:protocol] if @state[:test][:protocol]
|
||||
end
|
||||
|
||||
# This hash feeds a vuln_details row for this vulnerability
|
||||
vdet = { :src => 'nexpose', :nx_vuln_id => @state[:test][:id] }
|
||||
|
||||
# This hash defines the matching criteria to overwrite an existing entry
|
||||
vkey = { :src => 'nexpose', :nx_vuln_id => @state[:test][:id] }
|
||||
|
||||
if @state[:device_id]
|
||||
vdet[:nx_device_id] = @state[:device_id]
|
||||
vkey[:nx_device_id] = @state[:device_id]
|
||||
end
|
||||
|
||||
if @state[:test][:key]
|
||||
vdet[:nx_proof_key] = @state[:test][:key]
|
||||
vkey[:nx_proof_key] = @state[:test][:key]
|
||||
end
|
||||
|
||||
vdet[:nx_console_id] = @console_id if @console_id
|
||||
vdet[:nx_vuln_status] = @state[:test][:status] if @state[:test][:status]
|
||||
|
||||
proof = @text.to_s.strip
|
||||
vuln_info[:info] = proof
|
||||
vdet[:proof] = proof
|
||||
|
||||
# Configure the find key for vuln_details
|
||||
vdet[:key] = vkey
|
||||
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
|
||||
# Report the vulnerability
|
||||
vuln = db.report_vuln(vuln_info)
|
||||
|
||||
if vuln
|
||||
# Report the vulnerability details
|
||||
detail = db.report_vuln_details(vuln, vdet)
|
||||
|
||||
# Cache returned host and service objects if necessary
|
||||
@state[:cached_host_object] ||= vuln.host
|
||||
@state[:cached_service_object] ||= vuln.service if vuln.service
|
||||
|
||||
# Record the ID of this vuln for a future mass update that
|
||||
# brings in title, risk, description, solution, etc
|
||||
@tests[ @state[:test][:id].downcase ] ||= []
|
||||
@tests[ @state[:test][:id].downcase ] << [ vuln.id, detail.id ]
|
||||
end
|
||||
|
||||
}
|
||||
@state[:test] = nil
|
||||
end
|
||||
|
||||
|
@ -321,6 +436,7 @@ module Rex
|
|||
:protocol => @state[:service]["protocol"],
|
||||
}
|
||||
@state[:test][:key] = test["key"] if test["key"]
|
||||
@state[:test][:status] = test["status"] if test["status"]
|
||||
end
|
||||
|
||||
def record_host(attrs)
|
||||
|
@ -330,6 +446,7 @@ module Rex
|
|||
@state[:host_is_alive] = true
|
||||
@state[:address] = host_attrs["address"]
|
||||
@state[:mac] = host_attrs["hardware-address"] if host_attrs["hardware-address"]
|
||||
@state[:device_id] = host_attrs["device-id"] if host_attrs["device-id"]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -345,15 +462,26 @@ module Rex
|
|||
@report_data[:mac] = @state[:mac]
|
||||
end
|
||||
end
|
||||
|
||||
@report_data[:device_id] = @state[:device_id] if @state[:device_id]
|
||||
end
|
||||
|
||||
def report_host(&block)
|
||||
if host_is_okay
|
||||
db.emit(:address,@report_data[:host],&block) if block
|
||||
host_object = db_report(:host, @report_data.merge(
|
||||
:workspace => @args[:wspace] ) )
|
||||
device_id = @report_data.delete(:device_id)
|
||||
host_object = db_report(:host, @report_data.merge(:workspace => @args[:wspace] ) )
|
||||
if host_object
|
||||
db.report_import_note(host_object.workspace, host_object)
|
||||
if device_id
|
||||
detail = {
|
||||
:key => { :src => 'nexpose' },
|
||||
:src => 'nexpose',
|
||||
:nx_device_id => device_id
|
||||
}
|
||||
detail[:nx_console_id] = @nx_console_id if @nx_console_id
|
||||
db.report_host_details(host_object, detail)
|
||||
end
|
||||
end
|
||||
host_object
|
||||
end
|
||||
|
|
|
@ -149,7 +149,11 @@ module Parser
|
|||
[:host, :host_name, :mac, :workspace]
|
||||
when :vuln
|
||||
::Mdm::Vuln.new.attribute_names.map {|x| x.to_sym} |
|
||||
[:host, :refs, :workspace, :port, :proto]
|
||||
[:host, :refs, :workspace, :port, :proto, :details, :exploited_at]
|
||||
when :vuln_details
|
||||
::Mdm::VulnDetails.new.attribute_names.map {|x| x.to_sym} | [ :key ]
|
||||
when :host_details
|
||||
::Mdm::HostDetails.new.attribute_names.map {|x| x.to_sym} | [ :key ]
|
||||
when :note, :web_site, :web_page, :web_form, :web_vuln
|
||||
# These guys don't complain
|
||||
[:anything]
|
||||
|
|
Loading…
Reference in New Issue