nexpose 0.2.8 → 0.5.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 +4 -4
- data/lib/nexpose.rb +18 -9
- data/lib/nexpose/ajax.rb +127 -0
- data/lib/nexpose/alert.rb +29 -36
- data/lib/nexpose/common.rb +13 -12
- data/lib/nexpose/connection.rb +18 -13
- data/lib/nexpose/creds.rb +16 -55
- data/lib/nexpose/dag.rb +73 -0
- data/lib/nexpose/data_table.rb +134 -0
- data/lib/nexpose/device.rb +111 -0
- data/lib/nexpose/engine.rb +194 -0
- data/lib/nexpose/filter.rb +341 -0
- data/lib/nexpose/group.rb +33 -37
- data/lib/nexpose/manage.rb +4 -0
- data/lib/nexpose/pool.rb +142 -0
- data/lib/nexpose/report.rb +72 -278
- data/lib/nexpose/report_template.rb +249 -0
- data/lib/nexpose/scan.rb +196 -54
- data/lib/nexpose/scan_template.rb +103 -0
- data/lib/nexpose/site.rb +91 -237
- data/lib/nexpose/ticket.rb +173 -119
- data/lib/nexpose/user.rb +11 -3
- data/lib/nexpose/vuln.rb +81 -338
- data/lib/nexpose/vuln_exception.rb +368 -0
- metadata +12 -4
- data/lib/nexpose/misc.rb +0 -35
- data/lib/nexpose/scan_engine.rb +0 -325
@@ -0,0 +1,368 @@
|
|
1
|
+
module Nexpose
|
2
|
+
module NexposeAPI
|
3
|
+
include XMLUtils
|
4
|
+
|
5
|
+
# Retrieve vulnerability exceptions.
|
6
|
+
#
|
7
|
+
# @param [String] status Filter exceptions by the current status.
|
8
|
+
# @see Nexpose::VulnException::Status
|
9
|
+
# @param [String] duration A time interval in the format "PnYnMnDTnHnMnS".
|
10
|
+
# @return [Array[VulnException]] List of matching vulnerability exceptions.
|
11
|
+
#
|
12
|
+
def list_vuln_exceptions(status = nil, duration = nil)
|
13
|
+
option = {}
|
14
|
+
option['status'] = status if status
|
15
|
+
option['time-duration'] = duration if duration
|
16
|
+
xml = make_xml('VulnerabilityExceptionListingRequest', option)
|
17
|
+
response = execute(xml, '1.2')
|
18
|
+
|
19
|
+
xs = []
|
20
|
+
if response.success
|
21
|
+
response.res.elements.each('//VulnerabilityException') do |ve|
|
22
|
+
xs << VulnException.parse(ve)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
xs
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :vuln_exceptions, :list_vuln_exceptions
|
29
|
+
|
30
|
+
# Resubmit a vulnerability exception request with a new comment and reason
|
31
|
+
# after an exception has been rejected.
|
32
|
+
#
|
33
|
+
# You can only resubmit a request that has a "Rejected" status; if an
|
34
|
+
# exception is "Approved" or "Under Review" you will receive an error
|
35
|
+
# message stating that the exception request cannot be resubmitted.
|
36
|
+
#
|
37
|
+
# @param [Fixnum] id Unique identifier of the exception to resubmit.
|
38
|
+
# @param [String] comment Comment to justify the exception resubmission.
|
39
|
+
# @param [String] reason The reason for the exception status, if changing.
|
40
|
+
# @see Nexpose::VulnException::Reason
|
41
|
+
# @return [Boolean] Whether or not the resubmission was valid.
|
42
|
+
#
|
43
|
+
def resubmit_vuln_exception(id, comment, reason = nil)
|
44
|
+
options = { 'exception-id' => id }
|
45
|
+
options['reason'] = reason if reason
|
46
|
+
xml = make_xml('VulnerabilityExceptionResubmitRequest', options)
|
47
|
+
comment_xml = make_xml('comment', {}, comment, false)
|
48
|
+
xml.add_element(comment_xml)
|
49
|
+
r = execute(xml, '1.2')
|
50
|
+
r.success
|
51
|
+
end
|
52
|
+
|
53
|
+
# Recall a vulnerability exception. Recall is used by a submitter to undo an
|
54
|
+
# exception request that has not been approved yet.
|
55
|
+
#
|
56
|
+
# You can only recall a vulnerability exception that has 'Under Review'
|
57
|
+
# status.
|
58
|
+
#
|
59
|
+
# @param [Fixnum] id Unique identifier of the exception to resubmit.
|
60
|
+
# @return [Boolean] Whether or not the recall was accepted by the console.
|
61
|
+
#
|
62
|
+
def recall_vuln_exception(id)
|
63
|
+
xml = make_xml('VulnerabilityExceptionRecallRequest',
|
64
|
+
{ 'exception-id' => id })
|
65
|
+
execute(xml, '1.2').success
|
66
|
+
end
|
67
|
+
|
68
|
+
# Delete an existing vulnerability exception.
|
69
|
+
#
|
70
|
+
# @param [Fixnum] id The ID of a vuln exception.
|
71
|
+
# @return [Boolean] Whether or not deletion was successful.
|
72
|
+
#
|
73
|
+
def delete_vuln_exception(id)
|
74
|
+
xml = make_xml('VulnerabilityExceptionDeleteRequest',
|
75
|
+
{ 'exception-id' => id })
|
76
|
+
execute(xml, '1.2').success
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# A vulnerability exception.
|
81
|
+
#
|
82
|
+
# Certain attributes are necessary for some exception scopes, even though
|
83
|
+
# they are optional otherwise.
|
84
|
+
# • An exception for all instances of a vulnerability on all assets only
|
85
|
+
# requires the vuln_id attribute. The device_id, vuln_key and port
|
86
|
+
# attributes are ignored for this scope type.
|
87
|
+
# • An exception for all instances on a specific asset requires the vuln_id
|
88
|
+
# and device_id attributes. The vuln_key and port attributes are ignored for
|
89
|
+
# this scope type.
|
90
|
+
# • An exception for a specific instance of a vulnerability on a specific
|
91
|
+
# asset requires the vuln_id, device_id. Additionally, the port and/or the
|
92
|
+
# key attribute must be specified.
|
93
|
+
#
|
94
|
+
class VulnException
|
95
|
+
|
96
|
+
# Unique identifier assigned to an exception.
|
97
|
+
attr_accessor :id
|
98
|
+
# Unique identifier of a vulnerability.
|
99
|
+
attr_accessor :vuln_id
|
100
|
+
# The name of submitter of the exception.
|
101
|
+
attr_accessor :submitter
|
102
|
+
# The name of the reviewer of the exception.
|
103
|
+
attr_accessor :reviewer
|
104
|
+
# The state of the exception in the work flow process.
|
105
|
+
# @see Nexpose::VulnException::Status
|
106
|
+
attr_accessor :status
|
107
|
+
# The reason for the exception status.
|
108
|
+
# @see Nexpose::VulnException::Reason
|
109
|
+
attr_accessor :reason
|
110
|
+
# The scope of the exception.
|
111
|
+
# @see Nexpose::VulnException::Scope
|
112
|
+
attr_accessor :scope
|
113
|
+
# ID of device, if this exception applies to only one device.
|
114
|
+
attr_accessor :device_id
|
115
|
+
# Port on a device, if this exception applies to a specific port.
|
116
|
+
attr_accessor :port
|
117
|
+
# The specific vulnerable component in a discovered instance of the
|
118
|
+
# vulnerability referenced by the vuln_id, such as a program, file or user
|
119
|
+
# account.
|
120
|
+
attr_accessor :vuln_key
|
121
|
+
# The date an exception will expire, causing the vulnerability to be
|
122
|
+
# included in report risk scores.
|
123
|
+
attr_accessor :expiration
|
124
|
+
# Any comment provided by the submitter.
|
125
|
+
attr_accessor :submitter_comment
|
126
|
+
# Any comment provided by the reviewer.
|
127
|
+
attr_accessor :reviewer_comment
|
128
|
+
|
129
|
+
def initialize(vuln_id, scope, reason, status = nil)
|
130
|
+
@vuln_id, @scope, @reason, @status = vuln_id, scope, reason, status
|
131
|
+
end
|
132
|
+
|
133
|
+
# Submit this exception on the security console.
|
134
|
+
#
|
135
|
+
# @param [Connection] connection Connection to security console.
|
136
|
+
# @return [Fixnum] Newly assigned exception ID.
|
137
|
+
#
|
138
|
+
def save(connection, comment = nil)
|
139
|
+
validate
|
140
|
+
|
141
|
+
xml = connection.make_xml('VulnerabilityExceptionCreateRequest')
|
142
|
+
xml.add_attributes({ 'vuln-id' => @vuln_id,
|
143
|
+
'scope' => @scope,
|
144
|
+
'reason' => @reason })
|
145
|
+
case @scope
|
146
|
+
when Scope::ALL_INSTANCES_ON_A_SPECIFIC_ASSET
|
147
|
+
xml.add_attributes({ 'device-id' => @device_id })
|
148
|
+
when Scope::SPECIFIC_INSTANCE_OF_SPECIFIC_ASSET
|
149
|
+
xml.add_attributes({ 'device-id' => @device_id,
|
150
|
+
'port-no' => @port,
|
151
|
+
'vuln-key' => @vuln_key })
|
152
|
+
end
|
153
|
+
|
154
|
+
@submitter_comment = comment if comment
|
155
|
+
if @submitter_comment
|
156
|
+
comment = REXML::Element.new('comment')
|
157
|
+
comment.add_text(comment)
|
158
|
+
xml.add_element(comment)
|
159
|
+
end
|
160
|
+
|
161
|
+
response = connection.execute(xml, '1.2')
|
162
|
+
@id = response.attributes['exception-id'].to_i if response.success
|
163
|
+
end
|
164
|
+
|
165
|
+
# Resubmit a vulnerability exception request with a new comment and reason
|
166
|
+
# after an exception has been rejected.
|
167
|
+
#
|
168
|
+
# You can only resubmit a request that has a "Rejected" status; if an
|
169
|
+
# exception is "Approved" or "Under Review" you will receive an error
|
170
|
+
# message stating that the exception request cannot be resubmitted.
|
171
|
+
#
|
172
|
+
# This call will use the object's current state to resubmit.
|
173
|
+
#
|
174
|
+
# @param [Connection] connection Connection to security console.
|
175
|
+
# @return [Boolean] Whether or not the resubmission was valid.
|
176
|
+
#
|
177
|
+
def resubmit(connection)
|
178
|
+
raise ArgumentError.new('Only Rejected exceptions can be resubmitted.') unless @status == Status::REJECTED
|
179
|
+
connection.resubmit_vuln_exception(@id, @submitter_comment, @reason)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Recall a vulnerability exception. Recall is used by a submitter to undo an
|
183
|
+
# exception request that has not been approved yet.
|
184
|
+
#
|
185
|
+
# You can only recall a vulnerability exception that has 'Under Review'
|
186
|
+
# status.
|
187
|
+
#
|
188
|
+
# @param [Connection] connection Connection to security console.
|
189
|
+
# @return [Boolean] Whether or not the recall was accepted by the console.
|
190
|
+
#
|
191
|
+
def recall(connection)
|
192
|
+
connection.recall_vuln_exception(id)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Approve a vulnerability exception request, update comments and expiration
|
196
|
+
# dates on vulnerability exceptions that are "Under Review".
|
197
|
+
#
|
198
|
+
# @param [Connection] connection Connection to security console.
|
199
|
+
# @param [String] comment Comment to accompany the approval.
|
200
|
+
# @return [Boolean] Whether or not the approval was accepted by the console.
|
201
|
+
#
|
202
|
+
def approve(connection, comment = nil)
|
203
|
+
xml = connection.make_xml('VulnerabilityExceptionApproveRequest',
|
204
|
+
{ 'exception-id' => @id })
|
205
|
+
if comment
|
206
|
+
cxml = REXML::Element.new('comment')
|
207
|
+
cxml.add_text(comment)
|
208
|
+
xml.add_element(cxml)
|
209
|
+
@reviewer_comment = comment
|
210
|
+
end
|
211
|
+
|
212
|
+
connection.execute(xml, '1.2').success
|
213
|
+
end
|
214
|
+
|
215
|
+
# Reject a vulnerability exception request and update comments for the
|
216
|
+
# vulnerability exception request.
|
217
|
+
#
|
218
|
+
# @param [Connection] connection Connection to security console.
|
219
|
+
# @param [String] comment Comment to accompany the rejection.
|
220
|
+
# @return [Boolean] Whether or not the reject was accepted by the console.
|
221
|
+
#
|
222
|
+
def reject(connection, comment = nil)
|
223
|
+
xml = connection.make_xml('VulnerabilityExceptionRejectRequest',
|
224
|
+
{ 'exception-id' => @id })
|
225
|
+
if comment
|
226
|
+
cxml = REXML::Element.new('comment')
|
227
|
+
cxml.add_text(comment)
|
228
|
+
xml.add_element(cxml)
|
229
|
+
end
|
230
|
+
|
231
|
+
connection.execute(xml, '1.2').success
|
232
|
+
end
|
233
|
+
|
234
|
+
# Deletes this vulnerability exception.
|
235
|
+
#
|
236
|
+
# @param [Connection] connection Connection to security console.
|
237
|
+
# @return [Boolean] Whether or not deletion was successful.
|
238
|
+
#
|
239
|
+
def delete(connection)
|
240
|
+
connection.delete_vuln_exception(@id)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Update security console with submitter comment on this vulnerability
|
244
|
+
# exceptions.
|
245
|
+
#
|
246
|
+
# Cannot update a submit comment unless exception is under review or has
|
247
|
+
# expired.
|
248
|
+
#
|
249
|
+
# @param [Connection] connection Connection to security console.
|
250
|
+
# @param [String] comment Submitter comment on this exception.
|
251
|
+
# @return [Boolean] Whether the comment was successfully submitted.
|
252
|
+
#
|
253
|
+
def update_submitter_comment(connection, comment)
|
254
|
+
xml = connection.make_xml('VulnerabilityExceptionUpdateCommentRequest',
|
255
|
+
{ 'exception-id' => @id })
|
256
|
+
cxml = REXML::Element.new('submitter-comment')
|
257
|
+
cxml.add_text(comment)
|
258
|
+
xml.add_element(cxml)
|
259
|
+
@submitter_comment = comment
|
260
|
+
|
261
|
+
connection.execute(xml, '1.2').success
|
262
|
+
end
|
263
|
+
|
264
|
+
# Update security console with reviewer comment on this vulnerability
|
265
|
+
# exceptions.
|
266
|
+
#
|
267
|
+
# @param [Connection] connection Connection to security console.
|
268
|
+
# @param [String] comment Reviewer comment on this exception.
|
269
|
+
# @return [Boolean] Whether the comment was successfully submitted.
|
270
|
+
#
|
271
|
+
def update_reviewer_comment(connection, comment)
|
272
|
+
xml = connection.make_xml('VulnerabilityExceptionUpdateCommentRequest',
|
273
|
+
{ 'exception-id' => @id })
|
274
|
+
cxml = REXML::Element.new('reviewer-comment')
|
275
|
+
cxml.add_text(comment)
|
276
|
+
xml.add_element(cxml)
|
277
|
+
@reviewer_comment = comment
|
278
|
+
|
279
|
+
connection.execute(xml, '1.2').success
|
280
|
+
end
|
281
|
+
|
282
|
+
# Update the expiration date for this exception.
|
283
|
+
# The expiration time cannot be in the past.
|
284
|
+
#
|
285
|
+
# @param [Connection] connection Connection to security console.
|
286
|
+
# @param [String] new_date Date in the format "YYYY-MM-DD".
|
287
|
+
# @return [Boolean] Whether the update was successfully submitted.
|
288
|
+
#
|
289
|
+
def update_expiration_date(connection, new_date)
|
290
|
+
xml = connection.make_xml('VulnerabilityExceptionUpdateExpirationDateRequest',
|
291
|
+
{ 'exception-id' => @id,
|
292
|
+
'expiration-date' => new_date })
|
293
|
+
connection.execute(xml, '1.2').success
|
294
|
+
end
|
295
|
+
|
296
|
+
# Validate that this exception meets to requires for the assigned scope.
|
297
|
+
#
|
298
|
+
def validate
|
299
|
+
raise ArgumentError.new('No vuln_id.') unless @vuln_id
|
300
|
+
raise ArgumentError.new('No scope.') unless @scope
|
301
|
+
raise ArgumentError.new('No reason.') unless @reason
|
302
|
+
|
303
|
+
case @scope
|
304
|
+
when Scope::ALL_INSTANCES
|
305
|
+
@device_id = @port = @vuln_key = nil
|
306
|
+
when Scope::ALL_INSTANCES_ON_A_SPECIFIC_ASSET
|
307
|
+
raise ArgumentError.new('No device_id.') unless @device_id
|
308
|
+
@port = @vuln_key = nil
|
309
|
+
when Scope::SPECIFIC_INSTANCE_OF_SPECIFIC_ASSET
|
310
|
+
raise ArgumentError.new('No device_id.') unless @device_id
|
311
|
+
raise ArgumentError.new('Port or vuln_key is required.') unless @port || @vuln_key
|
312
|
+
else
|
313
|
+
raise ArgumentError.new("Invalid scope: #{@scope}")
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def self.parse(xml)
|
318
|
+
exception = new(xml.attributes['vuln-id'],
|
319
|
+
xml.attributes['scope'],
|
320
|
+
xml.attributes['reason'],
|
321
|
+
xml.attributes['status'])
|
322
|
+
|
323
|
+
exception.id = xml.attributes['exception-id']
|
324
|
+
exception.submitter = xml.attributes['submitter']
|
325
|
+
exception.reviewer = xml.attributes['reviewer']
|
326
|
+
exception.device_id = xml.attributes['device-id']
|
327
|
+
exception.port = xml.attributes['port-no']
|
328
|
+
exception.vuln_key = xml.attributes['vuln-key']
|
329
|
+
# TODO: Convert to Date/Time object?
|
330
|
+
exception.expiration = xml.attributes['expiration-date']
|
331
|
+
|
332
|
+
submitter_comment = xml.elements['submitter-comment']
|
333
|
+
exception.submitter_comment = submitter_comment.text if submitter_comment
|
334
|
+
reviewer_comment = xml.elements['reviewer-comment']
|
335
|
+
exception.reviewer_comment = reviewer_comment.text if reviewer_comment
|
336
|
+
|
337
|
+
exception
|
338
|
+
end
|
339
|
+
|
340
|
+
# The state of a vulnerability exception in the work flow process.
|
341
|
+
#
|
342
|
+
module Status
|
343
|
+
UNDER_REVIEW = 'Under Review'
|
344
|
+
APPROVED = 'Approved'
|
345
|
+
REJECTED = 'Rejected'
|
346
|
+
DELETED = 'Deleted'
|
347
|
+
end
|
348
|
+
|
349
|
+
# The reason for the exception status.
|
350
|
+
#
|
351
|
+
module Reason
|
352
|
+
FALSE_POSITIVE = 'False Positive'
|
353
|
+
COMPENSATING_CONTROL = 'Compensating Control'
|
354
|
+
ACCEPTABLE_USE = 'Acceptable Use'
|
355
|
+
ACCEPTABLE_RISK = 'Acceptable Risk'
|
356
|
+
OTHER = 'Other'
|
357
|
+
end
|
358
|
+
|
359
|
+
# The scope of the exception.
|
360
|
+
#
|
361
|
+
module Scope
|
362
|
+
ALL_INSTANCES = 'All Instances'
|
363
|
+
ALL_INSTANCES_ON_A_SPECIFIC_ASSET = 'All Instances on a Specific Asset'
|
364
|
+
ALL_INSTANCES_IN_A_SPECIFIC_SITE = 'All Instances in a Specific Site'
|
365
|
+
SPECIFIC_INSTANCE_OF_SPECIFIC_ASSET = 'Specific Instance of Specific Asset'
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nexpose
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- HD Moore
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-09-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: librex
|
@@ -54,25 +54,33 @@ files:
|
|
54
54
|
- README.markdown
|
55
55
|
- Rakefile
|
56
56
|
- lib/nexpose.rb
|
57
|
+
- lib/nexpose/pool.rb
|
57
58
|
- lib/nexpose/group.rb
|
59
|
+
- lib/nexpose/device.rb
|
60
|
+
- lib/nexpose/report_template.rb
|
58
61
|
- lib/nexpose/ticket.rb
|
59
62
|
- lib/nexpose/common.rb
|
60
|
-
- lib/nexpose/
|
63
|
+
- lib/nexpose/data_table.rb
|
64
|
+
- lib/nexpose/dag.rb
|
61
65
|
- lib/nexpose/creds.rb
|
62
66
|
- lib/nexpose/api_request.rb
|
63
67
|
- lib/nexpose/role.rb
|
68
|
+
- lib/nexpose/engine.rb
|
64
69
|
- lib/nexpose/manage.rb
|
65
70
|
- lib/nexpose/scan.rb
|
71
|
+
- lib/nexpose/scan_template.rb
|
66
72
|
- lib/nexpose/report.rb
|
67
73
|
- lib/nexpose/vuln.rb
|
74
|
+
- lib/nexpose/ajax.rb
|
68
75
|
- lib/nexpose/util.rb
|
69
76
|
- lib/nexpose/site.rb
|
77
|
+
- lib/nexpose/filter.rb
|
70
78
|
- lib/nexpose/alert.rb
|
71
79
|
- lib/nexpose/silo.rb
|
80
|
+
- lib/nexpose/vuln_exception.rb
|
72
81
|
- lib/nexpose/user.rb
|
73
82
|
- lib/nexpose/tags
|
74
83
|
- lib/nexpose/connection.rb
|
75
|
-
- lib/nexpose/misc.rb
|
76
84
|
- lib/nexpose/error.rb
|
77
85
|
- lib/README.md
|
78
86
|
homepage: https://github.com/rapid7/nexpose-client
|
data/lib/nexpose/misc.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module Nexpose
|
2
|
-
module NexposeAPI
|
3
|
-
include XMLUtils
|
4
|
-
|
5
|
-
def device_delete(param)
|
6
|
-
r = execute(make_xml('DeviceDeleteRequest', {'device-id' => param}))
|
7
|
-
r.success
|
8
|
-
end
|
9
|
-
|
10
|
-
# Lists all the users for the NSC along with the user details.
|
11
|
-
#
|
12
|
-
def list_users
|
13
|
-
r = execute(make_xml('UserListingRequest'))
|
14
|
-
if r.success
|
15
|
-
res = []
|
16
|
-
r.res.elements.each('//UserSummary') do |user_summary|
|
17
|
-
res << {
|
18
|
-
:auth_source => user_summary.attributes['authSource'],
|
19
|
-
:auth_module => user_summary.attributes['authModule'],
|
20
|
-
:user_name => user_summary.attributes['userName'],
|
21
|
-
:full_name => user_summary.attributes['fullName'],
|
22
|
-
:email => user_summary.attributes['email'],
|
23
|
-
:is_admin => user_summary.attributes['isAdmin'].to_s.chomp.eql?('1'),
|
24
|
-
:is_disabled => user_summary.attributes['disabled'].to_s.chomp.eql?('1'),
|
25
|
-
:site_count => user_summary.attributes['siteCount'],
|
26
|
-
:group_count => user_summary.attributes['groupCount']
|
27
|
-
}
|
28
|
-
end
|
29
|
-
res
|
30
|
-
else
|
31
|
-
false
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|