tenable-ruby-sdk 0.1.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/LICENSE +21 -0
- data/README.md +164 -0
- data/lib/tenable/client.rb +57 -0
- data/lib/tenable/configuration.rb +103 -0
- data/lib/tenable/connection.rb +37 -0
- data/lib/tenable/error.rb +63 -0
- data/lib/tenable/middleware/authentication.rb +25 -0
- data/lib/tenable/middleware/logging.rb +41 -0
- data/lib/tenable/middleware/retry.rb +64 -0
- data/lib/tenable/models/asset.rb +24 -0
- data/lib/tenable/models/export.rb +43 -0
- data/lib/tenable/models/finding.rb +25 -0
- data/lib/tenable/models/scan.rb +26 -0
- data/lib/tenable/models/vulnerability.rb +35 -0
- data/lib/tenable/models/web_app_scan.rb +24 -0
- data/lib/tenable/models/web_app_scan_config.rb +23 -0
- data/lib/tenable/pagination.rb +72 -0
- data/lib/tenable/pollable.rb +31 -0
- data/lib/tenable/resources/asset_exports.rb +95 -0
- data/lib/tenable/resources/base.rb +135 -0
- data/lib/tenable/resources/exports.rb +104 -0
- data/lib/tenable/resources/scans.rb +256 -0
- data/lib/tenable/resources/vulnerabilities.rb +69 -0
- data/lib/tenable/resources/web_app_scans.rb +294 -0
- data/lib/tenable/version.rb +5 -0
- data/lib/tenable.rb +31 -0
- metadata +192 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tenable
|
|
4
|
+
module Resources
|
|
5
|
+
# Provides access to the Tenable.io scan management endpoints.
|
|
6
|
+
class Scans < Base
|
|
7
|
+
# Supported scan export formats.
|
|
8
|
+
SUPPORTED_EXPORT_FORMATS = %w[pdf csv nessus].freeze
|
|
9
|
+
|
|
10
|
+
# @return [Integer] default seconds between export status polls
|
|
11
|
+
DEFAULT_EXPORT_POLL_INTERVAL = 5
|
|
12
|
+
|
|
13
|
+
# @return [Integer] default timeout in seconds for waiting on export completion
|
|
14
|
+
DEFAULT_EXPORT_TIMEOUT = 600
|
|
15
|
+
|
|
16
|
+
# Lists all scans.
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash] parsed response containing scan list under +"scans"+ key
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# client.scans.list
|
|
22
|
+
def list
|
|
23
|
+
get('/scans')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Creates a new scan.
|
|
27
|
+
#
|
|
28
|
+
# @param params [Hash] scan configuration (e.g., +uuid+, +settings+)
|
|
29
|
+
# @return [Hash] the created scan data
|
|
30
|
+
# @raise [ApiError] on non-2xx responses
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# client.scans.create(uuid: template_uuid, settings: { name: "My Scan", text_targets: "10.0.0.1" })
|
|
34
|
+
def create(params)
|
|
35
|
+
post('/scans', params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Launches an existing scan.
|
|
39
|
+
#
|
|
40
|
+
# @param scan_id [Integer, String] the scan ID
|
|
41
|
+
# @return [Hash] response containing the scan instance UUID
|
|
42
|
+
def launch(scan_id)
|
|
43
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
44
|
+
post("/scans/#{scan_id}/launch")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Retrieves full details of a scan including host and vulnerability info.
|
|
48
|
+
#
|
|
49
|
+
# @param scan_id [Integer, String] the scan ID
|
|
50
|
+
# @return [Hash] detailed scan data
|
|
51
|
+
def details(scan_id)
|
|
52
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
53
|
+
get("/scans/#{scan_id}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Updates an existing scan configuration.
|
|
57
|
+
#
|
|
58
|
+
# @param scan_id [Integer, String] the scan ID
|
|
59
|
+
# @param params [Hash] scan configuration to update
|
|
60
|
+
# @return [Hash] the updated scan data
|
|
61
|
+
def update(scan_id, params)
|
|
62
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
63
|
+
put("/scans/#{scan_id}", params)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Deletes a scan.
|
|
67
|
+
#
|
|
68
|
+
# @param scan_id [Integer, String] the scan ID
|
|
69
|
+
# @return [Hash, nil] parsed response or nil
|
|
70
|
+
def destroy(scan_id)
|
|
71
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
72
|
+
delete("/scans/#{scan_id}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Retrieves the latest status of a scan.
|
|
76
|
+
#
|
|
77
|
+
# @param scan_id [Integer, String] the scan ID
|
|
78
|
+
# @return [Hash] status data for the scan
|
|
79
|
+
def status(scan_id)
|
|
80
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
81
|
+
get("/scans/#{scan_id}/latest-status")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Pauses a running scan.
|
|
85
|
+
#
|
|
86
|
+
# @param scan_id [Integer, String] the scan ID
|
|
87
|
+
# @return [Hash, nil] parsed response
|
|
88
|
+
def pause(scan_id)
|
|
89
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
90
|
+
post("/scans/#{scan_id}/pause")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Resumes a paused scan.
|
|
94
|
+
#
|
|
95
|
+
# @param scan_id [Integer, String] the scan ID
|
|
96
|
+
# @return [Hash, nil] parsed response
|
|
97
|
+
def resume(scan_id)
|
|
98
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
99
|
+
post("/scans/#{scan_id}/resume")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Stops a running scan.
|
|
103
|
+
#
|
|
104
|
+
# @param scan_id [Integer, String] the scan ID
|
|
105
|
+
# @return [Hash, nil] parsed response
|
|
106
|
+
def stop(scan_id)
|
|
107
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
108
|
+
post("/scans/#{scan_id}/stop")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Copies a scan.
|
|
112
|
+
#
|
|
113
|
+
# @param scan_id [Integer, String] the scan ID
|
|
114
|
+
# @return [Hash] the copied scan data
|
|
115
|
+
def copy(scan_id)
|
|
116
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
117
|
+
post("/scans/#{scan_id}/copy")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Updates the schedule for a scan.
|
|
121
|
+
#
|
|
122
|
+
# @param scan_id [Integer, String] the scan ID
|
|
123
|
+
# @param params [Hash] schedule configuration
|
|
124
|
+
# @return [Hash] the updated schedule data
|
|
125
|
+
def schedule(scan_id, params)
|
|
126
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
127
|
+
put("/scans/#{scan_id}/schedule", params)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Retrieves the scan history.
|
|
131
|
+
#
|
|
132
|
+
# @param scan_id [Integer, String] the scan ID
|
|
133
|
+
# @return [Hash] history data including an array of history records
|
|
134
|
+
def history(scan_id)
|
|
135
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
136
|
+
get("/scans/#{scan_id}/history")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Retrieves details for a specific host within a scan.
|
|
140
|
+
#
|
|
141
|
+
# @param scan_id [Integer, String] the scan ID
|
|
142
|
+
# @param host_id [Integer, String] the host ID
|
|
143
|
+
# @return [Hash] host details including vulnerability info
|
|
144
|
+
def host_details(scan_id, host_id)
|
|
145
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
146
|
+
validate_path_segment!(host_id, name: 'host_id')
|
|
147
|
+
get("/scans/#{scan_id}/hosts/#{host_id}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Retrieves plugin output for a specific host and plugin within a scan.
|
|
151
|
+
#
|
|
152
|
+
# @param scan_id [Integer, String] the scan ID
|
|
153
|
+
# @param host_id [Integer, String] the host ID
|
|
154
|
+
# @param plugin_id [Integer, String] the plugin ID
|
|
155
|
+
# @return [Hash] plugin output data
|
|
156
|
+
def plugin_output(scan_id, host_id, plugin_id)
|
|
157
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
158
|
+
validate_path_segment!(host_id, name: 'host_id')
|
|
159
|
+
validate_path_segment!(plugin_id, name: 'plugin_id')
|
|
160
|
+
get("/scans/#{scan_id}/hosts/#{host_id}/plugins/#{plugin_id}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Initiates a scan report export.
|
|
164
|
+
#
|
|
165
|
+
# @param scan_id [Integer, String] the scan ID
|
|
166
|
+
# @param format [String] export format — one of "pdf", "csv", or "nessus"
|
|
167
|
+
# @param body [Hash] additional export parameters (e.g., +chapters+ for PDF)
|
|
168
|
+
# @return [Hash] response containing the file ID under +"file"+ key
|
|
169
|
+
# @raise [ArgumentError] if the format is not supported
|
|
170
|
+
#
|
|
171
|
+
# @example
|
|
172
|
+
# client.scans.export_request(123, format: 'pdf', chapters: 'vuln_hosts_summary')
|
|
173
|
+
def export_request(scan_id, format:, **body)
|
|
174
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
175
|
+
unless SUPPORTED_EXPORT_FORMATS.include?(format)
|
|
176
|
+
raise ArgumentError, "Unsupported format '#{format}'. Must be one of: #{SUPPORTED_EXPORT_FORMATS.join(', ')}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
post("/scans/#{scan_id}/export", body.merge(format: format))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Retrieves the status of a scan export.
|
|
183
|
+
#
|
|
184
|
+
# @param scan_id [Integer, String] the scan ID
|
|
185
|
+
# @param file_id [Integer, String] the export file ID
|
|
186
|
+
# @return [Hash] status data with +"status"+ key ("ready" or "loading")
|
|
187
|
+
def export_status(scan_id, file_id)
|
|
188
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
189
|
+
validate_path_segment!(file_id, name: 'file_id')
|
|
190
|
+
get("/scans/#{scan_id}/export/#{file_id}/status")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Downloads a completed scan export as raw binary data.
|
|
194
|
+
#
|
|
195
|
+
# @param scan_id [Integer, String] the scan ID
|
|
196
|
+
# @param file_id [Integer, String] the export file ID
|
|
197
|
+
# @return [String] raw binary content of the export file
|
|
198
|
+
def export_download(scan_id, file_id)
|
|
199
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
200
|
+
validate_path_segment!(file_id, name: 'file_id')
|
|
201
|
+
get_raw("/scans/#{scan_id}/export/#{file_id}/download")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Polls until a scan export is ready for download.
|
|
205
|
+
#
|
|
206
|
+
# @param scan_id [Integer, String] the scan ID
|
|
207
|
+
# @param file_id [Integer, String] the export file ID
|
|
208
|
+
# @param timeout [Integer] maximum seconds to wait (default: 600)
|
|
209
|
+
# @param poll_interval [Integer] seconds between status checks (default: 5)
|
|
210
|
+
# @return [Hash] the final status data when export is ready
|
|
211
|
+
# @raise [Tenable::TimeoutError] if the export does not become ready within the timeout
|
|
212
|
+
def wait_for_export(scan_id, file_id, timeout: DEFAULT_EXPORT_TIMEOUT,
|
|
213
|
+
poll_interval: DEFAULT_EXPORT_POLL_INTERVAL)
|
|
214
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
215
|
+
validate_path_segment!(file_id, name: 'file_id')
|
|
216
|
+
poll_until(timeout: timeout, poll_interval: poll_interval, label: "Scan export #{file_id}") do
|
|
217
|
+
status_data = export_status(scan_id, file_id)
|
|
218
|
+
status_data if status_data['status'] == 'ready'
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Convenience method: requests an export, waits for completion, and downloads the result.
|
|
223
|
+
#
|
|
224
|
+
# @param scan_id [Integer, String] the scan ID
|
|
225
|
+
# @param format [String] export format — one of "pdf", "csv", or "nessus"
|
|
226
|
+
# @param save_path [String, nil] if provided, writes binary content to this file path.
|
|
227
|
+
# The caller is responsible for ensuring the path is safe and writable.
|
|
228
|
+
# This value is used as-is with +File.binwrite+ — no sanitization is performed.
|
|
229
|
+
# @param timeout [Integer] maximum seconds to wait (default: 600)
|
|
230
|
+
# @param poll_interval [Integer] seconds between status checks (default: 5)
|
|
231
|
+
# @param body [Hash] additional export parameters
|
|
232
|
+
# @return [String] the save_path if given, otherwise the raw binary content
|
|
233
|
+
#
|
|
234
|
+
# @example Download PDF to disk
|
|
235
|
+
# client.scans.export(123, format: 'pdf', save_path: '/tmp/report.pdf')
|
|
236
|
+
#
|
|
237
|
+
# @example Get raw binary content
|
|
238
|
+
# binary = client.scans.export(123, format: 'nessus')
|
|
239
|
+
def export(scan_id, format:, save_path: nil, timeout: DEFAULT_EXPORT_TIMEOUT,
|
|
240
|
+
poll_interval: DEFAULT_EXPORT_POLL_INTERVAL, **body)
|
|
241
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
242
|
+
result = export_request(scan_id, format: format, **body)
|
|
243
|
+
file_id = result['file']
|
|
244
|
+
wait_for_export(scan_id, file_id, timeout: timeout, poll_interval: poll_interval)
|
|
245
|
+
content = export_download(scan_id, file_id)
|
|
246
|
+
|
|
247
|
+
if save_path
|
|
248
|
+
File.binwrite(save_path, content)
|
|
249
|
+
save_path
|
|
250
|
+
else
|
|
251
|
+
content
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tenable
|
|
4
|
+
module Resources
|
|
5
|
+
# Provides access to the Tenable.io vulnerability workbench endpoints.
|
|
6
|
+
class Vulnerabilities < Base
|
|
7
|
+
# Lists vulnerabilities from the workbench.
|
|
8
|
+
#
|
|
9
|
+
# @param params [Hash] optional query parameters for filtering
|
|
10
|
+
# @return [Hash] parsed API response containing vulnerability data
|
|
11
|
+
# @raise [AuthenticationError] on 401 responses
|
|
12
|
+
# @raise [ApiError] on other non-2xx responses
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# client.vulnerabilities.list(date_range: 7)
|
|
16
|
+
def list(params = {})
|
|
17
|
+
get('/workbenches/vulnerabilities', params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Retrieves detailed information for a specific vulnerability plugin.
|
|
21
|
+
#
|
|
22
|
+
# @param plugin_id [Integer, String] the plugin ID
|
|
23
|
+
# @param params [Hash] optional query parameters
|
|
24
|
+
# @return [Hash] vulnerability info data
|
|
25
|
+
def info(plugin_id, params = {})
|
|
26
|
+
validate_path_segment!(plugin_id, name: 'plugin_id')
|
|
27
|
+
get("/workbenches/vulnerabilities/#{plugin_id}/info", params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Retrieves plugin outputs for a specific vulnerability.
|
|
31
|
+
#
|
|
32
|
+
# @param plugin_id [Integer, String] the plugin ID
|
|
33
|
+
# @param params [Hash] optional query parameters
|
|
34
|
+
# @return [Hash] plugin output data
|
|
35
|
+
def outputs(plugin_id, params = {})
|
|
36
|
+
validate_path_segment!(plugin_id, name: 'plugin_id')
|
|
37
|
+
get("/workbenches/vulnerabilities/#{plugin_id}/outputs", params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Lists assets from the workbench.
|
|
41
|
+
#
|
|
42
|
+
# @param params [Hash] optional query parameters for filtering
|
|
43
|
+
# @return [Hash] parsed API response containing asset data
|
|
44
|
+
def assets(params = {})
|
|
45
|
+
get('/workbenches/assets', params)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieves detailed information for a specific asset.
|
|
49
|
+
#
|
|
50
|
+
# @param asset_id [String] the asset UUID
|
|
51
|
+
# @param params [Hash] optional query parameters
|
|
52
|
+
# @return [Hash] asset info data
|
|
53
|
+
def asset_info(asset_id, params = {})
|
|
54
|
+
validate_path_segment!(asset_id, name: 'asset_id')
|
|
55
|
+
get("/workbenches/assets/#{asset_id}/info", params)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Lists vulnerabilities for a specific asset.
|
|
59
|
+
#
|
|
60
|
+
# @param asset_id [String] the asset UUID
|
|
61
|
+
# @param params [Hash] optional query parameters
|
|
62
|
+
# @return [Hash] vulnerability data for the asset
|
|
63
|
+
def asset_vulnerabilities(asset_id, params = {})
|
|
64
|
+
validate_path_segment!(asset_id, name: 'asset_id')
|
|
65
|
+
get("/workbenches/assets/#{asset_id}/vulnerabilities", params)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tenable
|
|
4
|
+
module Resources
|
|
5
|
+
# Provides access to the Tenable.io Web Application Scanning (WAS) endpoints.
|
|
6
|
+
class WebAppScans < Base
|
|
7
|
+
TERMINAL_STATUSES = %w[completed failed cancelled error].freeze
|
|
8
|
+
|
|
9
|
+
# Supported scan export formats.
|
|
10
|
+
SUPPORTED_EXPORT_FORMATS = %w[pdf csv nessus].freeze
|
|
11
|
+
|
|
12
|
+
# @return [Integer] default seconds between status polls
|
|
13
|
+
DEFAULT_POLL_INTERVAL = 2
|
|
14
|
+
|
|
15
|
+
# @return [Integer] default seconds between export status polls
|
|
16
|
+
DEFAULT_EXPORT_POLL_INTERVAL = 5
|
|
17
|
+
|
|
18
|
+
# @return [Integer] default timeout in seconds for waiting on scan completion
|
|
19
|
+
DEFAULT_SCAN_TIMEOUT = 3600
|
|
20
|
+
|
|
21
|
+
# @return [Integer] default timeout in seconds for waiting on export completion
|
|
22
|
+
DEFAULT_EXPORT_TIMEOUT = 600
|
|
23
|
+
|
|
24
|
+
# Creates a new web application scan configuration.
|
|
25
|
+
#
|
|
26
|
+
# @param name [String] name for the scan configuration
|
|
27
|
+
# @param target [String] the target URL to scan
|
|
28
|
+
# @return [Hash] the created configuration data
|
|
29
|
+
# @raise [ApiError] on non-2xx responses
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# client.web_app_scans.create_config(name: "My App", target: "https://example.com")
|
|
33
|
+
def create_config(name:, target:)
|
|
34
|
+
post('/was/v2/configs', { 'name' => name, 'target' => target })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Retrieves a scan configuration by ID.
|
|
38
|
+
#
|
|
39
|
+
# @param config_id [String] the scan configuration ID
|
|
40
|
+
# @return [Hash] the configuration data
|
|
41
|
+
def get_config(config_id)
|
|
42
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
43
|
+
get("/was/v2/configs/#{config_id}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Updates a scan configuration.
|
|
47
|
+
#
|
|
48
|
+
# @param config_id [String] the scan configuration ID
|
|
49
|
+
# @param params [Hash] configuration parameters to update
|
|
50
|
+
# @return [Hash] the updated configuration data
|
|
51
|
+
def update_config(config_id, params)
|
|
52
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
53
|
+
put("/was/v2/configs/#{config_id}", params)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Deletes a scan configuration.
|
|
57
|
+
#
|
|
58
|
+
# @param config_id [String] the scan configuration ID
|
|
59
|
+
# @return [Hash, nil] parsed response or nil
|
|
60
|
+
def delete_config(config_id)
|
|
61
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
62
|
+
delete("/was/v2/configs/#{config_id}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Searches scan configurations.
|
|
66
|
+
#
|
|
67
|
+
# @param params [Hash] search parameters
|
|
68
|
+
# @return [Hash] search results with items and pagination
|
|
69
|
+
def search_configs(**params)
|
|
70
|
+
post('/was/v2/configs/search', params)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Launches a scan for the given configuration.
|
|
74
|
+
#
|
|
75
|
+
# @param config_id [String] the scan configuration ID
|
|
76
|
+
# @return [Hash] response containing the scan ID
|
|
77
|
+
def launch(config_id)
|
|
78
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
79
|
+
post("/was/v2/configs/#{config_id}/scans")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Retrieves the status of a specific scan.
|
|
83
|
+
#
|
|
84
|
+
# @param config_id [String] the scan configuration ID
|
|
85
|
+
# @param scan_id [String] the scan ID
|
|
86
|
+
# @return [Hash] scan status data
|
|
87
|
+
def status(config_id, scan_id)
|
|
88
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
89
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
90
|
+
get("/was/v2/configs/#{config_id}/scans/#{scan_id}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Retrieves findings for a scan configuration.
|
|
94
|
+
#
|
|
95
|
+
# @param config_id [String] the scan configuration ID
|
|
96
|
+
# @param params [Hash] optional query parameters for filtering
|
|
97
|
+
# @return [Hash] findings data
|
|
98
|
+
#
|
|
99
|
+
# @example
|
|
100
|
+
# client.web_app_scans.findings(config_id, severity: "high")
|
|
101
|
+
def findings(config_id, **params)
|
|
102
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
103
|
+
get("/was/v2/configs/#{config_id}/findings", params)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Polls until the scan reaches a terminal status.
|
|
107
|
+
#
|
|
108
|
+
# @param config_id [String] the scan configuration ID
|
|
109
|
+
# @param scan_id [String] the scan ID
|
|
110
|
+
# @param timeout [Integer] maximum seconds to wait (default: 3600)
|
|
111
|
+
# @param poll_interval [Integer] seconds between status checks (default: 2)
|
|
112
|
+
# @return [Hash] the final scan status data
|
|
113
|
+
# @raise [Tenable::TimeoutError] if the scan does not complete within the timeout
|
|
114
|
+
def wait_until_complete(config_id, scan_id, timeout: DEFAULT_SCAN_TIMEOUT, poll_interval: DEFAULT_POLL_INTERVAL)
|
|
115
|
+
validate_path_segment!(config_id, name: 'config_id')
|
|
116
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
117
|
+
poll_until(timeout: timeout, poll_interval: poll_interval, label: "WAS scan #{scan_id}") do
|
|
118
|
+
result = status(config_id, scan_id)
|
|
119
|
+
result if TERMINAL_STATUSES.include?(result['status'])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Retrieves details of a specific WAS scan.
|
|
124
|
+
#
|
|
125
|
+
# @param scan_id [String] the scan ID
|
|
126
|
+
# @return [Hash] scan details
|
|
127
|
+
def get_scan(scan_id)
|
|
128
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
129
|
+
get("/was/v2/scans/#{scan_id}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Stops a running WAS scan.
|
|
133
|
+
#
|
|
134
|
+
# @param scan_id [String] the scan ID
|
|
135
|
+
# @return [Hash] the updated scan status
|
|
136
|
+
def stop_scan(scan_id)
|
|
137
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
138
|
+
patch("/was/v2/scans/#{scan_id}/status", { 'status' => 'stopped' })
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Deletes a WAS scan.
|
|
142
|
+
#
|
|
143
|
+
# @param scan_id [String] the scan ID
|
|
144
|
+
# @return [Hash, nil] parsed response or nil
|
|
145
|
+
def delete_scan(scan_id)
|
|
146
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
147
|
+
delete("/was/v2/scans/#{scan_id}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Searches WAS scans.
|
|
151
|
+
#
|
|
152
|
+
# @param params [Hash] search parameters
|
|
153
|
+
# @return [Hash] search results with items and pagination
|
|
154
|
+
def search_scans(**params)
|
|
155
|
+
post('/was/v2/scans/search', params)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Searches WAS vulnerabilities.
|
|
159
|
+
#
|
|
160
|
+
# @param params [Hash] search parameters
|
|
161
|
+
# @return [Hash] search results with items and pagination
|
|
162
|
+
def search_vulnerabilities(**params)
|
|
163
|
+
post('/was/v2/vulnerabilities/search', params)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Retrieves details for a specific WAS vulnerability.
|
|
167
|
+
#
|
|
168
|
+
# @param vuln_id [String] the vulnerability ID
|
|
169
|
+
# @return [Hash] vulnerability details
|
|
170
|
+
def vulnerability_details(vuln_id)
|
|
171
|
+
validate_path_segment!(vuln_id, name: 'vuln_id')
|
|
172
|
+
get("/was/v2/vulns/#{vuln_id}")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Initiates an export for a specific WAS scan.
|
|
176
|
+
#
|
|
177
|
+
# @param scan_id [String] the scan ID
|
|
178
|
+
# @param format [String] export format — one of "pdf", "csv", or "nessus"
|
|
179
|
+
# @param body [Hash] additional export parameters
|
|
180
|
+
# @return [Hash] export initiation response
|
|
181
|
+
# @raise [ArgumentError] if the format is not supported
|
|
182
|
+
def export_scan(scan_id, format:, **body)
|
|
183
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
184
|
+
unless SUPPORTED_EXPORT_FORMATS.include?(format)
|
|
185
|
+
raise ArgumentError, "Unsupported format '#{format}'. Must be one of: #{SUPPORTED_EXPORT_FORMATS.join(', ')}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
put("/was/v2/scans/#{scan_id}/export", body.merge('format' => format))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Retrieves the status of a WAS scan export.
|
|
192
|
+
#
|
|
193
|
+
# @param scan_id [String] the scan ID
|
|
194
|
+
# @return [Hash] status data with +"status"+ key ("ready" or "loading")
|
|
195
|
+
def export_scan_status(scan_id)
|
|
196
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
197
|
+
get("/was/v2/scans/#{scan_id}/export/status")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Downloads a completed WAS scan export as raw binary data.
|
|
201
|
+
#
|
|
202
|
+
# @param scan_id [String] the scan ID
|
|
203
|
+
# @return [String] raw binary content of the export
|
|
204
|
+
def download_scan_export(scan_id)
|
|
205
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
206
|
+
get_raw("/was/v2/scans/#{scan_id}/export/download")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Polls until a WAS scan export is ready for download.
|
|
210
|
+
#
|
|
211
|
+
# @param scan_id [String] the scan ID
|
|
212
|
+
# @param timeout [Integer] maximum seconds to wait (default: 600)
|
|
213
|
+
# @param poll_interval [Integer] seconds between status checks (default: 5)
|
|
214
|
+
# @return [Hash] the final status data when export is ready
|
|
215
|
+
# @raise [Tenable::TimeoutError] if the export does not become ready within the timeout
|
|
216
|
+
def wait_for_scan_export(scan_id, timeout: DEFAULT_EXPORT_TIMEOUT, poll_interval: DEFAULT_EXPORT_POLL_INTERVAL)
|
|
217
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
218
|
+
poll_until(timeout: timeout, poll_interval: poll_interval, label: "WAS scan export for #{scan_id}") do
|
|
219
|
+
status_data = export_scan_status(scan_id)
|
|
220
|
+
status_data if status_data['status'] == 'ready'
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Convenience method: requests an export, waits for completion, and downloads the result.
|
|
225
|
+
#
|
|
226
|
+
# @param scan_id [String] the scan ID
|
|
227
|
+
# @param format [String] export format — one of "pdf", "csv", or "nessus"
|
|
228
|
+
# @param save_path [String, nil] if provided, writes binary content to this file path.
|
|
229
|
+
# The caller is responsible for ensuring the path is safe and writable.
|
|
230
|
+
# This value is used as-is with +File.binwrite+ — no sanitization is performed.
|
|
231
|
+
# @param timeout [Integer] maximum seconds to wait (default: 600)
|
|
232
|
+
# @param poll_interval [Integer] seconds between status checks (default: 5)
|
|
233
|
+
# @param body [Hash] additional export parameters
|
|
234
|
+
# @return [String] the save_path if given, otherwise the raw binary content
|
|
235
|
+
#
|
|
236
|
+
# @example Download PDF to disk
|
|
237
|
+
# client.web_app_scans.export('scan-123', format: 'pdf', save_path: '/tmp/report.pdf')
|
|
238
|
+
#
|
|
239
|
+
# @example Get raw binary content
|
|
240
|
+
# binary = client.web_app_scans.export('scan-123', format: 'nessus')
|
|
241
|
+
def export(scan_id, format:, save_path: nil, timeout: DEFAULT_EXPORT_TIMEOUT,
|
|
242
|
+
poll_interval: DEFAULT_EXPORT_POLL_INTERVAL, **body)
|
|
243
|
+
validate_path_segment!(scan_id, name: 'scan_id')
|
|
244
|
+
export_scan(scan_id, format: format, **body)
|
|
245
|
+
wait_for_scan_export(scan_id, timeout: timeout, poll_interval: poll_interval)
|
|
246
|
+
content = download_scan_export(scan_id)
|
|
247
|
+
|
|
248
|
+
if save_path
|
|
249
|
+
File.binwrite(save_path, content)
|
|
250
|
+
save_path
|
|
251
|
+
else
|
|
252
|
+
content
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Initiates a bulk WAS findings export.
|
|
257
|
+
#
|
|
258
|
+
# @param body [Hash] export request parameters
|
|
259
|
+
# @return [Hash] response containing the export UUID
|
|
260
|
+
def export_findings(body = {})
|
|
261
|
+
post('/was/v1/export/vulns', body)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Retrieves the status of a WAS findings export.
|
|
265
|
+
#
|
|
266
|
+
# @param export_uuid [String] the export UUID
|
|
267
|
+
# @return [Hash] status data
|
|
268
|
+
def export_findings_status(export_uuid)
|
|
269
|
+
validate_path_segment!(export_uuid, name: 'export_uuid')
|
|
270
|
+
get("/was/v1/export/vulns/#{export_uuid}/status")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Downloads a single chunk of WAS findings export data.
|
|
274
|
+
#
|
|
275
|
+
# @param export_uuid [String] the export UUID
|
|
276
|
+
# @param chunk_id [Integer] the chunk identifier
|
|
277
|
+
# @return [Array<Hash>] array of finding records
|
|
278
|
+
def export_findings_chunk(export_uuid, chunk_id)
|
|
279
|
+
validate_path_segment!(export_uuid, name: 'export_uuid')
|
|
280
|
+
validate_path_segment!(chunk_id, name: 'chunk_id')
|
|
281
|
+
get("/was/v1/export/vulns/#{export_uuid}/chunks/#{chunk_id}")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Cancels an in-progress WAS findings export.
|
|
285
|
+
#
|
|
286
|
+
# @param export_uuid [String] the export UUID
|
|
287
|
+
# @return [Hash] cancellation response
|
|
288
|
+
def export_findings_cancel(export_uuid)
|
|
289
|
+
validate_path_segment!(export_uuid, name: 'export_uuid')
|
|
290
|
+
post("/was/v1/export/vulns/#{export_uuid}/cancel")
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
data/lib/tenable.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require_relative 'tenable/version'
|
|
7
|
+
require_relative 'tenable/error'
|
|
8
|
+
require_relative 'tenable/configuration'
|
|
9
|
+
require_relative 'tenable/middleware/authentication'
|
|
10
|
+
require_relative 'tenable/middleware/retry'
|
|
11
|
+
require_relative 'tenable/middleware/logging'
|
|
12
|
+
require_relative 'tenable/pollable'
|
|
13
|
+
require_relative 'tenable/connection'
|
|
14
|
+
require_relative 'tenable/pagination'
|
|
15
|
+
require_relative 'tenable/models/asset'
|
|
16
|
+
require_relative 'tenable/models/vulnerability'
|
|
17
|
+
require_relative 'tenable/models/export'
|
|
18
|
+
require_relative 'tenable/models/scan'
|
|
19
|
+
require_relative 'tenable/models/web_app_scan_config'
|
|
20
|
+
require_relative 'tenable/models/web_app_scan'
|
|
21
|
+
require_relative 'tenable/models/finding'
|
|
22
|
+
require_relative 'tenable/resources/base'
|
|
23
|
+
require_relative 'tenable/resources/vulnerabilities'
|
|
24
|
+
require_relative 'tenable/resources/exports'
|
|
25
|
+
require_relative 'tenable/resources/asset_exports'
|
|
26
|
+
require_relative 'tenable/resources/scans'
|
|
27
|
+
require_relative 'tenable/resources/web_app_scans'
|
|
28
|
+
require_relative 'tenable/client'
|
|
29
|
+
|
|
30
|
+
module Tenable
|
|
31
|
+
end
|