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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6020ebd94aaa44a36cf4eeebeca87d7ceaf6730f3ff15dbe377b3d332cf8a90e
4
+ data.tar.gz: 5debd6afde04c5f130fb3c1e867f68b0fbbdfbe160c2fffac20bb0ad4fa488dd
5
+ SHA512:
6
+ metadata.gz: bc1dc472bbe24ee07fa0447d3c87a60e09451cad4f31714d667e5bba43c1d098e5b1d82006b01a431cfc2a8401e760a007a95507123358e54e6c7282cfaf9758
7
+ data.tar.gz: 3f0ce6c911b2a72ed66b9e805541e6e4ae4864b3948c1f9e89807c4b58b9005d3747192f8fdbbdd550765ea6d6b8f97b63cfdd8d6bdc4515a3d7853458d2ca80
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vudx00
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # tenable-ruby-sdk
2
+
3
+ > **Unofficial** — This project is not affiliated with, endorsed by, or sponsored by Tenable, Inc. "Tenable" is a registered trademark of Tenable, Inc.
4
+
5
+ Ruby SDK for the [Tenable.io API](https://developer.tenable.com/reference/navigate). Covers vulnerability management, bulk exports, VM scans, and WAS v2 web application scanning.
6
+
7
+ Requires Ruby >= 3.2. Uses [Faraday](https://github.com/lostisland/faraday) for HTTP.
8
+
9
+ ## Installation
10
+
11
+ Add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'tenable-ruby-sdk'
15
+ ```
16
+
17
+ Then `bundle install`.
18
+
19
+ ## Authentication
20
+
21
+ Get your API keys from Tenable.io under **Settings > My Account > API Keys**.
22
+
23
+ Pass them directly:
24
+
25
+ ```ruby
26
+ client = Tenable::Client.new(
27
+ access_key: "your-access-key",
28
+ secret_key: "your-secret-key"
29
+ )
30
+ ```
31
+
32
+ Or set environment variables and omit them:
33
+
34
+ ```sh
35
+ export TENABLE_ACCESS_KEY="your-access-key"
36
+ export TENABLE_SECRET_KEY="your-secret-key"
37
+ ```
38
+
39
+ ```ruby
40
+ client = Tenable::Client.new
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### List Vulnerabilities
46
+
47
+ ```ruby
48
+ data = client.vulnerabilities.list
49
+ data["vulnerabilities"].each do |vuln|
50
+ puts "#{vuln['plugin_name']} (severity: #{vuln['severity']})"
51
+ end
52
+ ```
53
+
54
+ ### Export Vulnerabilities
55
+
56
+ For large datasets, use the export workflow:
57
+
58
+ ```ruby
59
+ exports = client.exports
60
+
61
+ # Start the export
62
+ result = exports.export(num_assets: 50)
63
+ uuid = result["export_uuid"]
64
+
65
+ # Wait for it to finish (polls automatically, 5 min timeout by default)
66
+ exports.wait_for_completion(uuid)
67
+
68
+ # Iterate over all chunks
69
+ exports.each(uuid) do |vuln|
70
+ puts vuln["plugin"]["name"]
71
+ end
72
+ ```
73
+
74
+ ### VM Scans
75
+
76
+ ```ruby
77
+ # List scans
78
+ client.scans.list
79
+
80
+ # Launch a scan
81
+ client.scans.launch(scan_id)
82
+
83
+ # Check status
84
+ client.scans.status(scan_id)
85
+ ```
86
+
87
+ ### Web App Scans (WAS v2)
88
+
89
+ ```ruby
90
+ was = client.web_app_scans
91
+
92
+ # Create a scan config
93
+ config = was.create_config(name: "My Scan", target: "https://example.com")
94
+ config_id = config["config_id"]
95
+
96
+ # Launch the scan
97
+ scan = was.launch(config_id)
98
+ scan_id = scan["scan_id"]
99
+
100
+ # Wait for completion (polls until terminal status)
101
+ was.wait_until_complete(config_id, scan_id)
102
+
103
+ # Get findings
104
+ was.findings(config_id)
105
+ ```
106
+
107
+ ## Configuration
108
+
109
+ All options with their defaults:
110
+
111
+ ```ruby
112
+ client = Tenable::Client.new(
113
+ access_key: "...", # or TENABLE_ACCESS_KEY env var
114
+ secret_key: "...", # or TENABLE_SECRET_KEY env var
115
+ base_url: "https://cloud.tenable.com", # must be HTTPS
116
+ timeout: 30, # request timeout (seconds)
117
+ open_timeout: 10, # connection timeout (seconds)
118
+ max_retries: 3, # retry attempts (0-10)
119
+ logger: Logger.new($stdout) # nil = silent (default)
120
+ )
121
+ ```
122
+
123
+ ## Error Handling
124
+
125
+ All errors inherit from `Tenable::Error`:
126
+
127
+ ```ruby
128
+ begin
129
+ client.vulnerabilities.list
130
+ rescue Tenable::AuthenticationError => e
131
+ # Bad or missing API keys (401)
132
+ rescue Tenable::RateLimitError => e
133
+ # Rate limited and retries exhausted (429)
134
+ e.status_code # => 429
135
+ rescue Tenable::TimeoutError => e
136
+ # Request or export poll timed out
137
+ rescue Tenable::ApiError => e
138
+ # Any other API error
139
+ e.status_code # => Integer
140
+ e.body # => String
141
+ rescue Tenable::ConnectionError => e
142
+ # Network failure
143
+ end
144
+ ```
145
+
146
+ Rate limiting (429) and server errors (5xx) are retried automatically with exponential backoff before raising.
147
+
148
+ ## Thread Safety
149
+
150
+ The client is frozen after initialization with eagerly instantiated resources. Safe to use across threads.
151
+
152
+ ## Development
153
+
154
+ ```sh
155
+ bundle install
156
+ bundle exec rspec # run tests
157
+ bundle exec rubocop # lint
158
+ bundle exec yard doc # generate docs
159
+ bundle audit check # check for vulnerable dependencies
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ # Primary entry point for the Tenable.io API.
5
+ #
6
+ # @example Basic usage
7
+ # client = Tenable::Client.new(access_key: "ak", secret_key: "sk")
8
+ # client.vulnerabilities.list
9
+ class Client
10
+ # @return [Configuration] the client configuration
11
+ attr_reader :configuration
12
+
13
+ # @return [Resources::Vulnerabilities]
14
+ attr_reader :vulnerabilities
15
+
16
+ # @return [Resources::Exports]
17
+ attr_reader :exports
18
+
19
+ # @return [Resources::AssetExports]
20
+ attr_reader :asset_exports
21
+
22
+ # @return [Resources::Scans]
23
+ attr_reader :scans
24
+
25
+ # @return [Resources::WebAppScans]
26
+ attr_reader :web_app_scans
27
+
28
+ # Creates a new Tenable API client.
29
+ #
30
+ # @param options [Hash] configuration options passed to {Configuration#initialize}
31
+ # @option options [String] :access_key Tenable.io API access key
32
+ # @option options [String] :secret_key Tenable.io API secret key
33
+ # @option options [String] :base_url API base URL (default: https://cloud.tenable.com)
34
+ # @option options [Integer] :timeout request timeout in seconds (default: 30)
35
+ # @option options [Integer] :open_timeout connection open timeout in seconds (default: 10)
36
+ # @option options [Integer] :max_retries maximum retry attempts (default: 3)
37
+ # @option options [Logger] :logger optional logger instance
38
+ # @raise [ArgumentError] if required credentials are missing or options are invalid
39
+ #
40
+ # @example
41
+ # client = Tenable::Client.new(
42
+ # access_key: "your-access-key",
43
+ # secret_key: "your-secret-key",
44
+ # timeout: 60
45
+ # )
46
+ def initialize(**)
47
+ @configuration = Configuration.new(**)
48
+ connection = Connection.new(@configuration)
49
+ @vulnerabilities = Resources::Vulnerabilities.new(connection)
50
+ @exports = Resources::Exports.new(connection)
51
+ @asset_exports = Resources::AssetExports.new(connection)
52
+ @scans = Resources::Scans.new(connection)
53
+ @web_app_scans = Resources::WebAppScans.new(connection)
54
+ freeze
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ # Holds and validates all configuration options for the Tenable client.
5
+ #
6
+ # Configuration values can be passed directly or read from environment
7
+ # variables (+TENABLE_ACCESS_KEY+, +TENABLE_SECRET_KEY+).
8
+ class Configuration
9
+ # @return [String] the API access key
10
+ attr_reader :access_key
11
+
12
+ # @return [String] the API secret key
13
+ attr_reader :secret_key
14
+
15
+ # @return [String] the API base URL
16
+ attr_reader :base_url
17
+
18
+ # @return [Integer] the request timeout in seconds
19
+ attr_reader :timeout
20
+
21
+ # @return [Integer] the connection open timeout in seconds
22
+ attr_reader :open_timeout
23
+
24
+ # @return [Integer] the maximum number of retry attempts
25
+ attr_reader :max_retries
26
+
27
+ # @return [Logger, nil] optional logger instance
28
+ attr_reader :logger
29
+
30
+ DEFAULTS = {
31
+ base_url: 'https://cloud.tenable.com',
32
+ timeout: 30,
33
+ open_timeout: 10,
34
+ max_retries: 3
35
+ }.freeze
36
+
37
+ # Creates a new Configuration instance.
38
+ #
39
+ # @param access_key [String, nil] API access key (falls back to +TENABLE_ACCESS_KEY+ env var)
40
+ # @param secret_key [String, nil] API secret key (falls back to +TENABLE_SECRET_KEY+ env var)
41
+ # @param base_url [String, nil] API base URL (default: https://cloud.tenable.com)
42
+ # @param timeout [Integer, nil] request timeout in seconds (default: 30)
43
+ # @param open_timeout [Integer, nil] connection open timeout in seconds (default: 10)
44
+ # @param max_retries [Integer, nil] max retry attempts, 0-10 (default: 3)
45
+ # @param logger [Logger, nil] optional logger for request/response logging
46
+ # @raise [ArgumentError] if credentials are missing, base_url is invalid, or numeric values are out of range
47
+ def initialize(access_key: nil, secret_key: nil, base_url: nil, timeout: nil, open_timeout: nil, max_retries: nil,
48
+ logger: nil)
49
+ @access_key = access_key || ENV.fetch('TENABLE_ACCESS_KEY', nil)
50
+ @secret_key = secret_key || ENV.fetch('TENABLE_SECRET_KEY', nil)
51
+ @base_url = base_url || DEFAULTS[:base_url]
52
+ @timeout = timeout || DEFAULTS[:timeout]
53
+ @open_timeout = open_timeout || DEFAULTS[:open_timeout]
54
+ @max_retries = max_retries.nil? ? DEFAULTS[:max_retries] : max_retries
55
+ @logger = logger
56
+
57
+ validate!
58
+ freeze
59
+ end
60
+
61
+ private
62
+
63
+ def validate!
64
+ validate_credentials!
65
+ validate_base_url!
66
+ validate_timeouts!
67
+ validate_max_retries!
68
+ end
69
+
70
+ def validate_credentials!
71
+ raise ArgumentError, 'access_key is required (pass directly or set TENABLE_ACCESS_KEY)' if blank?(@access_key)
72
+ raise ArgumentError, 'secret_key is required (pass directly or set TENABLE_SECRET_KEY)' if blank?(@secret_key)
73
+ end
74
+
75
+ def validate_base_url!
76
+ uri = URI.parse(@base_url)
77
+ raise ArgumentError, "base_url must use HTTPS: #{@base_url}" unless uri.scheme == 'https'
78
+ rescue URI::InvalidURIError
79
+ raise ArgumentError, "base_url is not a valid URL: #{@base_url}"
80
+ end
81
+
82
+ def validate_timeouts!
83
+ unless @timeout.is_a?(Integer) && @timeout.positive?
84
+ raise ArgumentError,
85
+ "timeout must be positive, got #{@timeout}"
86
+ end
87
+
88
+ return if @open_timeout.is_a?(Integer) && @open_timeout.positive?
89
+
90
+ raise ArgumentError, "open_timeout must be positive, got #{@open_timeout}"
91
+ end
92
+
93
+ def validate_max_retries!
94
+ return if @max_retries.is_a?(Integer) && @max_retries >= 0 && @max_retries <= 10
95
+
96
+ raise ArgumentError, "max_retries must be between 0 and 10, got #{@max_retries}"
97
+ end
98
+
99
+ def blank?(value)
100
+ value.nil? || (value.is_a?(String) && value.strip.empty?)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ # Manages the Faraday HTTP connection with configured middleware.
5
+ #
6
+ # Automatically configures authentication, retry, and logging middleware
7
+ # based on the provided {Configuration}.
8
+ class Connection
9
+ # @return [Faraday::Connection] the underlying Faraday connection
10
+ attr_reader :faraday
11
+
12
+ # Creates a new connection from the given configuration.
13
+ #
14
+ # @param config [Configuration] a validated configuration instance
15
+ # @raise [ArgumentError] if the base_url does not use HTTPS
16
+ def initialize(config)
17
+ @config = config
18
+ @faraday = build_connection
19
+ end
20
+
21
+ private
22
+
23
+ def build_connection
24
+ Faraday.new(url: @config.base_url) do |f|
25
+ f.headers['Accept'] = 'application/json'
26
+ f.use Middleware::Authentication,
27
+ access_key: @config.access_key,
28
+ secret_key: @config.secret_key
29
+ f.use Middleware::Retry, max_retries: @config.max_retries
30
+ f.use Middleware::Logging, logger: @config.logger
31
+ f.options.timeout = @config.timeout
32
+ f.options.open_timeout = @config.open_timeout
33
+ f.adapter Faraday.default_adapter
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ # Base error class for all Tenable SDK errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when API authentication fails (HTTP 401).
8
+ class AuthenticationError < Error
9
+ DEFAULT_MESSAGE = 'Authentication failed. Verify your access key and secret key are correct.'
10
+
11
+ def initialize(msg = DEFAULT_MESSAGE)
12
+ super
13
+ end
14
+ end
15
+
16
+ # Raised for non-success HTTP responses from the Tenable API.
17
+ class ApiError < Error
18
+ # @return [Integer, nil] the HTTP status code
19
+ attr_reader :status_code
20
+
21
+ # @return [String, nil] the response body
22
+ attr_reader :body
23
+
24
+ # @param msg [String, nil] custom error message
25
+ # @param status_code [Integer, nil] HTTP status code
26
+ # @param body [String, nil] response body
27
+ def initialize(msg = nil, status_code: nil, body: nil)
28
+ @status_code = status_code
29
+ @body = body
30
+ message = msg || "API request failed with status #{status_code}"
31
+ message = "#{message}: #{body}" if body
32
+ super(message)
33
+ end
34
+ end
35
+
36
+ # Raised when the API rate limit is exceeded and retries are exhausted (HTTP 429).
37
+ class RateLimitError < ApiError
38
+ def initialize(msg = 'Rate limit exceeded. Retries exhausted.', **kwargs)
39
+ super
40
+ end
41
+ end
42
+
43
+ # Raised when a network connection to the Tenable API cannot be established.
44
+ class ConnectionError < Error
45
+ def initialize(msg = 'Connection to Tenable API failed. Check your network and base_url configuration.')
46
+ super
47
+ end
48
+ end
49
+
50
+ # Raised when an API request exceeds the configured timeout.
51
+ class TimeoutError < Error
52
+ def initialize(msg = 'Request timed out. Consider increasing the timeout configuration.')
53
+ super
54
+ end
55
+ end
56
+
57
+ # Raised when the API response body cannot be parsed as JSON.
58
+ class ParseError < Error
59
+ def initialize(msg = 'Failed to parse API response. The response may be malformed.')
60
+ super
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ module Middleware
5
+ class Authentication < Faraday::Middleware
6
+ def initialize(app, access_key:, secret_key:)
7
+ super(app)
8
+ @access_key = access_key
9
+ @secret_key = secret_key
10
+ end
11
+
12
+ def on_request(env)
13
+ env.request_headers['X-ApiKeys'] = "accessKey=#{@access_key};secretKey=#{@secret_key};"
14
+ end
15
+
16
+ def inspect
17
+ "#<#{self.class.name} [REDACTED]>"
18
+ end
19
+
20
+ def to_s
21
+ inspect
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ module Middleware
5
+ class Logging < Faraday::Middleware
6
+ API_KEY_PATTERN = /accessKey=[^;]*;?\s*secretKey=[^;]*/
7
+
8
+ def initialize(app, logger: nil)
9
+ super(app)
10
+ @logger = logger
11
+ end
12
+
13
+ def call(env)
14
+ log_request(env) if @logger
15
+ @app.call(env).on_complete do |response_env|
16
+ log_response(response_env) if @logger
17
+ end
18
+ rescue StandardError => e
19
+ @logger&.error("Tenable request error: #{e.message}")
20
+ raise
21
+ end
22
+
23
+ private
24
+
25
+ def log_request(env)
26
+ headers = redact_headers(env.request_headers)
27
+ @logger.debug("Tenable request: #{env.method.upcase} #{env.url} headers=#{headers}")
28
+ end
29
+
30
+ def log_response(env)
31
+ @logger.debug("Tenable response: status=#{env.status}")
32
+ end
33
+
34
+ def redact_headers(headers)
35
+ headers.transform_values do |value|
36
+ value.is_a?(String) ? value.gsub(API_KEY_PATTERN, '[REDACTED]') : value
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ module Middleware
5
+ class Retry < Faraday::Middleware
6
+ DEFAULT_MAX_RETRIES = 3
7
+ RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504].freeze
8
+ BASE_DELAY = 1
9
+
10
+ def initialize(app, max_retries: DEFAULT_MAX_RETRIES)
11
+ super(app)
12
+ @max_retries = max_retries
13
+ end
14
+
15
+ def call(env)
16
+ attempt = 0
17
+ loop do
18
+ attempt += 1
19
+ response = @app.call(env.dup)
20
+
21
+ return response unless retryable?(response.status)
22
+
23
+ raise_if_exhausted(response, attempt) if attempt >= @max_retries
24
+
25
+ sleep(retry_delay(response, attempt))
26
+ end
27
+ rescue Faraday::Error
28
+ raise if attempt >= @max_retries
29
+
30
+ sleep(BASE_DELAY * (2**(attempt - 1)))
31
+ retry
32
+ end
33
+
34
+ private
35
+
36
+ def retryable?(status)
37
+ RETRYABLE_STATUS_CODES.include?(status)
38
+ end
39
+
40
+ def retry_delay(response, attempt)
41
+ retry_after = response.headers&.[]('Retry-After')
42
+ return retry_after.to_i if retry_after
43
+
44
+ BASE_DELAY * (2**(attempt - 1))
45
+ end
46
+
47
+ def raise_if_exhausted(response, attempts)
48
+ if response.status == 429
49
+ raise Tenable::RateLimitError.new(
50
+ "Rate limit exceeded after #{attempts} attempts",
51
+ status_code: response.status,
52
+ body: response.body
53
+ )
54
+ end
55
+
56
+ raise Tenable::ApiError.new(
57
+ "Request failed after #{attempts} attempts",
58
+ status_code: response.status,
59
+ body: response.body
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ module Models
5
+ # Represents an asset (host/device) from the Tenable.io API.
6
+ Asset = Data.define(:uuid, :hostname, :ipv4, :operating_system, :fqdn, :netbios_name) do
7
+ # Builds an Asset from a raw API response hash.
8
+ #
9
+ # @param data [Hash] raw API response hash with string keys
10
+ # @return [Asset]
11
+ def self.from_api(data)
12
+ data = data.transform_keys(&:to_sym)
13
+ new(
14
+ uuid: data[:uuid],
15
+ hostname: data[:hostname],
16
+ ipv4: data[:ipv4],
17
+ operating_system: data[:operating_system] || [],
18
+ fqdn: data[:fqdn] || [],
19
+ netbios_name: data[:netbios_name]
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ module Models
5
+ # Represents the status of a vulnerability export job.
6
+ Export = Data.define(:uuid, :status, :chunks_available, :chunks_failed, :chunks_cancelled) do
7
+ # Builds an Export from a raw API response hash.
8
+ #
9
+ # @param data [Hash] raw API response hash with string keys
10
+ # @return [Export]
11
+ def self.from_api(data)
12
+ data = data.transform_keys(&:to_sym)
13
+ new(
14
+ uuid: data[:uuid],
15
+ status: data[:status],
16
+ chunks_available: data[:chunks_available] || [],
17
+ chunks_failed: data[:chunks_failed] || [],
18
+ chunks_cancelled: data[:chunks_cancelled] || []
19
+ )
20
+ end
21
+
22
+ # @return [Boolean] true if the export has completed successfully
23
+ def finished?
24
+ status == 'FINISHED'
25
+ end
26
+
27
+ # @return [Boolean] true if the export is still processing
28
+ def processing?
29
+ status == 'PROCESSING'
30
+ end
31
+
32
+ # @return [Boolean] true if the export encountered an error
33
+ def error?
34
+ status == 'ERROR'
35
+ end
36
+
37
+ # @return [Boolean] true if the export is queued
38
+ def queued?
39
+ status == 'QUEUED'
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tenable
4
+ module Models
5
+ # Represents a web application scan finding.
6
+ Finding = Data.define(:finding_id, :severity, :url, :name, :description, :remediation, :plugin_id) do
7
+ # Builds a Finding from a raw API response hash.
8
+ #
9
+ # @param data [Hash] raw API response hash with string keys
10
+ # @return [Finding]
11
+ def self.from_api(data)
12
+ data = data.transform_keys(&:to_sym)
13
+ new(
14
+ finding_id: data[:finding_id],
15
+ severity: data[:severity],
16
+ url: data[:url],
17
+ name: data[:name],
18
+ description: data[:description],
19
+ remediation: data[:remediation],
20
+ plugin_id: data[:plugin_id]
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end