dradis-nessus 3.18.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/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +36 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +23 -0
- data/LICENSE +339 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/dradis-nessus.gemspec +35 -0
- data/lib/dradis-nessus.rb +9 -0
- data/lib/dradis/plugins/nessus.rb +11 -0
- data/lib/dradis/plugins/nessus/engine.rb +13 -0
- data/lib/dradis/plugins/nessus/field_processor.rb +55 -0
- data/lib/dradis/plugins/nessus/gem_version.rb +19 -0
- data/lib/dradis/plugins/nessus/importer.rb +177 -0
- data/lib/dradis/plugins/nessus/version.rb +13 -0
- data/lib/nessus/host.rb +82 -0
- data/lib/nessus/report_item.rb +118 -0
- data/lib/tasks/thorfile.rb +21 -0
- data/spec/dradis/plugins/nessus/field_processor_spec.rb +41 -0
- data/spec/dradis/plugins/nessus/importer_spec.rb +55 -0
- data/spec/fixtures/files/example_v2.nessus +2076 -0
- data/spec/fixtures/files/host-01.xml +18 -0
- data/spec/fixtures/files/report_item-with-list.xml +45 -0
- data/spec/nessus/host_spec.rb +29 -0
- data/spec/spec_helper.rb +10 -0
- data/templates/evidence.fields +17 -0
- data/templates/evidence.sample +53 -0
- data/templates/evidence.template +5 -0
- data/templates/report_host.fields +8 -0
- data/templates/report_host.sample +12 -0
- data/templates/report_host.template +14 -0
- data/templates/report_item.fields +33 -0
- data/templates/report_item.sample +43 -0
- data/templates/report_item.template +20 -0
- metadata +172 -0
data/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Nessus add-on for Dradis
|
|
2
|
+
|
|
3
|
+
[](http://travis-ci.org/dradis/dradis-nessus) [](https://codeclimate.com/github/dradis/dradis-nessus.png)
|
|
4
|
+
|
|
5
|
+
The Nessus upload add-on will enable user to upload Nessus output files in the nessus client format (.nessus) to create a structure of nodes/notes that contain the same information about the hosts/ports/services as the original file.
|
|
6
|
+
|
|
7
|
+
The parser only supports version 2 of nessus xml format. Other formats (nbe, nsr) are not supported at the moment.
|
|
8
|
+
|
|
9
|
+
Also, the xml parser only extracts the results of a scan. It is not able to parse the scan policy itself which is also part of the xml file.
|
|
10
|
+
|
|
11
|
+
The add-on requires Dradis 3.0 or higher.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## More information
|
|
15
|
+
|
|
16
|
+
See the Dradis Framework's [README.md](https://github.com/dradis/dradisframework/blob/master/README.md)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Contributing
|
|
20
|
+
|
|
21
|
+
See the Dradis Framework's [CONTRIBUTING.md](https://github.com/dradis/dradisframework/blob/master/CONTRIBUTING.md)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
Dradis Framework and all its components are released under [GNU General Public License version 2.0](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html) as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Feature requests and bugs
|
|
30
|
+
|
|
31
|
+
Please use the [Dradis Framework issue tracker](https://github.com/dradis/dradis-ce/issues) for add-on improvements and bug reports.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
|
2
|
+
require 'dradis/plugins/nessus/version'
|
|
3
|
+
version = Dradis::Plugins::Nessus::VERSION::STRING
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Describe your gem and declare its dependencies:
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.platform = Gem::Platform::RUBY
|
|
9
|
+
spec.name = 'dradis-nessus'
|
|
10
|
+
spec.version = version
|
|
11
|
+
spec.summary = 'Nessus upload add-on for the Dradis Framework.'
|
|
12
|
+
spec.description = 'This add-on allows you to upload and parse output produced from Tenable\'s Nessus Scanner into Dradis.'
|
|
13
|
+
|
|
14
|
+
spec.license = 'GPL-2'
|
|
15
|
+
|
|
16
|
+
spec.authors = ['Daniel Martin']
|
|
17
|
+
spec.email = ['etd@nomejortu.com']
|
|
18
|
+
spec.homepage = 'http://dradisframework.org'
|
|
19
|
+
|
|
20
|
+
spec.files = `git ls-files`.split($\)
|
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
22
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
23
|
+
|
|
24
|
+
# By not including Rails as a dependency, we can use the gem with different
|
|
25
|
+
# versions of Rails (a sure recipe for disaster, I'm sure), which is needed
|
|
26
|
+
# until we bump Dradis Pro to 4.1.
|
|
27
|
+
# s.add_dependency 'rails', '~> 4.1.1'
|
|
28
|
+
spec.add_dependency 'dradis-plugins', '~> 3.6'
|
|
29
|
+
spec.add_dependency 'nokogiri'
|
|
30
|
+
|
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
|
33
|
+
spec.add_development_dependency 'rspec-rails'
|
|
34
|
+
spec.add_development_dependency 'combustion', '~> 0.5.2'
|
|
35
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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 =~ /^\s+-/
|
|
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").map do |paragraph|
|
|
42
|
+
if paragraph =~ /(.*)\s+:\s*$/m
|
|
43
|
+
$1 + ':'
|
|
44
|
+
elsif paragraph =~ /^\s+-\s+(.*)$/m
|
|
45
|
+
'* ' + $1.gsub(/\s{3,}/, ' ').gsub(/\n/, ' ')
|
|
46
|
+
else
|
|
47
|
+
paragraph
|
|
48
|
+
end
|
|
49
|
+
end.join("\n")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
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 = 18
|
|
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,177 @@
|
|
|
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
|
+
# host_node - The Dradis Node that represents the host in the project.
|
|
45
|
+
# xml_report_item - The Nokogiri XML node representing the Service Detection
|
|
46
|
+
# <ReportItem> tag.
|
|
47
|
+
#
|
|
48
|
+
# Returns nothing.
|
|
49
|
+
#
|
|
50
|
+
# Plugins processed using this method:
|
|
51
|
+
# - [11219] Nessus SYN Scanner
|
|
52
|
+
# - [34220] Netstat Portscanner (WMI)
|
|
53
|
+
def process_nessus_syn_scanner(host_node, xml_report_item)
|
|
54
|
+
process_service(
|
|
55
|
+
host_node,
|
|
56
|
+
xml_report_item,
|
|
57
|
+
{ 'syn-scanner' => xml_report_item.at_xpath('./plugin_output').try(:text)}
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Internal: Process each /NessusClientData_v2/Report/ReportHost creating a
|
|
62
|
+
# Dradis node and adding some properties to it (:ip, :os, etc.).
|
|
63
|
+
#
|
|
64
|
+
# xml_host - The Nokogiri XML node representing the parent host for
|
|
65
|
+
# this issue.
|
|
66
|
+
#
|
|
67
|
+
# Returns nothing.
|
|
68
|
+
#
|
|
69
|
+
def process_report_host(xml_host)
|
|
70
|
+
|
|
71
|
+
# 1. Create host node
|
|
72
|
+
host_label = xml_host.attributes['name'].value
|
|
73
|
+
host_label += " (#{xml_host.attributes['fqdn'].value})" if xml_host.attributes['fqdn']
|
|
74
|
+
|
|
75
|
+
host_node = content_service.create_node(label: host_label, type: :host)
|
|
76
|
+
logger.info{ "\tHost: #{host_label}" }
|
|
77
|
+
|
|
78
|
+
# 2. Add host info note and host properties
|
|
79
|
+
host_note_text = template_service.process_template(template: 'report_host', data: xml_host)
|
|
80
|
+
content_service.create_note(text: host_note_text, node: host_node)
|
|
81
|
+
|
|
82
|
+
if host_node.respond_to?(:properties)
|
|
83
|
+
nh = ::Nessus::Host.new(xml_host)
|
|
84
|
+
host_node.set_property(:fqdn, nh.fqdn) if nh.try(:fqdn)
|
|
85
|
+
host_node.set_property(:ip, nh.ip) if nh.try(:ip)
|
|
86
|
+
host_node.set_property(:mac_address, nh.mac_address) if nh.try(:mac_address)
|
|
87
|
+
host_node.set_property(:netbios_name, nh.netbios_name) if nh.try(:netbios_name)
|
|
88
|
+
host_node.set_property(:os, nh.operating_system) if nh.try(:operating_system)
|
|
89
|
+
host_node.save
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# 3. Add Issue and associated Evidence for this host/port combination
|
|
94
|
+
xml_host.xpath('./ReportItem').each do |xml_report_item|
|
|
95
|
+
case xml_report_item.attributes['pluginID'].value
|
|
96
|
+
when '0'
|
|
97
|
+
when '11219', '34220' # Nessus SYN scanner, Netstat Portscanner (WMI)
|
|
98
|
+
process_nessus_syn_scanner(host_node, xml_report_item)
|
|
99
|
+
when '22964' # Service Detection
|
|
100
|
+
process_service_detection(host_node, xml_report_item)
|
|
101
|
+
else
|
|
102
|
+
process_report_item(xml_host, host_node, xml_report_item)
|
|
103
|
+
end
|
|
104
|
+
end #/ReportItem
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Internal: Process each /NessusClientData_v2/Report/ReportHost/ReportItem
|
|
108
|
+
# and creates the corresponding Issue and Evidence in Dradis.
|
|
109
|
+
#
|
|
110
|
+
# xml_host - The Nokogiri XML node representing the parent host for
|
|
111
|
+
# this issue.
|
|
112
|
+
# host_node - The Dradis Node that represents the host in the project.
|
|
113
|
+
# xml_report_item - The Nokogiri XML node representing the Service Detection
|
|
114
|
+
# <ReportItem> tag.
|
|
115
|
+
#
|
|
116
|
+
# Returns nothing.
|
|
117
|
+
#
|
|
118
|
+
def process_report_item(xml_host, host_node, xml_report_item)
|
|
119
|
+
# 3.1. Add Issue to the project
|
|
120
|
+
plugin_id = xml_report_item.attributes['pluginID'].value
|
|
121
|
+
logger.info{ "\t\t => Creating new issue (plugin_id: #{plugin_id})" }
|
|
122
|
+
|
|
123
|
+
issue_text = template_service.process_template(template: 'report_item', data: xml_report_item)
|
|
124
|
+
|
|
125
|
+
issue = content_service.create_issue(text: issue_text, id: plugin_id)
|
|
126
|
+
|
|
127
|
+
# 3.2. Add Evidence to link the port/protocol and Issue
|
|
128
|
+
port_info = xml_report_item.attributes['protocol'].value
|
|
129
|
+
port_info += "/"
|
|
130
|
+
port_info += xml_report_item.attributes['port'].value
|
|
131
|
+
|
|
132
|
+
logger.info{ "\t\t\t => Adding reference to this host" }
|
|
133
|
+
evidence_content = template_service.process_template(template: 'evidence', data: xml_report_item)
|
|
134
|
+
|
|
135
|
+
content_service.create_evidence(issue: issue, node: host_node, content: evidence_content)
|
|
136
|
+
|
|
137
|
+
# 3.3. Compliance check information
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Internal: Parses the specific "Service Detection" plugin into Dradis node
|
|
141
|
+
# properties.
|
|
142
|
+
#
|
|
143
|
+
# host_node - The Dradis Node that represents the host in the project.
|
|
144
|
+
# xml_report_item - The Nokogiri XML node representing the Service Detection
|
|
145
|
+
# <ReportItem> tag.
|
|
146
|
+
#
|
|
147
|
+
# Returns nothing.
|
|
148
|
+
#
|
|
149
|
+
def process_service_detection(host_node, xml_report_item)
|
|
150
|
+
output = xml_report_item.at_xpath('./plugin_output').try(:text) || xml_report_item.at_xpath('./description').try(:text)
|
|
151
|
+
process_service(
|
|
152
|
+
host_node,
|
|
153
|
+
xml_report_item,
|
|
154
|
+
{ 'service-detection' => output }
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def process_service(host_node, xml_report_item, service_extra)
|
|
159
|
+
name = xml_report_item['svc_name']
|
|
160
|
+
port = xml_report_item['port'].to_i
|
|
161
|
+
protocol = xml_report_item['protocol']
|
|
162
|
+
logger.info { "\t\t => Creating new service: #{protocol}/#{port}" }
|
|
163
|
+
|
|
164
|
+
host_node.set_service(
|
|
165
|
+
service_extra.merge({
|
|
166
|
+
name: name,
|
|
167
|
+
port: port,
|
|
168
|
+
protocol: protocol,
|
|
169
|
+
state: :open,
|
|
170
|
+
source: :nessus
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
host_node.save
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
data/lib/nessus/host.rb
ADDED
|
@@ -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
|