tenable-ruby 0.2
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/lib/error/authentication_error.rb +6 -0
- data/lib/tenable-ruby.rb +765 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a7f7c335ff236793a5687cc416a23398aec7264e
|
4
|
+
data.tar.gz: 50be6ad12e2f3f4bd6a75c43d83af180eca6483a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9cdcd8832b724c65bc9217497d77c501e669a9f35bff4d9935943a91fecde7d43bd1908e55da271d82301e13b32679ac79bbf4d2e1d54dd4cac176d0d76a98e4
|
7
|
+
data.tar.gz: 8d34cc3920773f68f329ce96fe137e723a1f8b8344cd7262610a30a8f0041362073687cbb88d14f427663adc8274a29558f4e6f3b2d93e832c0629dd47235080
|
data/lib/tenable-ruby.rb
ADDED
@@ -0,0 +1,765 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
# = tenable-ruby.rb: Unofficial Ruby library for communicating with the tenable.io API
|
4
|
+
#
|
5
|
+
# Authors:: Vlatko Kosturjak, Patrick Craston
|
6
|
+
#
|
7
|
+
# (C) Vlatko Kosturjak, Kost. Distributed under MIT license.
|
8
|
+
#
|
9
|
+
# == What is this library?
|
10
|
+
#
|
11
|
+
# Unofficial Ruby library for communicating with the tenable.io API (also works with Nessus 6 API).
|
12
|
+
# You can start, stop, pause and resume scans. Get status of scans, download reports, create policies, etc.
|
13
|
+
# Based on the excellent library for interacting with Nessus
|
14
|
+
# https://github.com/kost/nessus_rest-ruby by https://github.com/kost.
|
15
|
+
#
|
16
|
+
# == Requirements
|
17
|
+
#
|
18
|
+
# Standard Ruby libraries: uri, net/https and json.
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'openssl'
|
22
|
+
require 'uri'
|
23
|
+
require 'net/http'
|
24
|
+
require 'net/https'
|
25
|
+
require 'json'
|
26
|
+
require 'error/authentication_error'
|
27
|
+
|
28
|
+
module TenableRuby
|
29
|
+
class Client
|
30
|
+
attr_accessor :quick_defaults
|
31
|
+
attr_accessor :defsleep, :httpsleep, :httpretry, :ssl_use, :ssl_verify, :autologin
|
32
|
+
attr_reader :header
|
33
|
+
|
34
|
+
class << self
|
35
|
+
@connection
|
36
|
+
end
|
37
|
+
|
38
|
+
# initialize quick scan defaults: these will be used when not specifying defaults
|
39
|
+
#
|
40
|
+
# Usage:
|
41
|
+
#
|
42
|
+
# n.init_quick_defaults()
|
43
|
+
def init_quick_defaults
|
44
|
+
@quick_defaults = Hash.new
|
45
|
+
@quick_defaults['enabled'] = false
|
46
|
+
@quick_defaults['launch'] = 'ONETIME'
|
47
|
+
@quick_defaults['launch_now'] = true
|
48
|
+
@quick_defaults['description'] = 'Created with tenable-ruby https//gitlab.com/intruder/tenable-ruby'
|
49
|
+
end
|
50
|
+
|
51
|
+
# initialize object: try to connect to tenable.io
|
52
|
+
# Usage:
|
53
|
+
#
|
54
|
+
# TenableRuby::Client.new (:credentials => {username: 'user', password: 'password'})
|
55
|
+
# or
|
56
|
+
# TenableRuby::Client.new (:credentials => {access_key: 'XXX', secret_key: 'XXX'})
|
57
|
+
#
|
58
|
+
# default url is set to tenable.io, change to Nessus appliance url if required, e.g.
|
59
|
+
# TenableRuby::Client.new (:url => 'https://nessus_url:8834',
|
60
|
+
# :credentials => {access_key: 'XXX', secret_key: 'XXX'})
|
61
|
+
def initialize(params = {})
|
62
|
+
# defaults
|
63
|
+
@tenable_url = params.fetch(:url, 'https://cloud.tenable.com')
|
64
|
+
@credentials = params.fetch(:credentials)
|
65
|
+
@ssl_verify = params.fetch(:ssl_verify, false)
|
66
|
+
@ssl_use = params.fetch(:ssl_use, true)
|
67
|
+
@autologin = params.fetch(:autologin, true)
|
68
|
+
@defsleep = params.fetch(:defsleep, 1)
|
69
|
+
@httpretry = params.fetch(:httpretry, 3)
|
70
|
+
@httpsleep = params.fetch(:httpsleep, 1)
|
71
|
+
|
72
|
+
init_quick_defaults
|
73
|
+
|
74
|
+
uri = URI.parse(@tenable_url)
|
75
|
+
@connection = Net::HTTP.new(uri.host, uri.port)
|
76
|
+
@connection.use_ssl = @ssl_use
|
77
|
+
|
78
|
+
if @ssl_verify
|
79
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
80
|
+
else
|
81
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
82
|
+
end
|
83
|
+
|
84
|
+
yield @connection if block_given?
|
85
|
+
authenticate if @autologin
|
86
|
+
end
|
87
|
+
|
88
|
+
# Tries to authenticate to the tenable.io REST JSON interface using username/password or API keys
|
89
|
+
def authenticate
|
90
|
+
if @credentials[:username] and @credentials[:password]
|
91
|
+
payload = {
|
92
|
+
:username => @credentials[:username],
|
93
|
+
:password => @credentials[:password],
|
94
|
+
:json => 1,
|
95
|
+
:authenticationmethod => true
|
96
|
+
}
|
97
|
+
res = http_post(:uri => "/session", :data => payload)
|
98
|
+
if res['token']
|
99
|
+
@token = "token=#{res['token']}"
|
100
|
+
@header = {'X-Cookie' => @token}
|
101
|
+
else
|
102
|
+
fail NessusREST::Error::AuthenticationError, "Authentication failed. Could not authenticate using
|
103
|
+
username/password."
|
104
|
+
end
|
105
|
+
elsif @credentials[:access_key] and @credentials[:secret_key]
|
106
|
+
@header = {'X-ApiKeys' => "accessKey=#{@credentials[:access_key]}; secretKey=#{@credentials[:secret_key]}"}
|
107
|
+
else
|
108
|
+
fail NessusREST::Error::AuthenticationError, "Authentication credentials were not provided. You must provide" \
|
109
|
+
" either a username and password or an API access key and secret key (these can be generated at " \
|
110
|
+
"https://cloud.tenable.com/app.html#/settings/my-account/api-keys."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the server version and other properties
|
115
|
+
#
|
116
|
+
# Reference:
|
117
|
+
# https://cloud.tenable.com/api#/resources/server/properties
|
118
|
+
def get_server_properties
|
119
|
+
http_get(:uri => "/server/properties", :fields => header)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Creates a new user
|
123
|
+
#
|
124
|
+
# Reference:
|
125
|
+
# https://cloud.tenable.com/api#/resources/users/create
|
126
|
+
def user_add(username, password, permissions, type)
|
127
|
+
payload = {
|
128
|
+
:username => username,
|
129
|
+
:password => password,
|
130
|
+
:permissions => permissions,
|
131
|
+
:type => type,
|
132
|
+
:json => 1
|
133
|
+
}
|
134
|
+
http_post(:uri => "/users", :fields => header, :data => payload)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Deletes a user
|
138
|
+
#
|
139
|
+
# Reference:
|
140
|
+
# https://cloud.tenable.com/api#/resources/users/delete
|
141
|
+
def user_delete(user_id)
|
142
|
+
res = http_delete(:uri => "/users/#{user_id}", :fields => header)
|
143
|
+
res.code
|
144
|
+
end
|
145
|
+
|
146
|
+
# Changes the password for the given user
|
147
|
+
#
|
148
|
+
# Reference:
|
149
|
+
# https://cloud.tenable.com/api#/resources/users/password
|
150
|
+
def user_chpasswd(user_id, password)
|
151
|
+
payload = {
|
152
|
+
:password => password,
|
153
|
+
:json => 1
|
154
|
+
}
|
155
|
+
res = http_put(:uri => "/users/#{user_id}/chpasswd", :data => payload, :fields => header)
|
156
|
+
res.code
|
157
|
+
end
|
158
|
+
|
159
|
+
# Logs the current user out and destroys the session
|
160
|
+
#
|
161
|
+
# Reference:
|
162
|
+
# https://cloud.tenable.com/api#/resources/session/destroy
|
163
|
+
def user_logout
|
164
|
+
res = http_delete(:uri => "/session", :fields => header)
|
165
|
+
res.code
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the policy list
|
169
|
+
#
|
170
|
+
# Reference:
|
171
|
+
# https://cloud.tenable.com/api#/resources/policies/list
|
172
|
+
def list_policies
|
173
|
+
http_get(:uri => "/policies", :fields => header)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns the user list
|
177
|
+
#
|
178
|
+
# Reference:
|
179
|
+
# https://cloud.tenable.com/api#/resources/users/list
|
180
|
+
def list_users
|
181
|
+
http_get(:uri => "/users", :fields => header)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns the current user's scan folders
|
185
|
+
#
|
186
|
+
# Reference:
|
187
|
+
# https://cloud.tenable.com/api#/resources/folders/list
|
188
|
+
def list_folders
|
189
|
+
http_get(:uri => "/folders", :fields => header)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns the scanner list
|
193
|
+
#
|
194
|
+
# Reference:
|
195
|
+
# https://cloud.tenable.com/api#/resources/scanners/list
|
196
|
+
def list_scanners
|
197
|
+
http_get(:uri => "/scanners", :fields => header)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns the list of plugin families
|
201
|
+
#
|
202
|
+
# Reference:
|
203
|
+
# https://cloud.tenable.com/api#/resources/plugins/families
|
204
|
+
def list_families
|
205
|
+
http_get(:uri => "/plugins/families", :fields => header)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the list of plugins in a family
|
209
|
+
#
|
210
|
+
# Reference:
|
211
|
+
# https://cloud.tenable.com/api#/resources/plugins/family-details
|
212
|
+
def list_plugins(family_id)
|
213
|
+
http_get(:uri => "/plugins/families/#{family_id}", :fields => header)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the template list
|
217
|
+
#
|
218
|
+
# Reference:
|
219
|
+
# https://cloud.tenable.com/api#/resources/editor/list
|
220
|
+
def list_templates(type)
|
221
|
+
http_get(:uri => "/editor/#{type}/templates", :fields => header)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Returns details for the given template
|
225
|
+
#
|
226
|
+
# Reference:
|
227
|
+
# https://cloud.tenable.com/api#/resources/editor/template-details
|
228
|
+
def editor_templates (type, uuid)
|
229
|
+
http_get(:uri => "/editor/#{type}/templates/#{uuid}", :fields => header)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns details for a given plugin
|
233
|
+
#
|
234
|
+
# Reference:
|
235
|
+
# https://cloud.tenable.com/api#/resources/plugins/plugin-details
|
236
|
+
def plugin_details(plugin_id)
|
237
|
+
http_get(:uri => "/plugins/plugin/#{plugin_id}", :fields => header)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns the server status
|
241
|
+
#
|
242
|
+
# Reference:
|
243
|
+
# https://cloud.tenable.com/api#/resources/server/status
|
244
|
+
def server_status
|
245
|
+
http_get(:uri => "/server/status", :fields => header)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Creates a scan
|
249
|
+
#
|
250
|
+
# Reference:
|
251
|
+
# https://cloud.tenable.com/api#/resources/scans/create
|
252
|
+
def scan_create(uuid, settings)
|
253
|
+
payload = {
|
254
|
+
:uuid => uuid,
|
255
|
+
:settings => settings,
|
256
|
+
:json => 1
|
257
|
+
}.to_json
|
258
|
+
http_post(:uri => "/scans", :body => payload, :fields => header, :ctype => 'application/json')
|
259
|
+
end
|
260
|
+
|
261
|
+
# Launches a scan
|
262
|
+
#
|
263
|
+
# Reference:
|
264
|
+
# https://cloud.tenable.com/api#/resources/scans/launch
|
265
|
+
def scan_launch(scan_id)
|
266
|
+
http_post(:uri => "/scans/#{scan_id}/launch", :fields => header)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Get List of Scans
|
270
|
+
#
|
271
|
+
# Reference:
|
272
|
+
# https://cloud.tenable.com/api#/resources/scans/list
|
273
|
+
def scan_list
|
274
|
+
http_get(:uri => "/scans", :fields => header)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns details for the given scan
|
278
|
+
#
|
279
|
+
# Reference:
|
280
|
+
# https://cloud.tenable.com/api#/resources/scans/details
|
281
|
+
def scan_details(scan_id)
|
282
|
+
http_get(:uri => "/scans/#{scan_id}", :fields => header)
|
283
|
+
end
|
284
|
+
|
285
|
+
# Pauses a scan
|
286
|
+
#
|
287
|
+
# Reference:
|
288
|
+
# https://cloud.tenable.com/api#/resources/scans/pause
|
289
|
+
def scan_pause(scan_id)
|
290
|
+
http_post(:uri => "/scans/#{scan_id}/pause", :fields => header)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Resumes a scan
|
294
|
+
#
|
295
|
+
# Reference:
|
296
|
+
# https://cloud.tenable.com/api#/resources/scans/resume
|
297
|
+
def scan_resume(scan_id)
|
298
|
+
http_post(:uri => "/scans/#{scan_id}/resume", :fields => header)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Stops a scan
|
302
|
+
#
|
303
|
+
# Reference:
|
304
|
+
# https://cloud.tenable.com/api#/resources/scans/stop
|
305
|
+
def scan_stop(scan_id)
|
306
|
+
http_post(:uri => "/scans/#{scan_id}/stop", :fields => header)
|
307
|
+
end
|
308
|
+
|
309
|
+
# Export the given scan. Once requested, the file can be downloaded using the export download method
|
310
|
+
# upon receiving a "ready" status from the export status method.
|
311
|
+
#
|
312
|
+
# Reference:
|
313
|
+
# https://cloud.tenable.com/api#/resources/scans/export-request
|
314
|
+
def scan_export(scan_id, format)
|
315
|
+
payload = {
|
316
|
+
:format => format
|
317
|
+
}.to_json
|
318
|
+
http_post(:uri => "/scans/#{scan_id}/export", :body => payload, :ctype => 'application/json', :fields => header)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Check the file status of an exported scan. When an export has been requested, it is necessary to poll this
|
322
|
+
# endpoint until a "ready" status is returned, at which point the file is complete and can be downloaded
|
323
|
+
# using the export download endpoint.
|
324
|
+
#
|
325
|
+
# Reference:
|
326
|
+
# https://cloud.tenable.com/api#/resources/scans/export-status
|
327
|
+
def scan_export_status(scan_id, file_id)
|
328
|
+
http_get(:uri => "/scans/#{scan_id}/export/#{file_id}/status", :fields => header)
|
329
|
+
end
|
330
|
+
|
331
|
+
# Deletes a scan. NOTE: Scans in running, paused or stopping states can not be deleted.
|
332
|
+
#
|
333
|
+
# Reference:
|
334
|
+
# https://cloud.tenable.com/api#/resources/scans/delete
|
335
|
+
def scan_delete(scan_id)
|
336
|
+
res = http_delete(:uri => "/scans/#{scan_id}", :fields => header)
|
337
|
+
if res.code == 200
|
338
|
+
true
|
339
|
+
else
|
340
|
+
false
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Returns details for the given host
|
345
|
+
#
|
346
|
+
# Reference:
|
347
|
+
# https://cloud.tenable.com/api#/resources/scans/host-details
|
348
|
+
def host_details(scan_id, host_id, history_id: nil)
|
349
|
+
uri = "/scans/#{scan_id}/hosts/#{host_id}"
|
350
|
+
unless history_id.nil?
|
351
|
+
uri += "?history_id=#{history_id}"
|
352
|
+
end
|
353
|
+
http_get(:uri => uri, :fields => header)
|
354
|
+
end
|
355
|
+
|
356
|
+
# Download an exported scan
|
357
|
+
#
|
358
|
+
# Reference:
|
359
|
+
# https://cloud.tenable.com/api#/resources/scans/export-download
|
360
|
+
def report_download(scan_id, file_id)
|
361
|
+
http_get(:uri => "/scans/#{scan_id}/export/#{file_id}/download", :raw_content => true, :fields => header)
|
362
|
+
end
|
363
|
+
|
364
|
+
# Returns details for the given policy
|
365
|
+
#
|
366
|
+
# Reference:
|
367
|
+
# https://cloud.tenable.com/api#/resources/scans/host-details
|
368
|
+
def policy_details(policy_id)
|
369
|
+
http_get(:uri => "/policies/#{policy_id}", :fields => header)
|
370
|
+
end
|
371
|
+
|
372
|
+
# Creates a policy
|
373
|
+
#
|
374
|
+
# Reference:
|
375
|
+
# https://cloud.tenable.com/api#/resources/policies/create
|
376
|
+
def policy_create(template_id, plugins, settings)
|
377
|
+
options = {
|
378
|
+
:uri => "/policies/",
|
379
|
+
:fields => header,
|
380
|
+
:ctype => 'application/json',
|
381
|
+
:body => {
|
382
|
+
:uuid => template_id,
|
383
|
+
:audits => {},
|
384
|
+
:credentials => {delete: []},
|
385
|
+
:plugins => plugins,
|
386
|
+
:settings => settings
|
387
|
+
}.to_json
|
388
|
+
}
|
389
|
+
http_post(options)
|
390
|
+
end
|
391
|
+
|
392
|
+
# Copy a policy
|
393
|
+
#
|
394
|
+
# Reference:
|
395
|
+
# https://cloud.tenable.com/api#/resources/policies/copy
|
396
|
+
def policy_copy(policy_id)
|
397
|
+
options = {
|
398
|
+
:uri => "/policies/#{policy_id}/copy",
|
399
|
+
:fields => header,
|
400
|
+
:ctype => 'application/json'
|
401
|
+
}
|
402
|
+
http_post(options)
|
403
|
+
end
|
404
|
+
|
405
|
+
# Changes the parameters of a policy
|
406
|
+
#
|
407
|
+
# Reference:
|
408
|
+
# https://cloud.tenable.com/api#/resources/policies/configure
|
409
|
+
def policy_configure(policy_id, template_id, plugins, settings)
|
410
|
+
options = {
|
411
|
+
:uri => "/policies/#{policy_id}",
|
412
|
+
:fields => header,
|
413
|
+
:ctype => 'application/json',
|
414
|
+
:body => {
|
415
|
+
:uuid => template_id,
|
416
|
+
:audits => {},
|
417
|
+
:credentials => {delete: []},
|
418
|
+
:plugins => plugins,
|
419
|
+
:settings => settings
|
420
|
+
}.to_json
|
421
|
+
}
|
422
|
+
http_put(options)
|
423
|
+
end
|
424
|
+
|
425
|
+
# Delete a policy
|
426
|
+
#
|
427
|
+
# Reference:
|
428
|
+
# https://cloud.tenable.com/api#/resources/policies/delete
|
429
|
+
def policy_delete(policy_id)
|
430
|
+
res = http_delete(:uri => "/policies/#{policy_id}", :fields => header)
|
431
|
+
res.code
|
432
|
+
end
|
433
|
+
|
434
|
+
# Schedules a software update for all components (only Nessus 6)
|
435
|
+
#
|
436
|
+
def software_update
|
437
|
+
if @tenable_url == 'https://cloud.tenable.com'
|
438
|
+
return "software_update only works on a Nessus 6 appliance"
|
439
|
+
end
|
440
|
+
http_post(:uri => "/settings/software-update", :fields => header)
|
441
|
+
end
|
442
|
+
|
443
|
+
# Performs scan with templatename provided (name, title or uuid of scan).
|
444
|
+
# Name is your scan name and targets are targets for scan
|
445
|
+
#
|
446
|
+
# returns: JSON parsed object with scan info
|
447
|
+
def scan_quick_template (templatename, name, targets)
|
448
|
+
templates = list_templates('scan')['templates'].select do |temp|
|
449
|
+
temp['uuid'] == templatename or temp['name'] == templatename or temp['title'] == templatename
|
450
|
+
end
|
451
|
+
if templates.nil?
|
452
|
+
return nil
|
453
|
+
end
|
454
|
+
template_uuid = templates.first['uuid']
|
455
|
+
settings = editor_templates('scan', template_uuid)
|
456
|
+
settings.merge!(@quick_defaults)
|
457
|
+
settings['name'] = name
|
458
|
+
settings['text_targets'] = targets
|
459
|
+
scan_create(template_uuid, settings)
|
460
|
+
end
|
461
|
+
|
462
|
+
# Performs scan with scan policy provided (uuid of policy or policy name).
|
463
|
+
# Name is your scan name and targets are targets for scan
|
464
|
+
# (foldername is optional - folder where to save the scan (if that folder exists))
|
465
|
+
# (scanner_id is optional - ID of the scanner/cloud scanner you want to run this scan on)
|
466
|
+
#
|
467
|
+
# returns: JSON parsed object with scan info
|
468
|
+
def scan_quick_policy (policyname, name, targets, foldername = nil, scanner_id = nil)
|
469
|
+
policies = list_policies['policies'].select do |pol|
|
470
|
+
pol['id'] == policyname or pol['name'] == policyname
|
471
|
+
end
|
472
|
+
if policies.nil?
|
473
|
+
return nil
|
474
|
+
end
|
475
|
+
policy = policies.first
|
476
|
+
template_uuid = policy['template_uuid']
|
477
|
+
settings = Hash.new
|
478
|
+
settings.merge!(@quick_defaults)
|
479
|
+
settings['name'] = name
|
480
|
+
settings['policy_id'] = policy['id']
|
481
|
+
settings['text_targets'] = targets
|
482
|
+
unless foldername.nil?
|
483
|
+
folders = list_folders['folders'].select do |folder|
|
484
|
+
folder['name'] == foldername
|
485
|
+
end
|
486
|
+
unless folders.empty?
|
487
|
+
settings['folder_id'] = folders.first['id']
|
488
|
+
end
|
489
|
+
end
|
490
|
+
unless scanner_id.nil?
|
491
|
+
settings['scanner_id'] = scanner_id
|
492
|
+
end
|
493
|
+
scan_create(template_uuid, settings)
|
494
|
+
end
|
495
|
+
|
496
|
+
# Returns scan status by performing a 'scan_details' API call
|
497
|
+
def scan_status(scan_id)
|
498
|
+
sd = scan_details(scan_id)
|
499
|
+
unless sd['error'].nil?
|
500
|
+
return 'error'
|
501
|
+
end
|
502
|
+
if sd.nil?
|
503
|
+
return 'error'
|
504
|
+
end
|
505
|
+
sd['info']['status']
|
506
|
+
end
|
507
|
+
|
508
|
+
# Returns the status of the latest history object of a scan by performing a 'scan_details' API call.
|
509
|
+
# Note this is currently updated more frequently than the scan status in the tenable.io API
|
510
|
+
def scan_latest_history_status(scan_id)
|
511
|
+
sd = scan_details(scan_id)
|
512
|
+
unless sd['error'].nil?
|
513
|
+
return 'error'
|
514
|
+
end
|
515
|
+
if sd.nil?
|
516
|
+
return 'error'
|
517
|
+
end
|
518
|
+
history = sd['history']
|
519
|
+
if history.nil? or history.length == 0
|
520
|
+
'error'
|
521
|
+
else
|
522
|
+
sd['history'].last['status']
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# Parse the scan status command to determine if a scan has finished
|
527
|
+
def scan_finished?(scan_id)
|
528
|
+
ss = scan_status(scan_id)
|
529
|
+
if ss == 'completed' or ss == 'canceled' or ss == 'imported'
|
530
|
+
true
|
531
|
+
else
|
532
|
+
false
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# use download scan API call to download a report in raw format
|
537
|
+
def report_download_quick(scan_id, format)
|
538
|
+
se = scan_export(scan_id, format)
|
539
|
+
# ready, loading
|
540
|
+
while (status = scan_export_status(scan_id, se['file'])['status']) != "ready" do
|
541
|
+
# puts status
|
542
|
+
if status.nil? or status == ''
|
543
|
+
return nil
|
544
|
+
end
|
545
|
+
sleep @defsleep
|
546
|
+
end
|
547
|
+
report_download(scan_id, se['file'])
|
548
|
+
end
|
549
|
+
|
550
|
+
# use download scan API call to save a report as file
|
551
|
+
def report_download_file(scan_id, format, outputfn)
|
552
|
+
report_content = report_download_quick(scan_id, format)
|
553
|
+
File.open(outputfn, 'w') do |f|
|
554
|
+
f.write(report_content)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
|
559
|
+
private
|
560
|
+
|
561
|
+
# Perform HTTP put method with uri, data and fields
|
562
|
+
#
|
563
|
+
# returns: HTTP result object
|
564
|
+
def http_put(opts = {})
|
565
|
+
ret = http_put_low(opts)
|
566
|
+
if ret.is_a?(Hash) and ret.has_key?('error') and ret['error'] == 'Invalid Credentials'
|
567
|
+
authenticate
|
568
|
+
http_put_low(opts)
|
569
|
+
else
|
570
|
+
ret
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
def http_put_low(opts = {})
|
575
|
+
uri = opts[:uri]
|
576
|
+
data = opts[:data]
|
577
|
+
fields = opts[:fields] || {}
|
578
|
+
res = nil
|
579
|
+
tries = @httpretry
|
580
|
+
|
581
|
+
req = Net::HTTP::Put.new(uri)
|
582
|
+
req.set_form_data(data) unless (data.nil? || data.empty?)
|
583
|
+
fields.each_pair do |name, value|
|
584
|
+
req.add_field(name, value)
|
585
|
+
end
|
586
|
+
|
587
|
+
begin
|
588
|
+
tries -= 1
|
589
|
+
res = @connection.request(req)
|
590
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
591
|
+
if tries > 0
|
592
|
+
sleep @httpsleep
|
593
|
+
retry
|
594
|
+
else
|
595
|
+
return res
|
596
|
+
end
|
597
|
+
rescue URI::InvalidURIError
|
598
|
+
return res
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Perform HTTP delete method with uri, data and fields
|
603
|
+
#
|
604
|
+
# returns: HTTP result object
|
605
|
+
def http_delete(opts = {})
|
606
|
+
ret = http_delete_low(opts)
|
607
|
+
if ret.is_a?(Hash) and ret.has_key?('error') and ret['error'] == 'Invalid Credentials'
|
608
|
+
authenticate
|
609
|
+
http_delete_low(opts)
|
610
|
+
ret
|
611
|
+
else
|
612
|
+
ret
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
def http_delete_low(opts = {})
|
617
|
+
uri = opts[:uri]
|
618
|
+
fields = opts[:fields] || {}
|
619
|
+
res = nil
|
620
|
+
tries = @httpretry
|
621
|
+
|
622
|
+
req = Net::HTTP::Delete.new(uri)
|
623
|
+
|
624
|
+
fields.each_pair do |name, value|
|
625
|
+
req.add_field(name, value)
|
626
|
+
end
|
627
|
+
|
628
|
+
begin
|
629
|
+
tries -= 1
|
630
|
+
res = @connection.request(req)
|
631
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
632
|
+
if tries > 0
|
633
|
+
sleep @httpsleep
|
634
|
+
retry
|
635
|
+
else
|
636
|
+
return res
|
637
|
+
end
|
638
|
+
rescue URI::InvalidURIError
|
639
|
+
return res
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
# Perform HTTP get method with uri and fields
|
644
|
+
#
|
645
|
+
# returns: JSON parsed object (if JSON parseable)
|
646
|
+
def http_get(opts = {})
|
647
|
+
raw_content = opts[:raw_content] || false
|
648
|
+
ret = http_get_low(opts)
|
649
|
+
if !raw_content
|
650
|
+
if ret.is_a?(Hash) and ret.has_key?('error') and ret['error'] == 'Invalid Credentials'
|
651
|
+
authenticate
|
652
|
+
ret = http_get_low(opts)
|
653
|
+
return ret
|
654
|
+
else
|
655
|
+
return ret
|
656
|
+
end
|
657
|
+
else
|
658
|
+
ret
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
def http_get_low(opts = {})
|
663
|
+
uri = opts[:uri]
|
664
|
+
fields = opts[:fields] || {}
|
665
|
+
raw_content = opts[:raw_content] || false
|
666
|
+
json = {}
|
667
|
+
tries = @httpretry
|
668
|
+
|
669
|
+
req = Net::HTTP::Get.new(uri)
|
670
|
+
fields.each_pair do |name, value|
|
671
|
+
req.add_field(name, value)
|
672
|
+
end
|
673
|
+
|
674
|
+
begin
|
675
|
+
tries -= 1
|
676
|
+
res = @connection.request(req)
|
677
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
678
|
+
if tries > 0
|
679
|
+
sleep @httpsleep
|
680
|
+
retry
|
681
|
+
else
|
682
|
+
return json
|
683
|
+
end
|
684
|
+
rescue URI::InvalidURIError
|
685
|
+
return json
|
686
|
+
end
|
687
|
+
if !raw_content
|
688
|
+
parse_json(res.body)
|
689
|
+
else
|
690
|
+
res.body
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
# Perform HTTP post method with uri, data, body and fields
|
695
|
+
#
|
696
|
+
# returns: JSON parsed object (if JSON parseable)
|
697
|
+
def http_post(opts = {})
|
698
|
+
if opts.has_key?(:authenticationmethod)
|
699
|
+
# i know authzmethod = opts.delete(:authorizationmethod) is short, but not readable
|
700
|
+
authzmethod = opts[:authenticationmethod]
|
701
|
+
opts.delete(:authenticationmethod)
|
702
|
+
end
|
703
|
+
ret = http_post_low(opts)
|
704
|
+
if ret.is_a?(Hash) and ret.has_key?('error') and ret['error'] == 'Invalid Credentials'
|
705
|
+
unless authzmethod
|
706
|
+
authenticate
|
707
|
+
ret = http_post_low(opts)
|
708
|
+
return ret
|
709
|
+
end
|
710
|
+
else
|
711
|
+
ret
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
def http_post_low(opts = {})
|
716
|
+
uri = opts[:uri]
|
717
|
+
data = opts[:data]
|
718
|
+
fields = opts[:fields] || {}
|
719
|
+
body = opts[:body]
|
720
|
+
ctype = opts[:ctype]
|
721
|
+
json = {}
|
722
|
+
tries = @httpretry
|
723
|
+
|
724
|
+
req = Net::HTTP::Post.new(uri)
|
725
|
+
req.set_form_data(data) unless (data.nil? || data.empty?)
|
726
|
+
req.body = body unless (body.nil? || body.empty?)
|
727
|
+
req['Content-Type'] = ctype unless (ctype.nil? || ctype.empty?)
|
728
|
+
fields.each_pair do |name, value|
|
729
|
+
req.add_field(name, value)
|
730
|
+
end
|
731
|
+
|
732
|
+
begin
|
733
|
+
tries -= 1
|
734
|
+
res = @connection.request(req)
|
735
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
736
|
+
if tries > 0
|
737
|
+
sleep @httpsleep
|
738
|
+
retry
|
739
|
+
else
|
740
|
+
return json
|
741
|
+
end
|
742
|
+
rescue URI::InvalidURIError
|
743
|
+
return json
|
744
|
+
end
|
745
|
+
|
746
|
+
parse_json(res.body)
|
747
|
+
end
|
748
|
+
|
749
|
+
# Perform JSON parsing of body
|
750
|
+
#
|
751
|
+
# returns: JSON parsed object (if JSON parseable)
|
752
|
+
def parse_json(body)
|
753
|
+
buf = {}
|
754
|
+
|
755
|
+
begin
|
756
|
+
buf = JSON.parse(body)
|
757
|
+
rescue JSON::ParserError
|
758
|
+
end
|
759
|
+
|
760
|
+
buf
|
761
|
+
end
|
762
|
+
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tenable-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vlatko Kosturjak
|
8
|
+
- Patrick Craston
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-06-20 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: "Unofficial Ruby library for communicating with the tenable.io API (also
|
15
|
+
works with Nessus 6).\n You can start, stop, pause and resume scan. Get status
|
16
|
+
of scans, download reports, create policies, etc.\n Based on the excellent library
|
17
|
+
for interacting with Nessus https://github.com/kost/nessus_rest-ruby by https://github.com/kost. "
|
18
|
+
email: patrick.craston@intruder.io
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- lib/error/authentication_error.rb
|
24
|
+
- lib/tenable-ruby.rb
|
25
|
+
homepage: https://gitlab.com/intruder/tenable-ruby
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 2.5.2
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Unofficial Ruby library for communicating with the tenable.io API
|
49
|
+
test_files: []
|