app/models/mdm/host.rb
# A system with an {#address IP address} on the network that has been discovered in some way.
class Mdm::Host < ApplicationRecord
extend ActiveSupport::Autoload
autoload :OperatingSystemNormalization
include Mdm::Host::OperatingSystemNormalization
include Metasploit::Model::Search
#
# CONSTANTS
#
# Special {#arch} value to indicate we should look at {#detected_arch}
# instead
UNKNOWN_ARCHITECTURE = 'Unknown'
# Either the CPU architecture for native code or the programming language
# name for exploits that run code in the programming language's virtual
# machine.
ARCHITECTURES = [
'armbe',
'armle',
'cbea',
'cbea64',
'cmd',
'java',
'mips',
'mipsbe',
'mipsle',
'php',
'ppc',
'ppc64',
'ruby',
'sparc',
'tty',
# To be used for compatability with 'X86_64'
'x64',
'x86',
'x86_64',
'',
UNKNOWN_ARCHITECTURE
]
# Fields searched for the search scope
SEARCH_FIELDS = [
'address::text',
'comments',
'mac',
'name',
'os_flavor',
'os_name',
'os_sp',
'purpose'
]
# Valid values for {#state}.
STATES = [
'alive',
'down',
'unknown'
]
# Valid MAC address value
#hyphen-separated: 1a-2B-3c-4D-5e-6f
MAC_ADDRESS_HYPHEN_REGEX = /\A(?:[A-F0-9]{2}[-]){5}[A-F0-9]{2}\z/i
#colon-separated: 1a:2B:3c:4D:5e:6f
MAC_ADDRESS_COLON_REGEX = /\A(?:[A-F0-9]{2}[:]){5}[A-F0-9]{2}\z/i
#XXX for now, allow en empty MAC so as not to break things that exist with
# empty MACs.
MAC_ADDRESS_EMPTY_REGEX = /\A\z/
# 6-tuple of hex (case-insensitive) doublets (or empty)
MAC_ADDRESS_REGEX = Regexp.union(MAC_ADDRESS_HYPHEN_REGEX, MAC_ADDRESS_COLON_REGEX,
MAC_ADDRESS_EMPTY_REGEX)
#
# Aggregations
#
# @!attribute [rw] address
# The IP address of this host. Necessary to avoid coercion to an `IPAddr` object.
#
# @return [String]
def address
self[:address].to_s
end
#
# Associations
#
# @!attribute [rw] clients
# Users connected to this host
#
# @return [ActiveRecord::Relation<Mdm::Client>]
has_many :clients,
class_name: 'Mdm::Client',
dependent: :destroy,
inverse_of: :host
# @!attribute events
# Events that occurred on this host.
#
# @return [ActiveRecord::Relation<Mdm::Event>]
has_many :events,
class_name: 'Mdm::Event',
dependent: :delete_all,
inverse_of: :host
# @!attribute [rw] task_hosts
# Details about what Tasks touched this host
#
# @return [ActiveRecord::Relation<Mdm::TaskHost>]
has_many :task_hosts,
class_name: 'Mdm::TaskHost',
dependent: :destroy,
inverse_of: :host
# @!attribute [rw] exploit_attempts
# Attempts to run exploits against this host.
#
# @return [ActiveRecord::Relation<Mdm::ExploitAttempt]
has_many :exploit_attempts,
class_name: 'Mdm::ExploitAttempt',
dependent: :destroy,
inverse_of: :host
# @!attribute exploited_hosts
# @todo MSP-2732
# @return [ActiveRecord::Relation<Mdm::ExploitedHost>]
has_many :exploited_hosts,
class_name: 'Mdm::ExploitedHost',
dependent: :destroy,
inverse_of: :host
# @!attribute [rw] host_details
# @return [ActiveRecord::Relation<Mdm::HostDetail>]
has_many :host_details,
class_name: 'Mdm::HostDetail',
dependent: :destroy,
inverse_of: :host
# @!attribute hosts_tags
# A join model between {Mdm::Tag} and {Mdm::Host}. Use {#tags} to get the actual {Mdm::Tag Mdm::Tags} on this host.
#
# @todo MSP-2723
# @return [ActiveRecord::Relation<Mdm::HostTag>]
has_many :hosts_tags,
class_name: 'Mdm::HostTag',
dependent: :destroy,
inverse_of: :host
# @!attribute loots
# Loot gathered from the host with {Mdm::Loot#created_at newest loot} first.
#
# @todo MSP-3065
# @return [ActiveRecord::Relation<Mdm::Loot>]
has_many :loots,
-> { order('loots.created_at DESC')},
class_name: 'Mdm::Loot',
dependent: :destroy,
inverse_of: :host
# @!attribute [rw] notes
# Notes about the host entered by a user with {Mdm::Note#created_at oldest notes} first.
#
# @return [ActiveRecord::Relation<Mdm::Note>]
has_many :notes,
-> { order('notes.created_at') },
class_name: 'Mdm::Note',
inverse_of: :host,
dependent: :delete_all
# @!attribute [rw] services
# The services running on {Mdm::Service#port ports} on the host with services ordered by {Mdm::Service#port port}
# and {Mdm::Service#proto protocol}.
#
# @return [ActiveRecord::Relation<Mdm::Service>]
has_many :services,
-> { order('services.port, services.proto') },
class_name: 'Mdm::Service',
dependent: :destroy,
inverse_of: :host
# @!attribute [rw] sessions
# Sessions that are open or previously were open on the host ordered by {Mdm::Session#opened_at when the session was
# opened}
#
# @return [ActiveRecord::Relation<Mdm::Session]
has_many :sessions,
-> { order('sessions.opened_at') },
class_name: 'Mdm::Session',
dependent: :destroy,
inverse_of: :host
# @!attribute [rw] vulns
# Vulnerabilities found on the host.
#
# @return [ActiveRecord::Relation<Mdm::Vuln>]
has_many :vulns,
class_name: 'Mdm::Vuln',
dependent: :delete_all,
inverse_of: :host
# @!attribute [rw] workspace
# The workspace in which this host was found.
#
# @return [Mdm::Workspace]
belongs_to :workspace,
class_name: 'Mdm::Workspace',
inverse_of: :hosts
#
# Through host_tags
#
# @!attribute [r] tags
# The tags on this host. Tags are used to filter hosts.
#
# @return [ActiveRecord::Relation<Mdm::Tag>]
# @see #hosts_tags
has_many :tags, :class_name => 'Mdm::Tag', :through => :hosts_tags
#
# Through services
#
# @!attribute [r] creds
# Credentials captured from {#services}.
#
# @return [ActiveRecord::Relation<Mdm::Cred>]
# @see #services
has_many :creds, :class_name => 'Mdm::Cred', :through => :services
# @!attribute [r] service_notes
# {Mdm::Note Notes} about {#services} running on this host.
#
# @return [ActiveRecord::Relation<Mdm::Note>]
# @see #services
has_many :service_notes,
class_name: 'Mdm::Note',
source: :notes,
through: :services
# @!attribute [r] web_sites
# {Mdm::WebSite Web sites} running on top of {#services} on this host.
#
# @return [ActiveRecord::Relation<Mdm::WebSite>]
# @see services
has_many :web_sites, :class_name => 'Mdm::WebSite', :through => :services
# @!attribute [r] module_runs
# Records of Metasploit modules being run on/against this {Mdm::Host}
#
# @return [ActiveRecord::Relation<MetasploitDataModels::ModuleRun>]
# @see services
has_many :module_runs,
class_name: 'MetasploitDataModels::ModuleRun',
as: :trackable
#
# through: :task_hosts
#
# @!attribute tasks
# Tasks that touched this service
#
# @return [ActiveRecord::Relation<Mdm::Task>]
has_many :tasks,
class_name: 'Mdm::Task',
through: :task_hosts
#
# Through vulns
#
# @!attribute [r] vuln_refs
# Join model between {#vulns} and {#refs}. Use either of those asssociations instead of this join model.
#
# @todo https://www.pivotaltracker.com/story/show/49004623
# @return [ActiveRecord::Relation<Mdm::VulnRef>]
# @see #refs
# @see #vulns
has_many :vuln_refs, :class_name => 'Mdm::VulnRef', :source => :vulns_refs, :through => :vulns
#
# Through vuln_refs
#
# @!attribute [r] refs
# External references, such as CVE, to vulnerabilities found on this host.
#
# @return [ActiveRecord::Relation<Mdm::Ref>]
# @see #vuln_refs
has_many :refs, :class_name => 'Mdm::Ref', :through => :vuln_refs
#
# Through refs
#
# @!attribute [r] module_refs
# {Mdm::Module::Ref References for modules} for {Mdm::Ref references for vulnerabilities}.
#
# @return [ActiveRecord::Relation<Mdm::Module::Ref>]
has_many :module_refs, :class_name => 'Mdm::Module::Ref', :through => :refs
#
# Through module_refs
#
# @!attribute [r] module_details
# {Mdm::Module::Detail Details about modules} that were used to find {#vulns vulnerabilities} on this host.
#
# @return [ActiveRecord::Relation<Mdm::Module::Detail]
has_many :module_details, -> { distinct } ,
:class_name => 'Mdm::Module::Detail',
:source =>:detail,
:through => :module_refs
#
# Attributes
#
# @!attribute [rw] address
# The IP address of this host.
#
# @return [String]
# @!attribute [rw] arch
# The architecture of the host's CPU OR the programming language for virtual machine programming language like
# Ruby, PHP, and Java.
#
# @return [String] an element of {ARCHITECTURES}
# @!attribute [rw] comm
# @todo https://www.pivotaltracker.com/story/show/49722411
#
# @return [String]
# @!attribute [rw] comments
# User supplied comments about host.
#
# @return [String]
# @!attribute [rw] created_at
# When this host was created in the database.
#
# @return [DateTime]
# @!attribute [rw] cred_count
# Counter cache for {#creds}.
#
# @return [Integer]
# @!attribute [rw] detected_arch
# The architecture of the host's CPU as detected by `Recog`. If {#arch} is
# not {UNKNOWN_ARCHITECTURE}, this is undefined.
#
# @return [String] a free-form string most likely from network data
# @!attribute [rw] exploit_attempt_count
# Counter cache for {#exploit_attempts}.
#
# @return [Integer]
# @!attribute [rw] host_detail_count
# Counter cache for {#host_details}.
#
# @return [Integer]
# @!attribute [rw] info
# Information about this host gathered from the host.
#
# @return [String]
# @!attribute [rw] mac
# The MAC address of this host.
#
# @return [String]
# @see http://en.wikipedia.org/wiki/Mac_address
# @!attribute [rw] name
# The name of the host. If the host name is not available, then it will just be the IP address.
#
# @return [String]
# @!attribute [rw] note_count
# Counter cache for {#notes}.
#
# @return [Integer]
# @!attribute [rw] os_flavor
# The flavor of {#os_name}.
#
# @example Windows XP
# host.os_name = 'Windows'
# host.os_flavor = 'XP'
#
# @return [String]
# @!attribute [rw] os_lang
# Free-form language of operating system. Usually either spelled out like 'English' or an
# {http://en.wikipedia.org/wiki/IETF_language_tag IETF language tag} like 'en' or 'en-US'.
#
# @return [String]
# @!attribute [rw] os_name
# The name of the operating system.
#
# @return [String]
# @!attribute [rw] os_sp
# The service pack of the {#os_flavor} of the {#os_name}.
#
# @example Windows XP SP2
# host.os_name = 'Windows'
# host.os_flavor = 'XP'
# host.os_sp = 'SP2'
#
# @return [String]
# @!attribute [rw] purpose
# The purpose of the host on the network, such as 'client' or 'firewall'.
#
# @return [String]
# @!attribute [rw] scope
# Interface identifier for link-local IPv6
#
# @return [String]
# @see http://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices
# @!attribute [rw] service_count
# Counter cache for {#services}.
#
# @return [Integer]
# @!attribute [rw] state
# Whether the host is alive, down, or in an unknown state.
#
# @return [String] element of {STATES}.
# @!attribute [rw] updated_at
# The last time this host was updated in the database.
#
# @return [DateTime]
# @!attribute [rw] virtual_host
# The name of the virtual machine host software, such as 'VMWare', 'QEMU', 'XEN', etc.
#
# @return [String]
# @!attribute [rw] vuln_count
# Counter cache for {#vulns}.
#
# @return [Integer]
#
# Callbacks
#
before_validation :normalize_arch
#
# Nested Attributes
# @note Must be declared after relations being referenced.
#
accepts_nested_attributes_for :services, :reject_if => lambda { |s| s[:port].blank? }, :allow_destroy => true
#
# Validations
#
validates :address,
:ip_format => true,
:presence => true,
:uniqueness => {
:scope => :workspace_id,
:unless => :ip_address_invalid?
}
validates :arch,
:allow_blank => true,
:inclusion => {
:in => ARCHITECTURES
}
validates :mac,
:format => {
:with => MAC_ADDRESS_REGEX,
:message => 'must be a valid MAC address'
}
validates :state,
:allow_nil => true,
:inclusion => {
:in => STATES
}
validates :workspace, :presence => true
#
# Scopes
#
scope :alive, -> { where({'hosts.state' => 'alive'}) }
scope :flagged, -> { where('notes.critical = true AND notes.seen = false').includes(:notes).references(:notes) }
scope :search,
lambda { |*args|
# @todo replace with AREL
terms = SEARCH_FIELDS.collect { |field|
"#{self.table_name}.#{field} ILIKE ?"
}
disjunction = terms.join(' OR ')
formatted_parameter = "%#{args[0]}%"
parameters = [formatted_parameter] * SEARCH_FIELDS.length
conditions = [disjunction] + parameters
where(*conditions)
}
scope :tag_search,
lambda { |*args| where("tags.name" => args[0]).includes(:tags).references(:tags) }
#
#
# Search
#
#
#
# Search Associations
#
search_association :services
#
# Search Attributes
#
search_attribute :name,
type: :string
search_attribute :os_flavor,
type: :string
search_attribute :os_name,
type: :string
search_attribute :os_sp,
type: :string
#
# Search Withs
#
search_with MetasploitDataModels::Search::Operator::Multitext,
name: :os,
operator_names: [
:os_name,
:os_flavor,
:os_sp
]
search_with MetasploitDataModels::Search::Operator::IPAddress,
attribute: :address
#
# Instance Methods
#
# Returns whether 'host.updated.<attr>' {#notes note} is {Mdm::Note#data locked}.
#
# @return [true] if Mdm::Note with 'host.updated.<attr>' as {Mdm::Note#name} exists and data[:locked] is `true`.
# @return [false] otherwise.
def attribute_locked?(attr)
n = notes.find_by_ntype("host.updated.#{attr}")
n && n.data[:locked]
end
# This is replicated by the IpAddressValidator class. Had to put it here as well to avoid
# SQL errors when checking address uniqueness.
#
# @return [void]
def ip_address_invalid?
begin
if address.is_a? IPAddr
potential_ip = address.dup
else
potential_ip = IPAddr.new(address)
end
return true unless potential_ip.ipv4? || potential_ip.ipv6?
rescue ArgumentError
return true
end
end
# Returns whether this host is a virtual machine.
#
# @return [true] unless {#virtual_host} is `nil`.
# @return [false] otherwise.
def is_vm?
!!self.virtual_host
end
private
def normalize_arch
if attribute_present?(:arch) && !ARCHITECTURES.include?(self.arch)
self.detected_arch = arch
self.arch = UNKNOWN_ARCHITECTURE
end
end
public
Metasploit::Concern.run(self)
end