dradis-nexpose 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +3 -0
  5. data/CONTRIBUTING.md +3 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE +339 -0
  8. data/README.md +29 -0
  9. data/Rakefile +1 -0
  10. data/dradis-nexpose.gemspec +35 -0
  11. data/lib/dradis-nexpose.rb +12 -0
  12. data/lib/dradis/plugins/nexpose.rb +11 -0
  13. data/lib/dradis/plugins/nexpose/engine.rb +9 -0
  14. data/lib/dradis/plugins/nexpose/field_processor.rb +89 -0
  15. data/lib/dradis/plugins/nexpose/formats/full.rb +152 -0
  16. data/lib/dradis/plugins/nexpose/formats/simple.rb +76 -0
  17. data/lib/dradis/plugins/nexpose/gem_version.rb +19 -0
  18. data/lib/dradis/plugins/nexpose/importer.rb +34 -0
  19. data/lib/dradis/plugins/nexpose/version.rb +13 -0
  20. data/lib/nexpose/endpoint.rb +81 -0
  21. data/lib/nexpose/node.rb +117 -0
  22. data/lib/nexpose/scan.rb +65 -0
  23. data/lib/nexpose/service.rb +101 -0
  24. data/lib/nexpose/vulnerability.rb +95 -0
  25. data/lib/tasks/thorfile.rb +26 -0
  26. data/spec/fixtures/files/full.xml +144 -0
  27. data/spec/fixtures/files/simple.xml +53 -0
  28. data/spec/nexpose_upload_spec.rb +154 -0
  29. data/spec/spec_helper.rb +13 -0
  30. data/templates/full_node.fields +8 -0
  31. data/templates/full_node.sample +65 -0
  32. data/templates/full_node.template +13 -0
  33. data/templates/full_scan.fields +5 -0
  34. data/templates/full_scan.sample +6 -0
  35. data/templates/full_scan.template +9 -0
  36. data/templates/full_service.fields +4 -0
  37. data/templates/full_service.sample +17 -0
  38. data/templates/full_service.template +11 -0
  39. data/templates/full_vulnerability.fields +13 -0
  40. data/templates/full_vulnerability.sample +59 -0
  41. data/templates/full_vulnerability.template +34 -0
  42. data/templates/simple_port.fields +2 -0
  43. data/templates/simple_port.sample +23 -0
  44. data/templates/simple_port.template +5 -0
  45. metadata +176 -0
@@ -0,0 +1,12 @@
1
+ # Hook into the framework
2
+ require 'dradis-plugins'
3
+
4
+ # Load this engine
5
+ require 'dradis/plugins/nexpose'
6
+
7
+ # Non-dradis related files
8
+ require 'nexpose/endpoint'
9
+ require 'nexpose/node'
10
+ require 'nexpose/scan'
11
+ require 'nexpose/service'
12
+ require 'nexpose/vulnerability'
@@ -0,0 +1,11 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nexpose
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'dradis/plugins/nexpose/engine'
9
+ require 'dradis/plugins/nexpose/field_processor'
10
+ require 'dradis/plugins/nexpose/importer'
11
+ require 'dradis/plugins/nexpose/version'
@@ -0,0 +1,9 @@
1
+ module Dradis::Plugins::Nexpose
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Dradis::Plugins::Nexpose
4
+
5
+ include ::Dradis::Plugins::Base
6
+ description 'Processes Nexpose XML format'
7
+ provides :upload
8
+ end
9
+ end
@@ -0,0 +1,89 @@
1
+ module Dradis::Plugins::Nexpose
2
+
3
+ # This processor defers to ::Acunetix::Scan for the scan template and to
4
+ # ::Acunetix::ReportItem for the report_item and evidence templates.
5
+ class FieldProcessor < Dradis::Plugins::Upload::FieldProcessor
6
+
7
+ def post_initialize(args={})
8
+ if data.kind_of?(Hash) ||
9
+ data.kind_of?(Nexpose::Scan) ||
10
+ data.kind_of?(Nexpose::Node) ||
11
+ data.kind_of?(Nexpose::Service) ||
12
+ data.kind_of?(Nexpose::Vulnerability)
13
+ @nexpose_object = data
14
+ else
15
+ # XML from Plugin Manager
16
+ if (data.name == 'scan')
17
+ @nexpose_object = Nexpose::Scan.new(data)
18
+ elsif (data.name == 'node')
19
+ # Full - node
20
+ @nexpose_object = Nexpose::Node.new(data)
21
+ elsif (data.name == 'service')
22
+ # Full - service
23
+ @nexpose_object = Nexpose::Service.new(data)
24
+ else
25
+ if data['added']
26
+ # Full - vulnerability
27
+ @nexpose_object = Nexpose::Vulnerability.new(data)
28
+ else
29
+ # Simple - port
30
+ @nexpose_object = {
31
+ id: data['id'],
32
+ finding: data.xpath('//id').collect{ |id_node| "#{id_node['type']} : #{id_node.text}" }.join("\n")
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def value(args={})
40
+ field = args[:field]
41
+
42
+ # fields in the template are of the form <foo>.<field>, where <foo>
43
+ # is common across all fields for a given template (and meaningless).
44
+ _, name = field.split('.')
45
+
46
+ # Simple - port
47
+ if @nexpose_object.kind_of?(Hash)
48
+ @nexpose_object[name.to_sym]
49
+ else
50
+ # Full - scan / node / service vulnerability
51
+ result = @nexpose_object.try(name, nil)
52
+ if result.kind_of?(Array)
53
+ result << 'n/a' if result.empty?
54
+ if result.first.is_a?(String)
55
+ result.join("\n")
56
+ else
57
+ # we have an array of hashes
58
+ format_array_as_table(result)
59
+ end
60
+ else
61
+ result || 'n/a'
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+ # Return an array as a table:
68
+ #
69
+ # [
70
+ # {:a => 1, :b => 2},
71
+ # {:a => 3, :b => 4}
72
+ # ]
73
+ #
74
+ # becomes
75
+ #
76
+ # |_. a |_. b |
77
+ # | 1 | 2 |
78
+ # | 3 | 4 |
79
+ #
80
+ def format_array_as_table(array)
81
+ rows = []
82
+ rows << "|_. #{array.first.keys.join(' |_. ')} |"
83
+ array.each do |hash|
84
+ rows << "| #{hash.collect{|_,v| v}.join(" | ")} |"
85
+ end
86
+ rows.join("\n")
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,152 @@
1
+ module Dradis::Plugins::Nexpose::Formats
2
+
3
+ # This module knows how to parse Nexpose Ful XML format.
4
+ module Full
5
+ private
6
+
7
+ def process_full(doc)
8
+ note_text = nil
9
+
10
+ @vuln_list = []
11
+ evidence = Hash.new { |h, k| h[k] = {} }
12
+ hosts = Array.new
13
+
14
+ # First, extract scans
15
+ scan_node = content_service.create_node(label: 'Nexpose Scan Summary')
16
+ logger.info{ "\tProcessing scan summary" }
17
+
18
+ doc.xpath('//scans/scan').each do |xml_scan|
19
+ note_text = template_service.process_template(template: 'full_scan', data: xml_scan)
20
+ content_service.create_note(node: scan_node, text: note_text)
21
+ end
22
+
23
+
24
+ # Second, we parse the nodes
25
+ doc.xpath('//nodes/node').each do |xml_node|
26
+ nexpose_node = Nexpose::Node.new(xml_node)
27
+
28
+ host_node = content_service.create_node(label: nexpose_node.address, type: :host)
29
+ logger.info{ "\tProcessing host: #{nexpose_node.address}" }
30
+
31
+ # add the summary note for this host
32
+ note_text = template_service.process_template(template: 'full_node', data: nexpose_node)
33
+ content_service.create_note(node: host_node, text: note_text)
34
+
35
+ if host_node.respond_to?(:properties)
36
+ logger.info{ "\tAdding host properties: :ip and :hostnames"}
37
+ host_node.set_property(:ip, nexpose_node.address)
38
+ host_node.set_property(:hostnames, nexpose_node.names)
39
+ end
40
+
41
+ # inject this node's address into any vulnerabilities identified
42
+ #
43
+ # TODO: There is room for improvement here, we could have a hash that
44
+ # linked vulns with test/service and host to create proper content for
45
+ # Evidence.
46
+ nexpose_node.tests.each do |node_test|
47
+ test_id = node_test[:id].to_s.downcase
48
+
49
+ # We can't use the straightforward version below because Nexpose uses
50
+ # mixed-case some times (!)
51
+ # xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[@id='#{node_test[:id]}']").first
52
+ # See:
53
+ # http://stackoverflow.com/questions/1625446/problem-with-upper-case-and-lower-case-xpath-functions-in-selenium-ide/1625859#1625859
54
+ xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='#{test_id}']").first
55
+ xml_vuln.add_child("<hosts/>") unless xml_vuln.last_element_child.name == "hosts"
56
+
57
+ if xml_vuln.xpath("./hosts/host[text()='#{nexpose_node.address}']").empty?
58
+ xml_vuln.last_element_child.add_child( "<host>#{nexpose_node.address}</host>")
59
+ end
60
+ end
61
+
62
+ nexpose_node.endpoints.each do |endpoint|
63
+ # endpoint_node = content_service.create_node(label: endpoint.label, parent: host_node)
64
+ logger.info{ "\t\tEndpoint: #{endpoint.label}" }
65
+
66
+ if host_node.respond_to?(:properties)
67
+ logger.info{ "\t\tAdding to Services table" }
68
+ host_node.set_property(:services, {
69
+ port: endpoint.port.to_i,
70
+ protocol: endpoint.protocol,
71
+ state: endpoint.status,
72
+ name: endpoint.services.map(&:name).join(', ')
73
+ # reason: port.reason,
74
+ # product: port.try('service').try('product'),
75
+ # version: port.try('service').try('version')
76
+ })
77
+ end
78
+
79
+ endpoint.services.each do |service|
80
+
81
+ # add the summary note for this service
82
+ note_text = template_service.process_template(template: 'full_service', data: service)
83
+ # content_service.create_note(node: endpoint_node, text: note_text)
84
+ content_service.create_note(node: host_node, text: note_text)
85
+
86
+ # inject this node's address into any vulnerabilities identified
87
+ service.tests.each do |service_test|
88
+ test_id = service_test[:id].to_s.downcase
89
+
90
+ # For some reason Nexpose fails to include the 'http-iis-0011' vulnerability definition
91
+ next if test_id == 'http-iis-0011'
92
+
93
+ # We can't use the straightforward version below because Nexpose uses
94
+ # mixed-case some times (!)
95
+ # xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[@id='#{service_test[:id]}']").first
96
+ # See:
97
+ # http://stackoverflow.com/questions/1625446/problem-with-upper-case-and-lower-case-xpath-functions-in-selenium-ide/1625859#1625859
98
+ #
99
+ xml_vuln = doc.xpath("//VulnerabilityDefinitions/vulnerability[translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='#{test_id}']").first
100
+ xml_vuln.add_child("<hosts/>") unless xml_vuln.last_element_child.name == "hosts"
101
+
102
+ if xml_vuln.xpath("./hosts/host[text()='#{nexpose_node.address}']").empty?
103
+ xml_vuln.last_element_child.add_child( "<host>#{nexpose_node.address}</host>")
104
+ end
105
+
106
+ evidence[test_id][nexpose_node.address] = service_test[:content]
107
+ end
108
+ end
109
+ end
110
+
111
+ # add note under this node for each vulnerable ./node/test/
112
+ host_node.save
113
+ end
114
+
115
+ # Third, parse vulnerability definitions
116
+ definitions_node = content_service.create_node(label: 'Definitions')
117
+ logger.info{ "\tProcessing issue definitions:" }
118
+
119
+ doc.xpath('//VulnerabilityDefinitions/vulnerability').each do |xml_vulnerability|
120
+ id = xml_vulnerability['id'].downcase
121
+ # if @vuln_list.include?(id)
122
+ issue_text = template_service.process_template(template: 'full_vulnerability', data: xml_vulnerability)
123
+
124
+ # retrieve hosts affected by this issue (injected in step 2)
125
+ #
126
+ # There is no need for the below as Issues are linked to hosts via the
127
+ # corresponding Evidence instance
128
+ #
129
+ # note_text << "\n\n#[host]#\n"
130
+ # note_text << xml_vulnerability.xpath('./hosts/host').collect(&:text).join("\n")
131
+ # note_text << "\n\n"
132
+
133
+ # 3.1 create the Issue
134
+ issue = content_service.create_issue(text: issue_text, id: id)
135
+ logger.info{ "\tIssue: #{issue.fields ? issue.fields['Title'] : id}" }
136
+
137
+
138
+ # 3.2 associate with the nodes via Evidence.
139
+ # TODO: there is room for improvement here by providing proper Evidence content
140
+ xml_vulnerability.xpath('./hosts/host').collect(&:text).each do |host_name|
141
+ # if the node exists, this just returns it
142
+ host_node = content_service.create_node(label: host_name, type: :host)
143
+
144
+ evidence_content = evidence[id][host_name]
145
+ content_service.create_evidence(content: evidence_content, issue: issue, node: host_node)
146
+ end
147
+
148
+ # end
149
+ end
150
+ end # /parse_nexpose_full_xml
151
+ end
152
+ end
@@ -0,0 +1,76 @@
1
+ module Dradis::Plugins::Nexpose::Formats
2
+
3
+ # This module knows how to parse Nexpose Simple XML format.
4
+ module Simple
5
+ private
6
+
7
+ def process_simple(doc)
8
+ hosts = process_nexpose_simple_xml(doc)
9
+ notes_simple(hosts)
10
+ end
11
+
12
+ def notes_simple(hosts)
13
+ return if hosts.nil?
14
+
15
+ hosts.each do |host|
16
+ host_node = content_service.create_node(label: host['address'], type: :host)
17
+ content_service.create_note node: host_node, text: "Host Description : #{host['description']} \nScanner Fingerprint certainty : #{host['fingerprint']}"
18
+
19
+ generic_findings_node = content_service.create_node(label: 'Generic Findings', parent: host_node)
20
+ host['generic_vulns'].each do |id, finding|
21
+ content_service.create_note node: generic_findings_node, text: "Finding ID : #{id} \n \n Finding Refs :\n-------\n #{finding}"
22
+ end
23
+
24
+ port_text = nil
25
+ host['ports'].each do |port_label, findings|
26
+ port_node = content_service.create_node(label: port_label, parent: host_node)
27
+
28
+ findings.each do |id, finding|
29
+ port_text = template_service.process_template(template: 'simple_port', data: {id: id, finding: finding})
30
+ port_text << "\n#[host]#\n#{host['address']}\n\n"
31
+ content_service.create_note node: port_node, text: port_text
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def process_nexpose_simple_xml(doc)
38
+ results = doc.search('device')
39
+ hosts = Array.new
40
+ results.each do |host|
41
+ current_host = Hash.new
42
+ current_host['address'] = host['address']
43
+ current_host['fingerprint'] = host.search('fingerprint')[0].nil? ? "N/A" : host.search('fingerprint')[0]['certainty']
44
+ current_host['description'] = host.search('description')[0].nil? ? "N/A" : host.search('description')[0].text
45
+ #So there's two sets of vulns in a NeXpose simple XML report for each host
46
+ #Theres some generic ones at the top of the report
47
+ #And some service specific ones further down the report.
48
+ #So we need to get the generic ones before moving on
49
+ current_host['generic_vulns'] = Hash.new
50
+ host.xpath('vulnerabilities/vulnerability').each do |vuln|
51
+ current_host['generic_vulns'][vuln['id']] = ''
52
+ vuln.xpath('id').each do |id|
53
+ current_host['generic_vulns'][vuln['id']] << id['type'] + " : " + id.text + "\n"
54
+ end
55
+ end
56
+
57
+ current_host['ports'] = Hash.new
58
+ host.xpath('services/service').each do |service|
59
+ protocol = service['protocol']
60
+ portid = service['port']
61
+ port_label = protocol + '-' + portid
62
+ current_host['ports'][port_label] = Hash.new
63
+ service.xpath('vulnerabilities/vulnerability').each do |vuln|
64
+ current_host['ports'][port_label][vuln['id']] = ''
65
+ vuln.xpath('id').each do |id|
66
+ current_host['ports'][port_label][vuln['id']] << id['type'] + " : " + id.text + "\n"
67
+ end
68
+ end
69
+ end
70
+
71
+ hosts << current_host
72
+ end
73
+ return hosts
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,19 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Nexpose
4
+ # Returns the version of the currently loaded Nexpose 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 = 6
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,34 @@
1
+ require 'dradis/plugins/nexpose/formats/full'
2
+ require 'dradis/plugins/nexpose/formats/simple'
3
+
4
+ module Dradis::Plugins::Nexpose
5
+ class Importer < Dradis::Plugins::Upload::Importer
6
+
7
+ include Formats::Full
8
+ include Formats::Simple
9
+
10
+ # The framework will call this function if the user selects this plugin from
11
+ # the dropdown list and uploads a file.
12
+ # @returns true if the operation was successful, false otherwise
13
+ def import(params={})
14
+ file_content = File.read( params[:file] )
15
+
16
+ logger.info { 'Parsing NeXpose output file...' }
17
+ doc = Nokogiri::XML(file_content)
18
+ logger.info { 'Done.' }
19
+
20
+ if doc.root.name == 'NeXposeSimpleXML'
21
+ logger.info { 'NeXpose-Simple format detected' }
22
+ process_simple(doc)
23
+ elsif doc.root.name == 'NexposeReport'
24
+ logger.info { 'NeXpose-Full format detected' }
25
+ process_full(doc)
26
+ else
27
+ error = "The document doesn't seem to be in either NeXpose-Simple or NeXpose-Full XML format. Ensure you uploaded a Nexpose XML report."
28
+ logger.fatal{ error }
29
+ content_service.create_note text: error
30
+ return false
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Dradis
4
+ module Plugins
5
+ module Nexpose
6
+ # Returns the version of the currently loaded Nexpose 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,81 @@
1
+ module Nexpose
2
+ # This class represents each of the /NexposeReport[@version='1.0']/nodes/node/endpoints/endpoint
3
+ # elements in the Nexpose Full 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 Endpoint
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. <references/>, <tags/>)
19
+ def supported_tags
20
+ [
21
+ # meta
22
+ :label,
23
+
24
+ # attributes
25
+ :protocol, :port, :status,
26
+
27
+ # simple tags
28
+
29
+ # multiple tags
30
+ :services
31
+ ]
32
+ end
33
+
34
+ # Save some time with a meta attribute, e.g. 80/tcp (open)
35
+ def label
36
+ "#{self.port}/#{self.protocol} (#{self.status})"
37
+ end
38
+
39
+ # Each of the services associated with this endpoint. Returns an array of
40
+ # Nexpose::Service objects
41
+ def services
42
+ @xml.xpath('./services/service').collect { |xml_service| Service.new(xml_service) }
43
+ end
44
+
45
+
46
+ # This allows external callers (and specs) to check for implemented
47
+ # properties
48
+ def respond_to?(method, include_private=false)
49
+ return true if supported_tags.include?(method.to_sym)
50
+ super
51
+ end
52
+
53
+ # This method is invoked by Ruby when a method that is not defined in this
54
+ # instance is called.
55
+ #
56
+ # In our case we inspect the @method@ parameter and try to find the
57
+ # attribute, simple descendent or collection that it maps to in the XML
58
+ # tree.
59
+ def method_missing(method, *args)
60
+
61
+ # We could remove this check and return nil for any non-recognized tag.
62
+ # The problem would be that it would make tricky to debug problems with
63
+ # typos. For instance: <>.potr would return nil instead of raising an
64
+ # exception
65
+ unless supported_tags.include?(method)
66
+ super
67
+ return
68
+ end
69
+
70
+ # First we try the attributes. In Ruby we use snake_case, but in XML
71
+ # CamelCase is used for some attributes
72
+ translations_table = {
73
+ }
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
+ return nil
79
+ end
80
+ end
81
+ end