dradis-nexpose 3.6.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.
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