dradis-nessus 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ require 'dradis-plugins'
2
+
3
+ # Require engine
4
+ require 'dradis/plugins/nessus'
5
+
6
+
7
+ # Require supporting Nessus library
8
+ require 'nessus/host'
9
+ require 'nessus/report_item'
@@ -0,0 +1,11 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nessus
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'dradis/plugins/nessus/engine'
9
+ require 'dradis/plugins/nessus/field_processor'
10
+ require 'dradis/plugins/nessus/importer'
11
+ require 'dradis/plugins/nessus/version'
@@ -0,0 +1,24 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nessus
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Dradis::Plugins::Nessus
6
+
7
+ include ::Dradis::Plugins::Base
8
+ description 'Processes Nessus XML v2 format (.nessus)'
9
+ provides :upload
10
+
11
+ # generators do
12
+ # require "path/to/my_railtie_generator"
13
+ # end
14
+
15
+ # Configuring the gem
16
+ # class Configuration < Core::Configurator
17
+ # configure :namespace => 'burp'
18
+ # setting :category, :default => 'Burp Scanner output'
19
+ # setting :author, :default => 'Burp Scanner plugin'
20
+ # end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nessus
4
+
5
+ class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
6
+
7
+ def post_initialize(args={})
8
+ @nessus_object = (data.name == 'ReportHost') ? ::Nessus::Host.new(data) : ::Nessus::ReportItem.new(data)
9
+ end
10
+
11
+ def value(args={})
12
+ field = args[:field]
13
+
14
+ # fields in the template are of the form <foo>.<field>, where <foo>
15
+ # is common across all fields for a given template (and meaningless).
16
+ _, name = field.split('.')
17
+
18
+ if name.end_with?('entries')
19
+ # report_item.bid_entries
20
+ # report_item.cve_entries
21
+ # report_item.xref_entries
22
+ entries = @nessus_object.try(name)
23
+ if entries.any?
24
+ entries.to_a.join("\n")
25
+ else
26
+ 'n/a'
27
+ end
28
+ else
29
+ output = @nessus_object.try(name) || 'n/a'
30
+
31
+ if field == 'report_item.description' && output =~ /^ -/
32
+ format_bullet_point_lists(output)
33
+ else
34
+ output
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+ def format_bullet_point_lists(input)
41
+ input.split("\n\n").map do |paragraph|
42
+ if paragraph =~ /^ - (.*)$/m
43
+ '* ' + $1.gsub(/ /, '').gsub(/\n/, ' ')
44
+ else
45
+ paragraph
46
+ end
47
+ end.join("\n\n")
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nessus
4
+ # Returns the version of the currently loaded Nessus as a <tt>Gem::Version</tt>
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 3
11
+ MINOR = 3
12
+ TINY = 0
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,178 @@
1
+ module Dradis::Plugins::Nessus
2
+ class Importer < Dradis::Plugins::Upload::Importer
3
+
4
+ # The framework will call this function if the user selects this plugin from
5
+ # the dropdown list and uploads a file.
6
+ # @returns true if the operation was successful, false otherwise
7
+ def import(params={})
8
+ file_content = File.read( params[:file] )
9
+
10
+ logger.info{'Parsing nessus output file...'}
11
+ doc = Nokogiri::XML( file_content )
12
+ logger.info{'Done.'}
13
+
14
+ if doc.xpath('/NessusClientData_v2/Report').empty?
15
+ error = "No reports were detected in the uploaded file (/NessusClientData_v2/Report). Ensure you uploaded a Nessus XML v2 (.nessus) report."
16
+ logger.fatal{ error }
17
+ content_service.create_note text: error
18
+ return false
19
+ end
20
+
21
+ doc.xpath('/NessusClientData_v2/Report').each do |xml_report|
22
+ report_label = xml_report.attributes['name'].value
23
+ logger.info{ "Processing report: #{report_label}" }
24
+ # No need to create a report node for each report. It may be good to
25
+ # create a plugin.output/nessus.reports with info for each scan, but
26
+ # for the time being we just append stuff to the Host
27
+ # report_node = parent.children.find_or_create_by_label(report_label)
28
+
29
+ xml_report.xpath('./ReportHost').each do |xml_host|
30
+ process_report_host(xml_host)
31
+ end #/ReportHost
32
+ logger.info{ "Report processed." }
33
+ end #/Report
34
+
35
+ return true
36
+ end # /import
37
+
38
+
39
+ private
40
+
41
+ # Internal: Parses the specific "Nessus SYN Scanner" and similar plugin into
42
+ # Dradis node properties.
43
+ #
44
+ # xml_host - The Nokogiri XML node representing the parent host for
45
+ # this issue.
46
+ # host_node - The Dradis Node that represents the host in the project.
47
+ # xml_report_item - The Nokogiri XML node representing the Service Detection
48
+ # <ReportItem> tag.
49
+ #
50
+ # Returns nothing.
51
+ #
52
+ # Plugins processed using this method:
53
+ # - [11219] Nessus SYN Scanner
54
+ # - [34220] Netstat Portscanner (WMI)
55
+ def process_nessus_syn_scanner(xml_host, host_node, xml_report_item)
56
+ port = xml_report_item['port'].to_i
57
+ protocol = xml_report_item['protocol']
58
+ logger.info{ "\t\t\t => Creating new service: #{protocol}/#{port}" }
59
+
60
+ host_node.set_property(:services, {
61
+ port: port,
62
+ protocol: protocol,
63
+ state: 'open',
64
+ name: xml_report_item['svc_name'],
65
+ x_nessus: xml_report_item.at_xpath('./plugin_output').text
66
+ })
67
+
68
+ host_node.save
69
+ end
70
+
71
+ # Internal: Process each /NessusClientData_v2/Report/ReportHost creating a
72
+ # Dradis node and adding some properties to it (:ip, :os, etc.).
73
+ #
74
+ # xml_host - The Nokogiri XML node representing the parent host for
75
+ # this issue.
76
+ #
77
+ # Returns nothing.
78
+ #
79
+ def process_report_host(xml_host)
80
+
81
+ # 1. Create host node
82
+ host_label = xml_host.attributes['name'].value
83
+ host_label += " (#{xml_host.attributes['fqdn'].value})" if xml_host.attributes['fqdn']
84
+
85
+ host_node = content_service.create_node(label: host_label, type: :host)
86
+ logger.info{ "\tHost: #{host_label}" }
87
+
88
+ # 2. Add host info note and host properties
89
+ host_note_text = template_service.process_template(template: 'report_host', data: xml_host)
90
+ content_service.create_note(text: host_note_text, node: host_node)
91
+
92
+ if host_node.respond_to?(:properties)
93
+ nh = ::Nessus::Host.new(xml_host)
94
+ host_node.set_property(:fqdn, nh.fqdn) if nh.try(:fqdn)
95
+ host_node.set_property(:ip, nh.ip) if nh.try(:ip)
96
+ host_node.set_property(:mac_address, nh.mac_address) if nh.try(:mac_address)
97
+ host_node.set_property(:netbios_name, nh.netbios_name) if nh.try(:netbios_name)
98
+ host_node.set_property(:os, nh.operating_system) if nh.try(:operating_system)
99
+ host_node.save
100
+ end
101
+
102
+
103
+ # 3. Add Issue and associated Evidence for this host/port combination
104
+ xml_host.xpath('./ReportItem').each do |xml_report_item|
105
+ case xml_report_item.attributes['pluginID'].value
106
+ when '0'
107
+ when '11219', '34220' # Nessus SYN scanner, Netstat Portscanner (WMI)
108
+ process_nessus_syn_scanner(xml_host, host_node, xml_report_item)
109
+ when '22964' # Service Detection
110
+ process_service_detection(xml_host, host_node, xml_report_item)
111
+ else
112
+ process_report_item(xml_host, host_node, xml_report_item)
113
+ end
114
+ end #/ReportItem
115
+ end
116
+
117
+ # Internal: Process each /NessusClientData_v2/Report/ReportHost/ReportItem
118
+ # and creates the corresponding Issue and Evidence in Dradis.
119
+ #
120
+ # xml_host - The Nokogiri XML node representing the parent host for
121
+ # this issue.
122
+ # host_node - The Dradis Node that represents the host in the project.
123
+ # xml_report_item - The Nokogiri XML node representing the Service Detection
124
+ # <ReportItem> tag.
125
+ #
126
+ # Returns nothing.
127
+ #
128
+ def process_report_item(xml_host, host_node, xml_report_item)
129
+ # 3.1. Add Issue to the project
130
+ plugin_id = xml_report_item.attributes['pluginID'].value
131
+ logger.info{ "\t\t\t => Creating new issue (plugin_id: #{plugin_id})" }
132
+
133
+ issue_text = template_service.process_template(template: 'report_item', data: xml_report_item)
134
+ issue_text << "\n\n#[Host]#\n#{xml_host.attributes['name']}\n\n"
135
+
136
+ issue = content_service.create_issue(text: issue_text, id: plugin_id)
137
+
138
+ # 3.2. Add Evidence to link the port/protocol and Issue
139
+ port_info = xml_report_item.attributes['protocol'].value
140
+ port_info += "/"
141
+ port_info += xml_report_item.attributes['port'].value
142
+
143
+ logger.info{ "\t\t\t => Adding reference to this host" }
144
+ evidence_content = template_service.process_template(template: 'evidence', data: xml_report_item)
145
+
146
+ content_service.create_evidence(issue: issue, node: host_node, content: evidence_content)
147
+
148
+ # 3.3. Compliance check information
149
+ end
150
+
151
+ # Internal: Parses the specific "Service Detection" plugin into Dradis node
152
+ # properties.
153
+ #
154
+ # xml_host - The Nokogiri XML node representing the parent host for
155
+ # this issue.
156
+ # host_node - The Dradis Node that represents the host in the project.
157
+ # xml_report_item - The Nokogiri XML node representing the Service Detection
158
+ # <ReportItem> tag.
159
+ #
160
+ # Returns nothing.
161
+ #
162
+ def process_service_detection(xml_host, host_node, xml_report_item)
163
+ port = xml_report_item['port'].to_i
164
+ protocol = xml_report_item['protocol']
165
+ logger.info{ "\t\t\t => Creating new service: #{protocol}/#{port}" }
166
+
167
+ host_node.set_property(:services, {
168
+ port: port,
169
+ protocol: protocol,
170
+ state: 'open',
171
+ name: xml_report_item['svc_name'],
172
+ x_nessus: xml_report_item.at_xpath('./description').text
173
+ })
174
+
175
+ host_node.save
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Dradis
4
+ module Plugins
5
+ module Nessus
6
+ # Returns the version of the currently loaded Nessus as a
7
+ # <tt>Gem::Version</tt>.
8
+ def self.version
9
+ gem_version
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ module Nessus
2
+ # This class represents each of the /NessusClientData_v2/Report/ReportHost
3
+ # elements in the Nessus XML document.
4
+ #
5
+ # It provides a convenient way to access the information scattered all over
6
+ # the XML in attributes and nested tags.
7
+ #
8
+ # Instead of providing separate methods for each supported property we rely
9
+ # on Ruby's #method_missing to do most of the work.
10
+ class Host
11
+ # Accepts an XML node from Nokogiri::XML.
12
+ def initialize(xml_node)
13
+ @xml = xml_node
14
+ end
15
+
16
+ # List of supported tags. They are all desdendents of the ./HostProperties
17
+ # node.
18
+ def supported_tags
19
+ [
20
+ # attributes
21
+ :name,
22
+
23
+ # simple tags
24
+ :ip, :fqdn, :operating_system, :mac_address, :netbios_name,
25
+ :scan_start_time, :scan_stop_time
26
+ ]
27
+ end
28
+
29
+ # Each of the entries associated with this host. Returns an array of
30
+ # Nessus::ReportItem objects
31
+ def report_items
32
+ @xml.xpath('./ReportItem').collect { |xml_report_item| ReportItem.new(xml_report_item) }
33
+ end
34
+
35
+ # This allows external callers (and specs) to check for implemented
36
+ # properties
37
+ def respond_to?(method, include_private=false)
38
+ return true if supported_tags.include?(method.to_sym)
39
+ super
40
+ end
41
+
42
+ # This method is invoked by Ruby when a method that is not defined in this
43
+ # instance is called.
44
+ #
45
+ # In our case we inspect the @method@ parameter and try to find the
46
+ # corresponding <tag/> element inside the ./HostProperties child.
47
+ def method_missing(method, *args)
48
+ # We could remove this check and return nil for any non-recognized tag.
49
+ # The problem would be that it would make tricky to debug problems with
50
+ # typos. For instance: <>.potr would return nil instead of raising an
51
+ # exception
52
+ unless supported_tags.include?(method)
53
+ super
54
+ return
55
+ end
56
+
57
+ # first we try the attributes: name
58
+ translations_table = {}
59
+ method_name = translations_table.fetch(method, method.to_s)
60
+ return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
61
+
62
+
63
+ # translation of Host properties
64
+ translations_table = {
65
+ ip: 'host-ip',
66
+ fqdn: 'host-fqdn',
67
+ operating_system: 'operating-system',
68
+ mac_address: 'mac-address',
69
+ netbios_name: 'netbios-name',
70
+ scan_start_time: 'HOST_START',
71
+ scan_stop_time: 'HOST_END'
72
+ }
73
+ method_name = translations_table.fetch(method, method.to_s)
74
+
75
+ if property = @xml.at_xpath("./HostProperties/tag[@name='#{method_name}']")
76
+ return property.text
77
+ else
78
+ return nil
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,118 @@
1
+ module Nessus
2
+ # This class represents each of the /NessusClientData_v2/Report/ReportHost/ReportItem
3
+ # elements in the Nessus XML document.
4
+ #
5
+ # It provides a convenient way to access the information scattered all over
6
+ # the XML in attributes and nested tags.
7
+ #
8
+ # Instead of providing separate methods for each supported property we rely
9
+ # on Ruby's #method_missing to do most of the work.
10
+ class ReportItem
11
+
12
+ # Accepts an XML node from Nokogiri::XML.
13
+ def initialize(xml_node)
14
+ @xml = xml_node
15
+ end
16
+
17
+ # List of supported tags. They can be attributes, simple descendans or
18
+ # collections (e.g. <bid/>, <cve/>, <xref/>)
19
+ def supported_tags
20
+ [
21
+ # attributes
22
+ :port, :svc_name, :protocol, :severity, :plugin_id, :plugin_name, :plugin_family,
23
+ # simple tags
24
+ :solution, :risk_factor, :description, :plugin_publication_date,
25
+ :metasploit_name, :cvss_vector, :cvss_temporal_vector, :synopsis,
26
+ :exploit_available, :patch_publication_date, :plugin_modification_date,
27
+ :cvss_temporal_score, :cvss_base_score, :plugin_output,
28
+ :plugin_version, :exploitability_ease, :vuln_publication_date,
29
+ :exploit_framework_canvas, :exploit_framework_metasploit,
30
+ :exploit_framework_core,
31
+ # multiple tags
32
+ :bid_entries, :cve_entries, :see_also_entries, :xref_entries,
33
+ # compliance tags
34
+ :cm_actual_value, :cm_audit_file, :cm_check_id, :cm_check_name, :cm_info,
35
+ :cm_output, :cm_policy_value, :cm_reference, :cm_result, :cm_see_also,
36
+ :cm_solution
37
+ ]
38
+ end
39
+
40
+ # This allows external callers (and specs) to check for implemented
41
+ # properties
42
+ def respond_to?(method, include_private=false)
43
+ return true if supported_tags.include?(method.to_sym)
44
+ super
45
+ end
46
+
47
+ # This method is invoked by Ruby when a method that is not defined in this
48
+ # instance is called.
49
+ #
50
+ # In our case we inspect the @method@ parameter and try to find the
51
+ # attribute, simple descendent or collection that it maps to in the XML
52
+ # tree.
53
+ def method_missing(method, *args)
54
+
55
+ # We could remove this check and return nil for any non-recognized tag.
56
+ # The problem would be that it would make tricky to debug problems with
57
+ # typos. For instance: <>.potr would return nil instead of raising an
58
+ # exception
59
+ unless supported_tags.include?(method)
60
+ super
61
+ return
62
+ end
63
+
64
+ # first we try the attributes: port, svc_name, protocol, severity,
65
+ # plugin_id, plugin_name, plugin_family
66
+ translations_table = {
67
+ # @port = xml.attributes["port"]
68
+ # @svc_name = xml.attributes["svc_name"]
69
+ # @protocol = xml.attributes["protocol"]
70
+ # @severity = xml.attributes["severity"]
71
+ :plugin_id => 'pluginID',
72
+ :plugin_name => 'pluginName',
73
+ :plugin_family => 'pluginFamily'
74
+ }
75
+ method_name = translations_table.fetch(method, method.to_s)
76
+ return @xml.attributes[method_name].value if @xml.attributes.key?(method_name)
77
+
78
+ # then we try the children tags: solution, risk_factor, description,
79
+ # plugin_publication_date, metasploit_name, cvss_vector,
80
+ # cvss_temporal_vector, synopsis, exploit_available,
81
+ # patch_publication_date, plugin_modification_date, cvss_temporal_score,
82
+ # cvss_base_score, plugin_output, plugin_version, exploitability_ease,
83
+ # vuln_publication_date, exploit_framework_canvas,
84
+ # exploit_framework_metasploit, exploit_framework_core
85
+ tag = @xml.xpath("./#{method_name}").first
86
+ if tag
87
+ return tag.text
88
+ end
89
+
90
+ # then the custom XML tags (cm: namespace)
91
+ if method_name.starts_with?('cm_')
92
+ method_name = method_name.sub(/cm_/, 'cm:compliance-').gsub(/_/, '-')
93
+ cm_value = @xml.at_xpath("./#{method_name}", { 'cm' => 'http://www.nessus.org/cm' })
94
+ if cm_value
95
+ return cm_value.text
96
+ else
97
+ return nil
98
+ end
99
+ end
100
+
101
+
102
+ # finally the enumerations: bid_entries, cve_entries, xref_entries
103
+ translations_table = {
104
+ :bid_entries => 'bid',
105
+ :cve_entries => 'cve',
106
+ :see_also_entries => 'see_also',
107
+ :xref_entries => 'xref'
108
+ }
109
+ method_name = translations_table.fetch(method, nil)
110
+ if method_name
111
+ @xml.xpath("./#{method_name}").collect(&:text)
112
+ else
113
+ # nothing found, the tag is valid but not present in this ReportItem
114
+ return nil
115
+ end
116
+ end
117
+ end
118
+ end