dradis-nexpose 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|