hammackj/risu

View on GitHub
lib/risu/parsers/nessus/nessus_sax_listener.rb

Summary

Maintainability
C
7 hrs
Test Coverage
# Copyright (c) 2010-2020 Jacob Hammack.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


require 'set'

ActiveRecord::Migration.verbose = false

module Risu
    module Parsers
        module Nessus

            # NessusSaxListener
            class NessusSaxListener
                include LibXML::XML::SaxParser::Callbacks

                attr_accessor :new_tags

                # An array of valid reference element names
                VALID_REFERENCES = Set.new(%w[
                    cpe bid see_also xref cve iava msft
                    osvdb cert edb-id rhsa secunia suse dsa
                    owasp cwe iavb iavt cisco-sa ics-alert
                    cisco-bug-id cisco-sr cert-vu vmsa apple-sa
                    icsa cert-cc msvr usn hp glsa freebsd tra
                ])

                # An array of valid host properties
                VALID_HOST_PROPERTIES = Set.new(%w[
                    HOST_END mac-address HOST_START operating-system host-ip host-fqdn netbios-name
                    local-checks-proto smb-login-used ssh-auth-meth ssh-login-used pci-dss-compliance
                    pci-dss-compliance: system-type bios-uuid pcidss:compliance:failed pcidss:compliance:passed
                    pcidss:deprecated_ssl pcidss:expired_ssl_certificate pcidss:high_risk_flaw pcidss:medium_risk_flaw
                    pcidss:reachable_db pcidss:www:xss pcidss:directory_browsing pcidss:known_credentials
                    pcidss:compromised_host:worm pcidss:obsolete_operating_system pcidss:dns_zone_transfer
                    pcidss:unprotected_mssql_db pcidss:obsolete_software pcidss:www:sql_injection pcidss:backup_files
                    traceroute-hop-0 traceroute-hop-1 traceroute-hop-2 operating-system-unsupported patch-summary-total-cves
                    pcidss:insecure_http_methods LastUnauthenticatedResults LastAuthenticatedResults cpe-0 cpe-1
                    cpe-2 cpe-3 Credentialed_Scan policy-used UnsupportedProduct:microsoft:windows_xp::sp2
                    UnsupportedProduct:microsoft:windows_xp UnsupportedProduct:microsoft:windows_2000 UnsupportedProduct
                    mcafee-epo-guid
                ])

                # An array of all valid elements expected during parsing
                VALID_ELEMENTS = VALID_REFERENCES \
                    + Set.new(%w[ReportItem plugin_version risk_factor
                    description cvss_base_score solution item plugin_output tag synopsis plugin_modification_date
                    FamilyName FamilyItem Status vuln_publication_date ReportHost HostProperties preferenceName
                    preferenceValues preferenceType fullName pluginId pluginName selectedValue selectedValue
                    name value preference plugin_publication_date cvss_vector patch_publication_date
                    NessusClientData_v2 Policy PluginName ServerPreferences policyComments policyName PluginItem
                    Report Family Preferences PluginsPreferences FamilySelection IndividualPluginSelection PluginId
                    pci-dss-compliance exploitability_ease cvss_temporal_vector exploit_framework_core cvss_temporal_score
                    exploit_available metasploit_name exploit_framework_canvas canvas_package exploit_framework_metasploit
                    plugin_type exploithub_sku exploit_framework_exploithub stig_severity plugin_name fname always_run
                    cm:compliance-info cm:compliance-actual-value cm:compliance-check-id cm:compliance-policy-value
                    cm:compliance-audit-file cm:compliance-check-name cm:compliance-result cm:compliance-output policyOwner
                    visibility script_version attachment policy_comments d2_elliot_name exploit_framework_d2_elliot
                    exploited_by_malware compliance cm:compliance-reference cm:compliance-see-also cm:compliance-solution
                    agent potential_vulnerability in_the_news exploited_by_nessus unsupported_by_vendor default_account
                ])

                # TODO: documentation. These are never used in the class
                VALID_HOST_PROPERTIES_REGEX = [
                    "patch-summary-cve-num", "patch-summary-cves", "patch-summary-txt", "cpe-\d+", "KB\d+"
                ]

                # Map of host properties to symbols for use with ActiveRecord
                #   interfaces
                #
                # These are the more commonly used host properties,
                #   mapping them here to store in the host table
                HOST_PROPERTIES_MAPPING = {
                    "HOST_END" => :end,
                    "mac-address" => :mac,
                    "HOST_START" => :start,
                    "operating-system" => :os,
                    "host-ip" => :ip,
                    "host-fqdn" => :fqdn,
                    "netbios-name" => :netbios
                }

                # Used to map element names to private methods so
                #   that the methods can be called when the parser
                #   encounters the opening of an element.
                #
                # {"ElementName" => :start_method_to_be_called}
                #
                # @example call #start_policy for a "Policy" element
                #   element = "Policy"
                #   send(DYNAMIC_START_METHOD_NAMES[element], element, attributes)
                #
                # @param element [String] the element name starting to be parsed
                # @param attributes [Array<Hash{Sring=>String}>] the array of
                #   key value pairs for the element that is starting to be parsed
                DYNAMIC_START_METHOD_NAMES = {
                    "Policy" => :start_policy,
                    "preference" => :start_preference,
                    "item" => :start_item,
                    "FamilyItem" => :start_family_item,
                    "PluginItem" => :start_plugin_item,
                    "Report" => :start_report,
                    "ReportHost" => :start_report_host,
                    "tag" => :start_tag,
                    "ReportItem" => :start_report_item,
                    "attachment" => :start_attachment
                }

                # @see DYNAMIC_START_MEHTOD_NAMES
                #
                # @note only one argument for the methods
                #
                # @example call #end_policy_name for a "policyName" element
                #   element = "policyName"
                #   send(DYNAMIC_END_METHOD_NAMES[element], element)
                #
                # @param element [String] the name of the element ending
                DYNAMIC_END_METHOD_NAMES = {
                    "policyName" => :end_policy_name,
                    "policyComments" => :end_policy_comments,
                    "policy_comments" => :end_policy_comments,
                    "policyOwner" => :end_policy_owner,
                    "visibility" => :end_visibility,
                    "preference" => :end_preference,
                    "item" => :end_item,
                    "FamilyItem" => :end_family_item,
                    "PluginItem" => :end_plugin_item,
                    "tag" => :end_tag,
                    "ReportItem" => :end_report_item,
                    "attachment" => :end_attachment
                }

                private_constant :DYNAMIC_END_METHOD_NAMES, :DYNAMIC_START_METHOD_NAMES,
                    :HOST_PROPERTIES_MAPPING, :VALID_HOST_PROPERTIES_REGEX, :VALID_HOST_PROPERTIES,
                    :VALID_ELEMENTS, :VALID_REFERENCES

                # vals tracks state for elements encountered during parsing
                def initialize
                    @vals = Hash.new
                    @new_tags = Array.new
                end

                # Callback for when the start of a XML element is reached
                #
                # @param element XML element
                # @param attributes Attributes for the XML element
                def on_start_element(element, attributes)
                    @tag = element
                    @vals[@tag] = ""

                    if !VALID_ELEMENTS.include?(element)
                        @new_tags << "New XML element detected: #{element}. Please report this at #{Risu::GITHUB}/issues/new or via email to #{Risu::EMAIL}"
                    end

                    if DYNAMIC_START_METHOD_NAMES.key?(element)
                        # Dynamic dispatch to private instance "policyComments"methods in the const hash
                        #   DYNAMIC_START_METHOD_NAMES where {"element" => :start_element}
                        send(DYNAMIC_START_METHOD_NAMES[element], element, attributes)
                    end
                end

                # Called when the inner text of a element is reached
                #
                # @param text
                def on_characters(text)
                    if @vals[@tag] == nil then
                        @vals[@tag] = text
                    else
                        @vals[@tag] << text
                    end
                end

                # Called when the end of the XML element is reached
                #
                # @param element
                def on_end_element(element)
                    @tag = nil

                    if DYNAMIC_END_METHOD_NAMES.key?(element)
                        # Dynamic dispatch to private instance methods in the const hash
                        #   DYNAMIC_END_METHOD_NAMES where {"element" => :end_element}
                        send(DYNAMIC_END_METHOD_NAMES[element], element)
                    elsif VALID_REFERENCES.include?(element)
                        end_valid_reference(element)
                    end
                end

                private

                # Dynamic dispatch start element methods
                def start_policy(_element, _attributes)
                    @policy = Risu::Models::Policy.create
                end

                def start_preference(_element, _attributes)
                    @sp = @policy.server_preferences.create
                end

                def start_item(_element, _attributes)
                    @item = @policy.plugins_preferences.create
                end

                def start_family_item(_element, _attributes)
                    @family = @policy.family_selections.create
                end

                def start_plugin_item(_element, _attributes)
                    @plugin_selection = @policy.individual_plugin_selections.create
                end

                def start_report(_element, attributes)
                    @report = @policy.reports.create(:name => attributes["name"])
                end

                def start_report_host(_element, attributes)
                    @rh = @report.hosts.create(:name => attributes["name"])
                end

                def start_tag(_, attributes)
                    @attr = nil
                    @hp = @rh.host_properties.create

                    if attributes["name"] =~ /[M|m][S|s]\d{2,}-\d{2,}/
                        @attr = attributes["name"]
                    #Ugly as fuck. Really this needs to be rewritten. Fuck.
                    elsif attributes['name'] =~ /
                                    (?:patch-summary-cve-num)
                                    | (?:patch-summary-cves)
                                    | (?:patch-summary-txt)
                                    | (?:cpe-\d+)
                                    | (?:KB\d+)
                                /x
                        @attr = attributes["name"]
                    elsif VALID_HOST_PROPERTIES.include?(attributes["name"])
                        @attr = attributes["name"]
                    end

                    # implicit nil check?
                    if attributes["name"] !~ /(netstat-(?:established|listen)-(?:tcp|udp)\d+-\d+)/ \
                            && attributes["name"] !~ /traceroute-hop-\d+/ \
                            && @attr.nil?
                        @new_tags << "New HostProperties attribute: #{attributes["name"]}. Please report this at #{Risu::GITHUB}/issues/new or via email to #{Risu::EMAIL}\n"
                    end
                end

                def start_report_item(_element, attributes)
                    @vals = Hash.new # have to clear this out or everything has the same references

                    if attributes["pluginID"] == "0"
                        @plugin = Risu::Models::Plugin.find_or_create_by(:id => 1)
                    else
                        @plugin = Risu::Models::Plugin.find_or_create_by(:id => attributes["pluginID"]) do |plugin|
                            plugin.plugin_name = attributes["pluginName"]
                            plugin.family_name = attributes["pluginFamily"]
                        end
                    end

                    @ri = @rh.items.create(:port => attributes["port"],
                                    :svc_name => attributes["svc_name"],
                                    :protocol => attributes["protocol"],
                                    :severity => attributes["severity"],
                                    :plugin_id => @plugin.id
                                )

                    @plugin.save
                end

                def start_attachment(_element, attributes)
                    @attachment = @ri.attachments.create(:name => attributes['name'], :ttype => attributes['type'])
                end

                # Dynamic dispatch end element methods
                def end_policy_name(_)
                    @policy.update(:name => @vals["policyName"])
                end

                def end_policy_comments(element)
                    return unless element == "policyComments" || element == "policy_comments"
                    @policy.update(:comments => @vals[element])
                end

                def end_policy_owner(_)
                    @policy.update(:owner => @vals["policyOwner"])
                end

                def end_visibility(_)
                    @policy.update(:visibility => @vals["visibility"])
                end

                def end_preference(_)
                    @sp.update(:name => @vals["name"], :value => @vals["value"])
                end

                    # This takes a really long time, there is about 34,000 pluginIDs in this
                    # field and it takes about 36 minutes to parse just this info =\
                    # lets pre-populate the plugins table with the known plugin_id's
                    #
                    # if @vals["name"] == "plugin_set"
                    #      @all_plugins = @vals["value"].split(";")
                    #
                    #      @all_plugins.each { |p|
                    #             @plug = Plugin.find_or_create_by_id(p)
                    #             @plug.save
                    #      }
                    # end
                def end_item(_)
                    @item.update(:plugin_name => @vals["pluginName"],
                        :plugin_id => @vals["pluginId"], :fullname => @vals["fullName"],
                        :preference_name => @vals["preferenceName"],
                        :preference_type => @vals["preferenceType"],
                        :preference_values => @vals["preferenceValues"],
                        :selected_values => @vals["selectedValue"])
                end

                def end_family_item(_)
                    @family.update( :family_name => @vals["FamilyName"],
                        :status => @vals["Status"])
                end

                def end_plugin_item(_)
                    @plugin_selection.update(:plugin_id => @vals["PluginId"],
                        :plugin_name => @vals["PluginName"],
                        :family => @vals["Family"], :status => @vals["Status"])
                end

                def end_tag(_)
                    return if @attr.nil?

                    if @attr =~ /[M|m][S|s]\d{2}-\d{2,}/
                        @patch = @rh.patches.create(:name => @attr, :value => @vals['tag'])
                    else
                        if HOST_PROPERTIES_MAPPING.key?(@attr)
                            @rh.update(HOST_PROPERTIES_MAPPING[@attr] => @vals["tag"].gsub("\n", ","))
                        end

                        @hp.update(:name => @attr, :value => @vals['tag'])
                    end
                end

                #We cannot handle the references in the same block as the rest of the ReportItem tag because
                #there tends to be more than of the different types of reference per ReportItem, this causes issue for a sax
                #parser. To solve this we do the references before the final plugin data, Valid references must be added
                #the VALID_REFERENCE set at the top to be parsed.
                def end_valid_reference(element)
                    @ref = @plugin.references.create(:reference_name => element, :value => @vals["#{element}"])
                end

                def end_report_item(_)
                    @ri.update(:plugin_output => @vals["plugin_output"],
                        :plugin_name => @vals["plugin_name"],
                        :cm_compliance_info => @vals["cm:compliance-info"],
                        :cm_compliance_actual_value => @vals["cm:compliance-actual-value"],
                        :cm_compliance_check_id => @vals["cm:compliance-check-id"],
                        :cm_compliance_policy_value => @vals["cm:compliance-policy-value"],
                        :cm_compliance_audit_file => @vals["cm:compliance-audit-file"],
                        :cm_compliance_check_name => @vals["cm:compliance-check-name"],
                        :cm_compliance_result => @vals["cm:compliance-result"],
                        :cm_compliance_output => @vals["cm:compliance-output"],
                        :cm_compliance_reference => @vals["cm:compliance-reference"],
                        :cm_compliance_see_also => @vals["cm:compliance-see-also" ],
                        :cm_compliance_solution => @vals["cm:compliance-solution"],
                        :rollup_finding => false
                    )

                    @plugin.update(:solution => @vals["solution"],
                        :risk_factor => @vals["risk_factor"],
                        :description => @vals["description"],
                        :plugin_publication_date => @vals["plugin_publication_date"],
                        :plugin_modification_date => @vals["plugin_modification_date"],
                        :synopsis => @vals["synopsis"],
                        :plugin_type => @vals["plugin_type"],
                        :cvss_vector => @vals["cvss_vector"],
                        :cvss_base_score => @vals["cvss_base_score"].to_f,
                        :vuln_publication_date => @vals["vuln_publication_date"],
                        :plugin_version => @vals["plugin_version"],
                        :cvss_temporal_score => @vals["cvss_temporal_score"],
                        :cvss_temporal_vector => @vals["cvss_temporal_vector"],
                        :exploitability_ease => @vals["exploitability_ease"],
                        :exploit_framework_core => @vals["exploit_framework_core"],
                        :exploit_available => @vals["exploit_available"] == "true",
                        :exploit_framework_metasploit => @vals["exploit_framework_metasploit"],
                        :metasploit_name => @vals["metasploit_name"],
                        :exploit_framework_canvas => @vals["exploit_framework_canvas"],
                        :canvas_package => @vals["canvas_package"],
                        :exploit_framework_exploithub => @vals["exploit_framework_exploithub"],
                        :exploithub_sku => @vals["exploithub_sku"],
                        :stig_severity => @vals["stig_severity"],
                        :fname => @vals["fname"],
                        :always_run => @vals["always_run"],
                        :script_version => @vals["script_version"],
                        :exploited_by_malware => @vals["exploited_by_malware"],
                        :compliance => @vals["compliance"],
                        :agent => @vals["agent"],
                        :in_the_news => @vals["in_the_news"]

                    )
                end

                def end_attachment(_)
                    @attachment.update(:ahash => @vals['attachment'])
                end
            end
        end
    end
end