bundler-trivy-plugin 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/CHANGELOG.md +44 -0
- data/LICENSE +21 -0
- data/README.md +480 -0
- data/lib/bundler/trivy/config.rb +326 -0
- data/lib/bundler/trivy/plugin.rb +99 -0
- data/lib/bundler/trivy/reporter.rb +213 -0
- data/lib/bundler/trivy/scan_result.rb +156 -0
- data/lib/bundler/trivy/scanner.rb +226 -0
- data/lib/bundler/trivy/version.rb +8 -0
- data/lib/bundler/trivy/vulnerability.rb +245 -0
- data/plugins.rb +11 -0
- metadata +115 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "vulnerability"
|
|
4
|
+
|
|
5
|
+
module Bundler
|
|
6
|
+
module Trivy
|
|
7
|
+
# Represents the results of a Trivy security scan.
|
|
8
|
+
#
|
|
9
|
+
# ScanResult wraps the raw JSON output from Trivy and provides convenient
|
|
10
|
+
# methods for accessing vulnerability information, filtering by severity,
|
|
11
|
+
# grouping vulnerabilities, and generating summaries. It automatically
|
|
12
|
+
# filters out ignored CVEs based on configuration.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# results = scanner.scan
|
|
16
|
+
# puts "Found #{results.vulnerability_count} vulnerabilities"
|
|
17
|
+
# puts "Critical: #{results.critical_vulnerabilities.count}"
|
|
18
|
+
#
|
|
19
|
+
# @example Filtering by severity
|
|
20
|
+
# critical_vulns = results.critical_vulnerabilities
|
|
21
|
+
# critical_vulns.each do |vuln|
|
|
22
|
+
# puts "#{vuln.id}: #{vuln.package_name}"
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @example Grouping vulnerabilities
|
|
26
|
+
# results.by_severity.each do |severity, vulns|
|
|
27
|
+
# puts "#{severity}: #{vulns.count} vulnerabilities"
|
|
28
|
+
# end
|
|
29
|
+
class ScanResult
|
|
30
|
+
# @return [Hash] Raw Trivy scan data
|
|
31
|
+
attr_reader :data
|
|
32
|
+
|
|
33
|
+
# Initializes a new ScanResult instance.
|
|
34
|
+
#
|
|
35
|
+
# @param data [Hash] Raw JSON data from Trivy scan
|
|
36
|
+
# @param config [Config, nil] Configuration object for filtering ignored CVEs
|
|
37
|
+
#
|
|
38
|
+
# @example Create from scan data
|
|
39
|
+
# data = JSON.parse(trivy_output)
|
|
40
|
+
# results = ScanResult.new(data, config)
|
|
41
|
+
def initialize(data, config = nil)
|
|
42
|
+
@data = data || {}
|
|
43
|
+
@config = config
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns all vulnerabilities found in the scan.
|
|
47
|
+
#
|
|
48
|
+
# Vulnerabilities are wrapped in Vulnerability objects for convenient access.
|
|
49
|
+
# CVEs marked as ignored in the configuration are automatically filtered out.
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<Vulnerability>] Array of vulnerability objects
|
|
52
|
+
#
|
|
53
|
+
# @example Get all vulnerabilities
|
|
54
|
+
# results.vulnerabilities.each do |vuln|
|
|
55
|
+
# puts "#{vuln.severity}: #{vuln.package_name} - #{vuln.id}"
|
|
56
|
+
# end
|
|
57
|
+
def vulnerabilities
|
|
58
|
+
results = @data["Results"] || []
|
|
59
|
+
|
|
60
|
+
results.flat_map do |result|
|
|
61
|
+
vulns = result["Vulnerabilities"]
|
|
62
|
+
next [] if vulns.nil? || vulns.empty?
|
|
63
|
+
|
|
64
|
+
vulns.map { |v| Vulnerability.new(v, result["Target"]) }
|
|
65
|
+
end.compact.reject { |v| ignored?(v) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Groups vulnerabilities by severity level.
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash<String, Array<Vulnerability>>] Hash with severity levels as keys
|
|
71
|
+
#
|
|
72
|
+
# @example Group by severity
|
|
73
|
+
# results.by_severity.each do |severity, vulns|
|
|
74
|
+
# puts "#{severity}: #{vulns.count}"
|
|
75
|
+
# end
|
|
76
|
+
def by_severity
|
|
77
|
+
vulnerabilities.group_by(&:severity)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns only CRITICAL severity vulnerabilities.
|
|
81
|
+
#
|
|
82
|
+
# @return [Array<Vulnerability>] Array of critical vulnerabilities
|
|
83
|
+
#
|
|
84
|
+
# @example Get critical vulnerabilities
|
|
85
|
+
# critical = results.critical_vulnerabilities
|
|
86
|
+
# puts "#{critical.count} critical issues found"
|
|
87
|
+
def critical_vulnerabilities
|
|
88
|
+
vulnerabilities.select(&:critical?)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns only HIGH severity vulnerabilities.
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<Vulnerability>] Array of high severity vulnerabilities
|
|
94
|
+
def high_vulnerabilities
|
|
95
|
+
vulnerabilities.select(&:high?)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Checks if any vulnerabilities were found.
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean] true if vulnerabilities exist
|
|
101
|
+
#
|
|
102
|
+
# @example Check for vulnerabilities
|
|
103
|
+
# if results.has_vulnerabilities?
|
|
104
|
+
# puts "Security issues detected!"
|
|
105
|
+
# end
|
|
106
|
+
def has_vulnerabilities?
|
|
107
|
+
!vulnerabilities.empty?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Checks if any CRITICAL severity vulnerabilities were found.
|
|
111
|
+
#
|
|
112
|
+
# This is useful for implementing fail-on-critical policies.
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean] true if critical vulnerabilities exist
|
|
115
|
+
#
|
|
116
|
+
# @example Fail on critical
|
|
117
|
+
# exit 1 if results.has_critical_vulnerabilities?
|
|
118
|
+
def has_critical_vulnerabilities?
|
|
119
|
+
!critical_vulnerabilities.empty?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns the total count of all vulnerabilities.
|
|
123
|
+
#
|
|
124
|
+
# @return [Integer] Total vulnerability count
|
|
125
|
+
#
|
|
126
|
+
# @example Get total count
|
|
127
|
+
# puts "Total: #{results.vulnerability_count}"
|
|
128
|
+
def vulnerability_count
|
|
129
|
+
vulnerabilities.size
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns vulnerability counts grouped by severity level.
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash<String, Integer>] Hash with severity levels as keys and counts as values
|
|
135
|
+
#
|
|
136
|
+
# @example Get severity breakdown
|
|
137
|
+
# counts = results.severity_counts
|
|
138
|
+
# # => {"CRITICAL" => 2, "HIGH" => 5, "MEDIUM" => 10}
|
|
139
|
+
def severity_counts
|
|
140
|
+
by_severity.transform_values(&:size)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Checks if a vulnerability should be ignored based on configuration.
|
|
146
|
+
#
|
|
147
|
+
# @param vulnerability [Vulnerability] The vulnerability to check
|
|
148
|
+
# @return [Boolean] true if the vulnerability should be ignored
|
|
149
|
+
def ignored?(vulnerability)
|
|
150
|
+
return false unless @config
|
|
151
|
+
|
|
152
|
+
@config.cve_ignored?(vulnerability.id)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require_relative "scan_result"
|
|
7
|
+
|
|
8
|
+
module Bundler
|
|
9
|
+
module Trivy
|
|
10
|
+
# Executes Trivy security scans on Ruby projects.
|
|
11
|
+
#
|
|
12
|
+
# The Scanner class is responsible for invoking the Trivy command-line tool,
|
|
13
|
+
# parsing its JSON output, and returning structured results. It handles
|
|
14
|
+
# timeout management, error conditions, and severity filtering.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# config = Config.new
|
|
18
|
+
# scanner = Scanner.new("/path/to/project", config)
|
|
19
|
+
# if scanner.trivy_available?
|
|
20
|
+
# results = scanner.scan
|
|
21
|
+
# puts "Found #{results.vulnerability_count} vulnerabilities"
|
|
22
|
+
# end
|
|
23
|
+
class Scanner
|
|
24
|
+
# @return [String] the root directory of the project being scanned
|
|
25
|
+
attr_reader :project_root
|
|
26
|
+
|
|
27
|
+
# @return [Config] the configuration object for the scanner
|
|
28
|
+
attr_reader :config
|
|
29
|
+
|
|
30
|
+
# Initializes a new Scanner instance.
|
|
31
|
+
#
|
|
32
|
+
# @param project_root [String] The root directory of the project to scan
|
|
33
|
+
# @param config [Config, nil] Configuration object. If nil, creates a new Config with defaults
|
|
34
|
+
#
|
|
35
|
+
# @example Create scanner with default config
|
|
36
|
+
# scanner = Scanner.new("/path/to/project")
|
|
37
|
+
#
|
|
38
|
+
# @example Create scanner with custom config
|
|
39
|
+
# config = Config.new
|
|
40
|
+
# scanner = Scanner.new("/path/to/project", config)
|
|
41
|
+
def initialize(project_root, config = nil)
|
|
42
|
+
@project_root = project_root
|
|
43
|
+
@config = config || Config.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Executes a Trivy security scan on the project.
|
|
47
|
+
#
|
|
48
|
+
# This method runs the Trivy CLI tool with appropriate arguments, captures
|
|
49
|
+
# its JSON output, and parses it into a ScanResult object. The scan checks
|
|
50
|
+
# for known vulnerabilities in Ruby dependencies listed in Gemfile.lock.
|
|
51
|
+
#
|
|
52
|
+
# @return [ScanResult] Parsed scan results containing vulnerability information
|
|
53
|
+
#
|
|
54
|
+
# @raise [ScanError] If Trivy execution fails with exit code > 1
|
|
55
|
+
# @raise [ScanError] If Trivy output is not valid JSON
|
|
56
|
+
# @raise [ScanError] If the scan exceeds the configured timeout
|
|
57
|
+
#
|
|
58
|
+
# @example Scanning a project
|
|
59
|
+
# scanner = Scanner.new("/path/to/project")
|
|
60
|
+
# begin
|
|
61
|
+
# results = scanner.scan
|
|
62
|
+
# puts "Scan completed: #{results.vulnerability_count} issues found"
|
|
63
|
+
# rescue ScanError => e
|
|
64
|
+
# puts "Scan failed: #{e.message}"
|
|
65
|
+
# end
|
|
66
|
+
def scan
|
|
67
|
+
args = build_trivy_args
|
|
68
|
+
timeout = @config.trivy_timeout
|
|
69
|
+
|
|
70
|
+
# Execute Trivy with Open3 for robust command execution
|
|
71
|
+
stdout, stderr, status = Open3.capture3(
|
|
72
|
+
*args,
|
|
73
|
+
timeout: timeout
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Handle Trivy exit codes:
|
|
77
|
+
# 0 = success, no vulnerabilities
|
|
78
|
+
# 1 = success, vulnerabilities found
|
|
79
|
+
# >1 = error condition
|
|
80
|
+
raise ScanError, build_error_message(status.exitstatus, stderr) if status.exitstatus > 1
|
|
81
|
+
|
|
82
|
+
# Parse JSON output into structured data
|
|
83
|
+
data = parse_json(stdout)
|
|
84
|
+
ScanResult.new(data, @config)
|
|
85
|
+
rescue JSON::ParserError => e
|
|
86
|
+
raise ScanError, build_json_error_message(e, stdout)
|
|
87
|
+
rescue Timeout::Error
|
|
88
|
+
raise ScanError, build_timeout_error_message(timeout)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Checks if the Trivy binary is available in the system PATH.
|
|
92
|
+
#
|
|
93
|
+
# This method performs a defensive check to verify that Trivy is installed
|
|
94
|
+
# and accessible before attempting to run a scan. It prevents cryptic errors
|
|
95
|
+
# by failing early with a clear message.
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean] true if Trivy is available, false otherwise
|
|
98
|
+
#
|
|
99
|
+
# @example Check availability before scanning
|
|
100
|
+
# scanner = Scanner.new("/path/to/project")
|
|
101
|
+
# if scanner.trivy_available?
|
|
102
|
+
# results = scanner.scan
|
|
103
|
+
# else
|
|
104
|
+
# puts "Please install Trivy first"
|
|
105
|
+
# end
|
|
106
|
+
def trivy_available?
|
|
107
|
+
system("which trivy > /dev/null 2>&1")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Builds the command-line arguments for the Trivy invocation.
|
|
113
|
+
#
|
|
114
|
+
# @return [Array<String>] Array of command arguments
|
|
115
|
+
def build_trivy_args
|
|
116
|
+
args = [
|
|
117
|
+
"trivy", "fs",
|
|
118
|
+
"--scanners", "vuln",
|
|
119
|
+
"--format", "json",
|
|
120
|
+
"--quiet"
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Add severity filtering if configured
|
|
124
|
+
severity_filter = @config.severity_filter
|
|
125
|
+
args += ["--severity", severity_filter.join(",")] if severity_filter && severity_filter.any?
|
|
126
|
+
|
|
127
|
+
args << @project_root
|
|
128
|
+
args
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parses Trivy JSON output into a Ruby hash.
|
|
132
|
+
#
|
|
133
|
+
# @param json_string [String] JSON string from Trivy
|
|
134
|
+
# @return [Hash] Parsed JSON data
|
|
135
|
+
def parse_json(json_string)
|
|
136
|
+
return {} if json_string.empty?
|
|
137
|
+
|
|
138
|
+
JSON.parse(json_string)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Builds a detailed error message for Trivy execution failures.
|
|
142
|
+
#
|
|
143
|
+
# @param exit_code [Integer] The exit code from Trivy
|
|
144
|
+
# @param stderr [String] Standard error output from Trivy
|
|
145
|
+
# @return [String] Formatted error message with troubleshooting guidance
|
|
146
|
+
def build_error_message(exit_code, stderr)
|
|
147
|
+
<<~ERROR
|
|
148
|
+
Trivy scan failed with exit code #{exit_code}
|
|
149
|
+
|
|
150
|
+
Error output:
|
|
151
|
+
#{stderr}
|
|
152
|
+
|
|
153
|
+
Possible causes:
|
|
154
|
+
- Trivy database is outdated or corrupted
|
|
155
|
+
- Network connectivity issues during database update
|
|
156
|
+
- Invalid or corrupted Gemfile.lock
|
|
157
|
+
- Insufficient disk space
|
|
158
|
+
|
|
159
|
+
Troubleshooting steps:
|
|
160
|
+
1. Update Trivy database: trivy image --download-db-only
|
|
161
|
+
2. Check network connectivity
|
|
162
|
+
3. Verify Gemfile.lock is valid: bundle check
|
|
163
|
+
4. Check disk space: df -h
|
|
164
|
+
|
|
165
|
+
For more information, visit: https://trivy.dev/docs/
|
|
166
|
+
ERROR
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Builds an error message for JSON parsing failures.
|
|
170
|
+
#
|
|
171
|
+
# @param error [JSON::ParserError] The JSON parsing error
|
|
172
|
+
# @param output [String] The output that failed to parse
|
|
173
|
+
# @return [String] Formatted error message
|
|
174
|
+
def build_json_error_message(error, output)
|
|
175
|
+
<<~ERROR
|
|
176
|
+
Invalid JSON output from Trivy: #{error.message}
|
|
177
|
+
|
|
178
|
+
This may indicate:
|
|
179
|
+
- Trivy version incompatibility (requires Trivy v0.40.0 or later)
|
|
180
|
+
- Corrupted output due to interrupted execution
|
|
181
|
+
- System error messages mixed with JSON output
|
|
182
|
+
|
|
183
|
+
Output received:
|
|
184
|
+
#{output.slice(0, 500)}#{"...\n[truncated]" if output.length > 500}
|
|
185
|
+
|
|
186
|
+
Troubleshooting:
|
|
187
|
+
1. Check Trivy version: trivy --version
|
|
188
|
+
2. Update Trivy to latest version
|
|
189
|
+
3. Run Trivy manually: trivy fs --format json #{@project_root}
|
|
190
|
+
ERROR
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Builds an error message for timeout conditions.
|
|
194
|
+
#
|
|
195
|
+
# @param timeout [Integer] The timeout value that was exceeded
|
|
196
|
+
# @return [String] Formatted error message
|
|
197
|
+
def build_timeout_error_message(timeout)
|
|
198
|
+
<<~ERROR
|
|
199
|
+
Trivy scan timed out after #{timeout} seconds
|
|
200
|
+
|
|
201
|
+
This may occur when:
|
|
202
|
+
- Scanning a very large project with many dependencies
|
|
203
|
+
- Slow network connection during database updates
|
|
204
|
+
- System resource constraints
|
|
205
|
+
|
|
206
|
+
Solutions:
|
|
207
|
+
1. Increase timeout in config: scanning.timeout: #{timeout * 2}
|
|
208
|
+
2. Update Trivy database before scanning: trivy image --download-db-only
|
|
209
|
+
3. Check system resources: top or htop
|
|
210
|
+
|
|
211
|
+
Current timeout: #{timeout} seconds
|
|
212
|
+
Suggested timeout for large projects: 300+ seconds
|
|
213
|
+
ERROR
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Exception raised when a Trivy scan fails.
|
|
218
|
+
#
|
|
219
|
+
# This error is raised for various scan failures including:
|
|
220
|
+
# - Trivy execution errors (exit code > 1)
|
|
221
|
+
# - Invalid JSON output
|
|
222
|
+
# - Timeout conditions
|
|
223
|
+
# - Database update failures
|
|
224
|
+
class ScanError < StandardError; end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Trivy
|
|
5
|
+
# Represents a single security vulnerability found in a Ruby gem.
|
|
6
|
+
#
|
|
7
|
+
# Vulnerability wraps vulnerability data from Trivy and provides convenient
|
|
8
|
+
# accessors for all vulnerability attributes. It includes helper methods for
|
|
9
|
+
# severity checking, version comparison, and determining if a fix is available.
|
|
10
|
+
#
|
|
11
|
+
# Vulnerabilities are sortable by severity (critical first) and then by package name.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# vuln = Vulnerability.new(trivy_data, "Gemfile.lock")
|
|
15
|
+
# puts "#{vuln.id}: #{vuln.package_name} (#{vuln.severity})"
|
|
16
|
+
# puts "Fixed in: #{vuln.fixed_version}" if vuln.fixable?
|
|
17
|
+
#
|
|
18
|
+
# @example Checking severity
|
|
19
|
+
# if vuln.critical?
|
|
20
|
+
# puts "CRITICAL: Immediate action required!"
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Sorting vulnerabilities
|
|
24
|
+
# vulns.sort.each { |v| puts v.package_name }
|
|
25
|
+
class Vulnerability
|
|
26
|
+
# Severity ranking for sorting (lower number = higher priority)
|
|
27
|
+
SEVERITY_ORDER = {
|
|
28
|
+
"CRITICAL" => 0,
|
|
29
|
+
"HIGH" => 1,
|
|
30
|
+
"MEDIUM" => 2,
|
|
31
|
+
"LOW" => 3,
|
|
32
|
+
"UNKNOWN" => 4
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# @return [Hash] Raw vulnerability data from Trivy
|
|
36
|
+
attr_reader :data
|
|
37
|
+
|
|
38
|
+
# @return [String] Target file where vulnerability was found (e.g., "Gemfile.lock")
|
|
39
|
+
attr_reader :target
|
|
40
|
+
|
|
41
|
+
# Initializes a new Vulnerability instance.
|
|
42
|
+
#
|
|
43
|
+
# @param data [Hash] Raw vulnerability data from Trivy JSON output
|
|
44
|
+
# @param target [String] Target file path where the vulnerability was found
|
|
45
|
+
#
|
|
46
|
+
# @example Create from Trivy data
|
|
47
|
+
# vuln = Vulnerability.new(data, "Gemfile.lock")
|
|
48
|
+
def initialize(data, target)
|
|
49
|
+
@data = data
|
|
50
|
+
@target = target
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the CVE or vulnerability identifier.
|
|
54
|
+
#
|
|
55
|
+
# @return [String] CVE ID (e.g., "CVE-2023-12345") or other vulnerability ID
|
|
56
|
+
#
|
|
57
|
+
# @example Get vulnerability ID
|
|
58
|
+
# vuln.id # => "CVE-2023-12345"
|
|
59
|
+
def id
|
|
60
|
+
@data["VulnerabilityID"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the name of the vulnerable package.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] Package/gem name
|
|
66
|
+
#
|
|
67
|
+
# @example Get package name
|
|
68
|
+
# vuln.package_name # => "rails"
|
|
69
|
+
def package_name
|
|
70
|
+
@data["PkgName"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the currently installed version of the vulnerable package.
|
|
74
|
+
#
|
|
75
|
+
# @return [String] Installed version number
|
|
76
|
+
def installed_version
|
|
77
|
+
@data["InstalledVersion"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the version(s) that fix this vulnerability.
|
|
81
|
+
#
|
|
82
|
+
# May contain multiple versions separated by commas (e.g., "2.1.4, 3.0.1").
|
|
83
|
+
#
|
|
84
|
+
# @return [String, nil] Fixed version string, or nil if no fix available
|
|
85
|
+
def fixed_version
|
|
86
|
+
@data["FixedVersion"]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns an array of all available fixed versions.
|
|
90
|
+
#
|
|
91
|
+
# Parses the fixed_version string and splits on commas.
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<String>] Array of fixed version strings, empty if no fix available
|
|
94
|
+
#
|
|
95
|
+
# @example Get all fixed versions
|
|
96
|
+
# vuln.fixed_versions # => ["2.1.4", "3.0.1", "4.0.0"]
|
|
97
|
+
def fixed_versions
|
|
98
|
+
return [] unless fixable?
|
|
99
|
+
|
|
100
|
+
fixed_version.split(",").map(&:strip)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns the most applicable fixed version for the installed version.
|
|
104
|
+
#
|
|
105
|
+
# Attempts to find a fixed version in the same major.minor series as the
|
|
106
|
+
# installed version. For example, if version 2.1.0 is installed and fixes
|
|
107
|
+
# exist for 2.1.4 and 3.0.1, this returns 2.1.4.
|
|
108
|
+
#
|
|
109
|
+
# @return [String, nil] Most applicable fixed version, or nil if no fix available
|
|
110
|
+
#
|
|
111
|
+
# @example Get applicable fix
|
|
112
|
+
# # If installed: 2.1.0, fixes: ["2.1.4", "3.0.1"]
|
|
113
|
+
# vuln.applicable_fixed_version # => "2.1.4"
|
|
114
|
+
def applicable_fixed_version
|
|
115
|
+
return nil unless fixable?
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
installed = Gem::Version.new(installed_version)
|
|
119
|
+
|
|
120
|
+
fixed_versions.find do |v|
|
|
121
|
+
fixed = Gem::Version.new(v)
|
|
122
|
+
fixed.segments[0..1] == installed.segments[0..1]
|
|
123
|
+
end
|
|
124
|
+
rescue ArgumentError
|
|
125
|
+
# If version parsing fails, return the first fixed version
|
|
126
|
+
fixed_versions.first
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns the severity level of this vulnerability.
|
|
131
|
+
#
|
|
132
|
+
# @return [String] Severity level: "CRITICAL", "HIGH", "MEDIUM", "LOW", or "UNKNOWN"
|
|
133
|
+
#
|
|
134
|
+
# @example Get severity
|
|
135
|
+
# vuln.severity # => "CRITICAL"
|
|
136
|
+
def severity
|
|
137
|
+
sev = @data["Severity"]
|
|
138
|
+
return "UNKNOWN" if sev.nil? || sev.empty?
|
|
139
|
+
|
|
140
|
+
sev
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns a human-readable title for the vulnerability.
|
|
144
|
+
#
|
|
145
|
+
# @return [String] Vulnerability title or default message
|
|
146
|
+
def title
|
|
147
|
+
@data["Title"] || "No title available"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns a detailed description of the vulnerability.
|
|
151
|
+
#
|
|
152
|
+
# @return [String] Vulnerability description or default message
|
|
153
|
+
def description
|
|
154
|
+
@data["Description"] || "No description available"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns the primary URL for more information about the vulnerability.
|
|
158
|
+
#
|
|
159
|
+
# @return [String, nil] URL to vulnerability details (often NVD or GitHub Advisory)
|
|
160
|
+
def primary_url
|
|
161
|
+
@data["PrimaryURL"]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns additional reference URLs for this vulnerability.
|
|
165
|
+
#
|
|
166
|
+
# @return [Array<String>] Array of reference URLs
|
|
167
|
+
def references
|
|
168
|
+
@data["References"] || []
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the date this vulnerability was published.
|
|
172
|
+
#
|
|
173
|
+
# @return [String, nil] Publication date in ISO 8601 format
|
|
174
|
+
def published_date
|
|
175
|
+
@data["PublishedDate"]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Checks if this is a CRITICAL severity vulnerability.
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] true if severity is CRITICAL
|
|
181
|
+
def critical?
|
|
182
|
+
severity == "CRITICAL"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Checks if this is a HIGH severity vulnerability.
|
|
186
|
+
#
|
|
187
|
+
# @return [Boolean] true if severity is HIGH
|
|
188
|
+
def high?
|
|
189
|
+
severity == "HIGH"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Checks if this is a MEDIUM severity vulnerability.
|
|
193
|
+
#
|
|
194
|
+
# @return [Boolean] true if severity is MEDIUM
|
|
195
|
+
def medium?
|
|
196
|
+
severity == "MEDIUM"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Checks if this is a LOW severity vulnerability.
|
|
200
|
+
#
|
|
201
|
+
# @return [Boolean] true if severity is LOW
|
|
202
|
+
def low?
|
|
203
|
+
severity == "LOW"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns the numeric rank for this severity level.
|
|
207
|
+
#
|
|
208
|
+
# Lower numbers indicate higher severity. Used for sorting.
|
|
209
|
+
#
|
|
210
|
+
# @return [Integer] Severity rank (0-4, or 999 for unknown)
|
|
211
|
+
def severity_rank
|
|
212
|
+
SEVERITY_ORDER[severity] || 999
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Compares this vulnerability with another for sorting.
|
|
216
|
+
#
|
|
217
|
+
# Vulnerabilities are sorted by severity (critical first), then by package name.
|
|
218
|
+
#
|
|
219
|
+
# @param other [Vulnerability] Another vulnerability to compare with
|
|
220
|
+
# @return [Integer] -1, 0, or 1 for sorting
|
|
221
|
+
#
|
|
222
|
+
# @example Sort vulnerabilities
|
|
223
|
+
# vulnerabilities.sort # Critical vulnerabilities first
|
|
224
|
+
def <=>(other)
|
|
225
|
+
# Sort by severity first (critical first), then by package name
|
|
226
|
+
comparison = severity_rank <=> other.severity_rank
|
|
227
|
+
comparison.zero? ? package_name <=> other.package_name : comparison
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Checks if a fix is available for this vulnerability.
|
|
231
|
+
#
|
|
232
|
+
# @return [Boolean] true if a fixed version exists
|
|
233
|
+
#
|
|
234
|
+
# @example Check if fixable
|
|
235
|
+
# if vuln.fixable?
|
|
236
|
+
# puts "Update to #{vuln.fixed_version}"
|
|
237
|
+
# else
|
|
238
|
+
# puts "No fix available yet"
|
|
239
|
+
# end
|
|
240
|
+
def fixable?
|
|
241
|
+
!fixed_version.nil? && !fixed_version.empty?
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
data/plugins.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# plugins.rb
|
|
2
|
+
require_relative "lib/bundler/trivy/plugin"
|
|
3
|
+
|
|
4
|
+
Bundler::Plugin.register("bundler-trivy-plugin") do |plugin|
|
|
5
|
+
# Registers the plugin with Bundler and declares a hook.
|
|
6
|
+
# The 'after-install-all' hook ensures our scanning logic runs
|
|
7
|
+
# immediately after all gems have been installed.
|
|
8
|
+
plugin.hook("after-install-all") do
|
|
9
|
+
Bundler::Trivy::Plugin.scan_after_install
|
|
10
|
+
end
|
|
11
|
+
end
|