nexpose 7.0.0 → 7.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -3
- data/Gemfile.lock +1 -1
- data/lib/nexpose/ajax.rb +12 -16
- data/lib/nexpose/alert.rb +20 -21
- data/lib/nexpose/api.rb +3 -3
- data/lib/nexpose/asset.rb +23 -23
- data/lib/nexpose/blackout.rb +6 -14
- data/lib/nexpose/common.rb +87 -92
- data/lib/nexpose/connection.rb +8 -10
- data/lib/nexpose/console.rb +9 -9
- data/lib/nexpose/dag.rb +2 -2
- data/lib/nexpose/data_table.rb +8 -12
- data/lib/nexpose/device.rb +35 -34
- data/lib/nexpose/discovery.rb +69 -69
- data/lib/nexpose/discovery/filter.rb +7 -8
- data/lib/nexpose/engine.rb +22 -21
- data/lib/nexpose/error.rb +7 -5
- data/lib/nexpose/external.rb +21 -16
- data/lib/nexpose/filter.rb +51 -52
- data/lib/nexpose/global_blackout.rb +6 -7
- data/lib/nexpose/global_settings.rb +2 -3
- data/lib/nexpose/group.rb +25 -19
- data/lib/nexpose/json_serializer.rb +4 -14
- data/lib/nexpose/maint.rb +8 -9
- data/lib/nexpose/manage.rb +2 -2
- data/lib/nexpose/multi_tenant_user.rb +42 -42
- data/lib/nexpose/password_policy.rb +14 -14
- data/lib/nexpose/pool.rb +6 -5
- data/lib/nexpose/report.rb +30 -34
- data/lib/nexpose/report_template.rb +17 -18
- data/lib/nexpose/role.rb +64 -55
- data/lib/nexpose/scan.rb +77 -60
- data/lib/nexpose/scan_template.rb +17 -17
- data/lib/nexpose/scheduled_backup.rb +8 -8
- data/lib/nexpose/scheduled_maintenance.rb +9 -9
- data/lib/nexpose/shared_credential.rb +30 -33
- data/lib/nexpose/shared_secret.rb +5 -5
- data/lib/nexpose/silo.rb +68 -66
- data/lib/nexpose/silo_profile.rb +47 -50
- data/lib/nexpose/site.rb +101 -123
- data/lib/nexpose/site_credentials.rb +15 -17
- data/lib/nexpose/tag.rb +73 -80
- data/lib/nexpose/ticket.rb +45 -42
- data/lib/nexpose/user.rb +45 -45
- data/lib/nexpose/util.rb +1 -1
- data/lib/nexpose/version.rb +1 -1
- data/lib/nexpose/vuln.rb +45 -43
- data/lib/nexpose/vuln_def.rb +7 -7
- data/lib/nexpose/vuln_exception.rb +35 -36
- data/lib/nexpose/wait.rb +32 -28
- data/lib/nexpose/web_credentials.rb +34 -36
- metadata +2 -2
@@ -7,20 +7,20 @@ module Nexpose
|
|
7
7
|
CLUSTER = 'CLUSTER'
|
8
8
|
|
9
9
|
# Valid Operators: IS, IS_NOT
|
10
|
-
DATACENTER ='DATACENTER'
|
10
|
+
DATACENTER = 'DATACENTER'
|
11
11
|
|
12
12
|
# Valid Operators: CONTAINS, NOT_CONTAINS
|
13
|
-
GUEST_OS_FAMILY = 'GUEST_OS_FAMILY'
|
13
|
+
GUEST_OS_FAMILY = 'GUEST_OS_FAMILY' # Also AWS Filter
|
14
14
|
|
15
15
|
# Valid Operators: IN, NOT_IN
|
16
|
-
IP_ADDRESS_RANGE = 'IP_ADDRESS'
|
16
|
+
IP_ADDRESS_RANGE = 'IP_ADDRESS' # Also AWS Filter
|
17
17
|
|
18
18
|
# Valid Operators: IN, NOT_IN
|
19
19
|
# Valid Values (See Value::PowerState): ON, OFF, SUSPENDED
|
20
20
|
POWER_STATE = 'POWER_STATE'
|
21
21
|
|
22
22
|
# Valid Operators: CONTAINS, NOT_CONTAINS
|
23
|
-
RESOURCE_POOL_PATH ='RESOURCE_POOL_PATH'
|
23
|
+
RESOURCE_POOL_PATH = 'RESOURCE_POOL_PATH'
|
24
24
|
|
25
25
|
# Valid Operators: IS, IS_NOT, CONTAINS, NOT_CONTAINS, STARTS_WITH
|
26
26
|
VIRTUAL_MACHINE_NAME = 'VM'
|
@@ -45,7 +45,7 @@ module Nexpose
|
|
45
45
|
INSTANCE_TYPE = 'INSTANCE_TYPE'
|
46
46
|
|
47
47
|
# Valid Operators: IN, NOT_IN
|
48
|
-
REGION ='REGION'
|
48
|
+
REGION = 'REGION'
|
49
49
|
|
50
50
|
###### Mobile or Active sync Filters ######
|
51
51
|
# Valid Operators: CONTAINS, NOT_CONTAINS
|
@@ -54,13 +54,12 @@ module Nexpose
|
|
54
54
|
# Valid Operators: IS, IS_NOT, CONTAINS, NOT_CONTAINS, STARTS_WITH
|
55
55
|
USER = 'DEVICE_USER_DISPLAY_NAME'
|
56
56
|
|
57
|
-
|
58
57
|
end
|
59
58
|
|
60
59
|
module Value
|
61
60
|
module PowerState
|
62
|
-
ON
|
63
|
-
OFF
|
61
|
+
ON = 'poweredOn'
|
62
|
+
OFF = 'poweredOff'
|
64
63
|
SUSPENDED = 'suspended'
|
65
64
|
end
|
66
65
|
end
|
data/lib/nexpose/engine.rb
CHANGED
@@ -10,8 +10,7 @@ module Nexpose
|
|
10
10
|
# @return [Boolean] true if engine successfully deleted.
|
11
11
|
#
|
12
12
|
def delete_engine(engine_id, scope = 'silo')
|
13
|
-
xml = make_xml('EngineDeleteRequest',
|
14
|
-
{'engine-id' => engine_id, 'scope' => scope})
|
13
|
+
xml = make_xml('EngineDeleteRequest', { 'engine-id' => engine_id, 'scope' => scope })
|
15
14
|
response = execute(xml, '1.2')
|
16
15
|
response.success
|
17
16
|
end
|
@@ -27,9 +26,9 @@ module Nexpose
|
|
27
26
|
# @return [Boolean] true if the connection is successfully reversed.
|
28
27
|
#
|
29
28
|
def reverse_engine_connection(engine_id)
|
30
|
-
uri
|
29
|
+
uri = "/api/2.1/engine/#{engine_id}/reverseConnection"
|
31
30
|
response = AJAX.put(self, uri)
|
32
|
-
response.eql?(
|
31
|
+
response.eql?('true')
|
33
32
|
end
|
34
33
|
|
35
34
|
# Kicks off an update on a single engine.
|
@@ -54,8 +53,8 @@ module Nexpose
|
|
54
53
|
# each active scan on the engine.
|
55
54
|
#
|
56
55
|
def engine_activity(engine_id)
|
57
|
-
xml = make_xml('EngineActivityRequest', {'engine-id' => engine_id})
|
58
|
-
r
|
56
|
+
xml = make_xml('EngineActivityRequest', { 'engine-id' => engine_id })
|
57
|
+
r = execute(xml)
|
59
58
|
arr = []
|
60
59
|
if r.success
|
61
60
|
r.res.elements.each('//ScanSummary') do |scan_event|
|
@@ -72,7 +71,7 @@ module Nexpose
|
|
72
71
|
#
|
73
72
|
def list_engines
|
74
73
|
response = execute(make_xml('EngineListingRequest'))
|
75
|
-
arr
|
74
|
+
arr = []
|
76
75
|
if response.success
|
77
76
|
response.res.elements.each('//EngineSummary') do |engine|
|
78
77
|
arr << EngineSummary.new(engine.attributes['id'].to_i,
|
@@ -86,7 +85,7 @@ module Nexpose
|
|
86
85
|
arr
|
87
86
|
end
|
88
87
|
|
89
|
-
|
88
|
+
alias engines list_engines
|
90
89
|
end
|
91
90
|
|
92
91
|
# Object representing the current details of a scan engine attached to the
|
@@ -110,12 +109,12 @@ module Nexpose
|
|
110
109
|
attr_reader :scope
|
111
110
|
|
112
111
|
def initialize(id, name, address, port, status, scope = 'silo')
|
113
|
-
@id
|
114
|
-
@name
|
112
|
+
@id = id
|
113
|
+
@name = name
|
115
114
|
@address = address
|
116
|
-
@port
|
117
|
-
@status
|
118
|
-
@scope
|
115
|
+
@port = port
|
116
|
+
@status = status
|
117
|
+
@scope = scope
|
119
118
|
end
|
120
119
|
end
|
121
120
|
|
@@ -143,13 +142,13 @@ module Nexpose
|
|
143
142
|
attr_accessor :sites
|
144
143
|
|
145
144
|
def initialize(address, name = nil, port = 40814)
|
146
|
-
@id
|
145
|
+
@id = -1
|
147
146
|
@address = address
|
148
|
-
@name
|
149
|
-
@name
|
150
|
-
@port
|
151
|
-
@scope
|
152
|
-
@sites
|
147
|
+
@name = name
|
148
|
+
@name ||= address
|
149
|
+
@port = port
|
150
|
+
@scope = 'silo'
|
151
|
+
@sites = []
|
153
152
|
end
|
154
153
|
|
155
154
|
def self.load(connection, id)
|
@@ -163,12 +162,14 @@ module Nexpose
|
|
163
162
|
engine = Engine.new(config.attributes['address'],
|
164
163
|
config.attributes['name'],
|
165
164
|
config.attributes['port'])
|
166
|
-
engine.id
|
167
|
-
engine.scope
|
165
|
+
engine.id = config.attributes['id']
|
166
|
+
engine.scope = config.attributes['scope'] if config.attributes['scope']
|
168
167
|
engine.priority = config.attributes['priority'] if config.attributes['priority']
|
168
|
+
|
169
169
|
config.elements.each('Site') do |site|
|
170
170
|
engine.sites << SiteSummary.new(site.attributes['id'], site.attributes['name'])
|
171
171
|
end
|
172
|
+
|
172
173
|
return engine
|
173
174
|
end
|
174
175
|
end
|
data/lib/nexpose/error.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module Nexpose
|
2
|
+
|
2
3
|
class APIError < ::RuntimeError
|
3
4
|
attr_accessor :req, :reason
|
4
5
|
attr_reader :code
|
5
6
|
|
6
7
|
def initialize(req, reason = '', code = 400)
|
7
|
-
@req
|
8
|
+
@req = req
|
8
9
|
@reason = reason
|
9
|
-
@code
|
10
|
+
@code = code
|
10
11
|
end
|
11
12
|
|
12
13
|
def to_s
|
@@ -16,15 +17,16 @@ module Nexpose
|
|
16
17
|
|
17
18
|
class AuthenticationFailed < APIError
|
18
19
|
def initialize(req)
|
19
|
-
@req
|
20
|
-
@reason =
|
20
|
+
@req = req
|
21
|
+
@reason = 'Login Failed'
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
25
|
class PermissionError < APIError
|
25
26
|
def initialize(req)
|
26
|
-
@req
|
27
|
+
@req = req
|
27
28
|
@reason = 'User does not have permission to perform this action.'
|
28
29
|
end
|
29
30
|
end
|
31
|
+
|
30
32
|
end
|
data/lib/nexpose/external.rb
CHANGED
@@ -27,10 +27,10 @@ module Nexpose
|
|
27
27
|
# @return [Array[ImportResult]] collection of import results.
|
28
28
|
#
|
29
29
|
def import_assets_from_json(site_id, json)
|
30
|
-
uri
|
30
|
+
uri = "/api/2.1/sites/#{site_id}/assets"
|
31
31
|
# Wait up to 5 minutes for a response.
|
32
32
|
resp = AJAX.post(self, uri, json, AJAX::CONTENT_TYPE::JSON, 300)
|
33
|
-
arr
|
33
|
+
arr = JSON.parse(resp, symbolize_names: true)
|
34
34
|
arr.map { |e| External::ImportResult.new.object_from_hash(self, e) }
|
35
35
|
end
|
36
36
|
end
|
@@ -87,15 +87,15 @@ module Nexpose
|
|
87
87
|
attr_accessor :vulnerabilities
|
88
88
|
|
89
89
|
def initialize
|
90
|
-
@aliases
|
91
|
-
@software
|
92
|
-
@services
|
93
|
-
@attributes
|
94
|
-
@users
|
95
|
-
@groups
|
96
|
-
@files
|
90
|
+
@aliases = []
|
91
|
+
@software = []
|
92
|
+
@services = []
|
93
|
+
@attributes = []
|
94
|
+
@users = []
|
95
|
+
@groups = []
|
96
|
+
@files = []
|
97
97
|
@unique_identifiers = []
|
98
|
-
@vulnerabilities
|
98
|
+
@vulnerabilities = []
|
99
99
|
end
|
100
100
|
|
101
101
|
def to_json
|
@@ -123,10 +123,10 @@ module Nexpose
|
|
123
123
|
|
124
124
|
# Valid host types for an asset.
|
125
125
|
module HostType
|
126
|
-
GUEST
|
126
|
+
GUEST = 'GUEST'
|
127
127
|
HYPERVISOR = 'HYPERVISOR'
|
128
|
-
PHYSICAL
|
129
|
-
MOBILE
|
128
|
+
PHYSICAL = 'PHYSICAL'
|
129
|
+
MOBILE = 'MOBILE'
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
@@ -143,7 +143,9 @@ module Nexpose
|
|
143
143
|
attr_accessor :vulnerabilities
|
144
144
|
|
145
145
|
def initialize(port, protocol = Protocol::RAW, name = nil)
|
146
|
-
@port
|
146
|
+
@port = port
|
147
|
+
@protocol = protocol
|
148
|
+
@name = name
|
147
149
|
@vulnerabilities = []
|
148
150
|
end
|
149
151
|
|
@@ -169,7 +171,10 @@ module Nexpose
|
|
169
171
|
attr_accessor :proof
|
170
172
|
|
171
173
|
def initialize(vuln_id, status = Status::EXPLOITED, proof = nil, key = nil)
|
172
|
-
@vuln_id
|
174
|
+
@vuln_id = vuln_id
|
175
|
+
@status = status
|
176
|
+
@proof = proof
|
177
|
+
@key = key
|
173
178
|
end
|
174
179
|
|
175
180
|
def to_h
|
@@ -185,7 +190,7 @@ module Nexpose
|
|
185
190
|
EXPLOITED = 'vulnerable-exploited'
|
186
191
|
# Vulnerable because the service or software version is associated with
|
187
192
|
# a known vulnerability.
|
188
|
-
VERSION
|
193
|
+
VERSION = 'vulnerable-version'
|
189
194
|
# A potential vulnerability.
|
190
195
|
POTENTIAL = 'potential'
|
191
196
|
end
|
data/lib/nexpose/filter.rb
CHANGED
@@ -14,7 +14,7 @@ module Nexpose
|
|
14
14
|
#
|
15
15
|
def filter(field, operator, value = '')
|
16
16
|
criterion = Criterion.new(field, operator, value)
|
17
|
-
criteria
|
17
|
+
criteria = Criteria.new(criterion)
|
18
18
|
search(criteria)
|
19
19
|
end
|
20
20
|
|
@@ -34,9 +34,7 @@ module Nexpose
|
|
34
34
|
# @return [Array[FilteredAsset]] List of matching assets.
|
35
35
|
#
|
36
36
|
def search(criteria)
|
37
|
-
results = DataTable._get_json_table(self,
|
38
|
-
'/data/asset/filterAssets',
|
39
|
-
criteria._to_payload)
|
37
|
+
results = DataTable._get_json_table(self, '/data/asset/filterAssets', criteria._to_payload)
|
40
38
|
results.map { |a| FilteredAsset.new(a) }
|
41
39
|
end
|
42
40
|
end
|
@@ -173,32 +171,32 @@ module Nexpose
|
|
173
171
|
# List of acceptable operators. Not all fields accept all operators.
|
174
172
|
#
|
175
173
|
module Operator
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
174
|
+
ARE = 'ARE'
|
175
|
+
BETWEEN = 'BETWEEN'
|
176
|
+
CONTAINS = 'CONTAINS'
|
177
|
+
DO_NOT_INCLUDE = 'DO_NOT_INCLUDE'
|
178
|
+
EARLIER_THAN = 'EARLIER_THAN'
|
179
|
+
ENDS_WITH = 'ENDS_WITH'
|
180
|
+
GREATER_THAN = 'GREATER_THAN'
|
181
|
+
IN = 'IN'
|
182
|
+
INCLUDE = 'INCLUDE'
|
183
|
+
IN_RANGE = 'IN_RANGE'
|
184
|
+
IS = 'IS'
|
185
|
+
IS_APPLIED = 'IS_APPLIED'
|
186
|
+
IS_EMPTY = 'IS_EMPTY'
|
187
|
+
IS_NOT = 'IS_NOT'
|
188
|
+
IS_NOT_APPLIED = 'IS_NOT_APPLIED'
|
189
|
+
IS_NOT_EMPTY = 'IS_NOT_EMPTY'
|
190
|
+
LESS_THAN = 'LESS_THAN'
|
191
|
+
LIKE = 'LIKE'
|
192
|
+
NOT_CONTAINS = 'NOT_CONTAINS'
|
193
|
+
NOT_IN = 'NOT_IN'
|
194
|
+
NOT_IN_RANGE = 'NOT_IN_RANGE'
|
195
|
+
NOT_LIKE = 'NOT_LIKE'
|
196
|
+
ON_OR_AFTER = 'ON_OR_AFTER'
|
197
|
+
ON_OR_BEFORE = 'ON_OR_BEFORE'
|
198
|
+
STARTS_WITH = 'STARTS_WITH'
|
191
199
|
WITHIN_THE_LAST = 'WITHIN_THE_LAST'
|
192
|
-
GREATER_THAN = 'GREATER_THAN'
|
193
|
-
LESS_THAN = 'LESS_THAN'
|
194
|
-
IS_EMPTY = 'IS_EMPTY'
|
195
|
-
IS_NOT_EMPTY = 'IS_NOT_EMPTY'
|
196
|
-
INCLUDE = 'INCLUDE'
|
197
|
-
DO_NOT_INCLUDE = 'DO_NOT_INCLUDE'
|
198
|
-
IS_APPLIED = 'IS_APPLIED'
|
199
|
-
IS_NOT_APPLIED = 'IS_NOT_APPLIED'
|
200
|
-
LIKE = 'LIKE'
|
201
|
-
NOT_LIKE = 'NOT_LIKE'
|
202
200
|
end
|
203
201
|
|
204
202
|
# Specialized values used by certain search fields
|
@@ -206,36 +204,36 @@ module Nexpose
|
|
206
204
|
module Value
|
207
205
|
# Constants for filtering on access complexity.
|
208
206
|
module AccessComplexity
|
209
|
-
LOW
|
207
|
+
LOW = 'L'
|
210
208
|
MEDIUM = 'M'
|
211
|
-
HIGH
|
209
|
+
HIGH = 'H'
|
212
210
|
end
|
213
211
|
|
214
212
|
# Constants for filtering on access vector.
|
215
213
|
module AccessVector
|
216
|
-
LOCAL
|
214
|
+
LOCAL = 'L'
|
217
215
|
ADJACENT = 'A'
|
218
|
-
NETWORK
|
216
|
+
NETWORK = 'N'
|
219
217
|
end
|
220
218
|
|
221
219
|
# Constants for filtering on whether authentication is required.
|
222
220
|
module AuthenticationRequired
|
223
|
-
NONE
|
224
|
-
SINGLE
|
221
|
+
NONE = 'N'
|
222
|
+
SINGLE = 'S'
|
225
223
|
MULTIPLE = 'M'
|
226
224
|
end
|
227
225
|
|
228
226
|
# Constants for filtering on CVSS impact.
|
229
227
|
module CVSSImpact
|
230
|
-
NONE
|
231
|
-
PARTIAL
|
228
|
+
NONE = 'N'
|
229
|
+
PARTIAL = 'P'
|
232
230
|
COMPLETE = 'C'
|
233
231
|
end
|
234
232
|
|
235
233
|
# Constants for filtering on host type.
|
236
234
|
module HostType
|
237
|
-
UNKNOWN
|
238
|
-
VIRTUAL
|
235
|
+
UNKNOWN = '0'
|
236
|
+
VIRTUAL = '1'
|
239
237
|
HYPERVISOR = '2'
|
240
238
|
BARE_METAL = '3'
|
241
239
|
end
|
@@ -263,15 +261,15 @@ module Nexpose
|
|
263
261
|
# Constants for filtering on vulnerability validations.
|
264
262
|
module ValidatedVulnerability
|
265
263
|
NOT_PRESENT = 1
|
266
|
-
PRESENT
|
264
|
+
PRESENT = 0
|
267
265
|
end
|
268
266
|
|
269
267
|
# Constants for filtering on vulnerability exposure.
|
270
268
|
module VulnerabilityExposure
|
271
|
-
MALWARE
|
269
|
+
MALWARE = 'type:"malware_type", name:"malwarekit"'
|
272
270
|
# TODO: A problem in Nexpose causes these values to not be constant.
|
273
271
|
METASPLOIT = 'type:"exploit_source_type", name:"2"'
|
274
|
-
DATABASE
|
272
|
+
DATABASE = 'type:"exploit_source_type", name:"1"'
|
275
273
|
end
|
276
274
|
end
|
277
275
|
end
|
@@ -288,7 +286,8 @@ module Nexpose
|
|
288
286
|
attr_accessor :value
|
289
287
|
|
290
288
|
def initialize(field, operator, value = '')
|
291
|
-
@field
|
289
|
+
@field = field.upcase
|
290
|
+
@operator = operator.upcase
|
292
291
|
if value.is_a? Array
|
293
292
|
@value = value.map(&:to_s)
|
294
293
|
else
|
@@ -384,17 +383,17 @@ module Nexpose
|
|
384
383
|
attr_reader :last_scan
|
385
384
|
|
386
385
|
def initialize(json)
|
387
|
-
@id
|
388
|
-
@ip
|
389
|
-
@name
|
390
|
-
@os
|
386
|
+
@id = json['assetID']
|
387
|
+
@ip = json['assetIP']
|
388
|
+
@name = json['assetName']
|
389
|
+
@os = json['assetOSName']
|
391
390
|
@exploit_count = json['exploitCount'].to_i
|
392
391
|
@malware_count = json['malwareCount'].to_i
|
393
|
-
@vuln_count
|
394
|
-
@risk_score
|
395
|
-
@site_ids
|
396
|
-
@site_id
|
397
|
-
@last_scan
|
392
|
+
@vuln_count = json['vulnCount'].to_i
|
393
|
+
@risk_score = json['riskScore'].to_f
|
394
|
+
@site_ids = json['sitePermissions'].map { |site| site['siteID'] }
|
395
|
+
@site_id = @site_ids.first
|
396
|
+
@last_scan = Time.at(json['lastScanDate'].to_i / 1000)
|
398
397
|
end
|
399
398
|
end
|
400
399
|
end
|
@@ -18,8 +18,7 @@ module Nexpose
|
|
18
18
|
|
19
19
|
def to_h
|
20
20
|
{
|
21
|
-
|
22
|
-
(@blackout || []).map { |blackout| blackout.to_h }
|
21
|
+
blackouts: (@blackout || []).map(&:to_h)
|
23
22
|
}
|
24
23
|
end
|
25
24
|
|
@@ -32,11 +31,11 @@ module Nexpose
|
|
32
31
|
end
|
33
32
|
|
34
33
|
def self.load(nsc)
|
35
|
-
uri
|
36
|
-
resp
|
37
|
-
hash
|
38
|
-
blackout
|
39
|
-
blackout.blackout = (hash[:blackouts] || []).map { |
|
34
|
+
uri = '/api/2.1/silo_blackout/'
|
35
|
+
resp = AJAX.get(nsc, uri, AJAX::CONTENT_TYPE::JSON)
|
36
|
+
hash = JSON.parse(resp, symbolize_names: true)
|
37
|
+
blackout = self.json_initializer(hash).deserialize(hash)
|
38
|
+
blackout.blackout = (hash[:blackouts] || []).map { |bout| Nexpose::Blackout.from_hash(bout) }
|
40
39
|
blackout
|
41
40
|
end
|
42
41
|
end
|