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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +19 -0
- data/LICENSE +339 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/dradis-nexpose.gemspec +35 -0
- data/lib/dradis-nexpose.rb +12 -0
- data/lib/dradis/plugins/nexpose.rb +11 -0
- data/lib/dradis/plugins/nexpose/engine.rb +9 -0
- data/lib/dradis/plugins/nexpose/field_processor.rb +89 -0
- data/lib/dradis/plugins/nexpose/formats/full.rb +152 -0
- data/lib/dradis/plugins/nexpose/formats/simple.rb +76 -0
- data/lib/dradis/plugins/nexpose/gem_version.rb +19 -0
- data/lib/dradis/plugins/nexpose/importer.rb +34 -0
- data/lib/dradis/plugins/nexpose/version.rb +13 -0
- data/lib/nexpose/endpoint.rb +81 -0
- data/lib/nexpose/node.rb +117 -0
- data/lib/nexpose/scan.rb +65 -0
- data/lib/nexpose/service.rb +101 -0
- data/lib/nexpose/vulnerability.rb +95 -0
- data/lib/tasks/thorfile.rb +26 -0
- data/spec/fixtures/files/full.xml +144 -0
- data/spec/fixtures/files/simple.xml +53 -0
- data/spec/nexpose_upload_spec.rb +154 -0
- data/spec/spec_helper.rb +13 -0
- data/templates/full_node.fields +8 -0
- data/templates/full_node.sample +65 -0
- data/templates/full_node.template +13 -0
- data/templates/full_scan.fields +5 -0
- data/templates/full_scan.sample +6 -0
- data/templates/full_scan.template +9 -0
- data/templates/full_service.fields +4 -0
- data/templates/full_service.sample +17 -0
- data/templates/full_service.template +11 -0
- data/templates/full_vulnerability.fields +13 -0
- data/templates/full_vulnerability.sample +59 -0
- data/templates/full_vulnerability.template +34 -0
- data/templates/simple_port.fields +2 -0
- data/templates/simple_port.sample +23 -0
- data/templates/simple_port.template +5 -0
- 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,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,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
|