gem_guard 0.1.10 ā 1.1.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 +4 -4
- data/lib/gem_guard/auto_fixer.rb +162 -0
- data/lib/gem_guard/cli.rb +81 -0
- data/lib/gem_guard/version.rb +1 -1
- data/lib/gem_guard.rb +3 -2
- data/test_nokogiri.lock.backup.20250810_002252 +13 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2870725c07a4b4c39c53e01c031663a934db6f9e457ad75f509d2ee6e3e35684
|
4
|
+
data.tar.gz: 6f630c2ef6939a0c5de8c3b8982781a907065b8c1a3901483f1101910387e244
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b7c598cd6789dfdf5d3b90c82fc9f2ade69c730418f1d2a9f6ba6c22555fad01b7e4bbd438bbb03d6116f429176a407e9b0f13221c47fd2cd0e9e2175563e98
|
7
|
+
data.tar.gz: 822e03ed7fa56e17d170abf92db8677fb27f92ff04d0f52ecdc1793c73db9aab3052e37e6cbcb28b7a217f3a30a7fb062a3860169f908067b2c3672964d9f977
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require "bundler"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
module GemGuard
|
5
|
+
class AutoFixer
|
6
|
+
def initialize(lockfile_path = "Gemfile.lock", gemfile_path = "Gemfile")
|
7
|
+
@lockfile_path = lockfile_path
|
8
|
+
@gemfile_path = gemfile_path
|
9
|
+
@backup_created = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def fix_vulnerabilities(vulnerable_dependencies, options = {})
|
13
|
+
dry_run = options.fetch(:dry_run, false)
|
14
|
+
interactive = options.fetch(:interactive, false)
|
15
|
+
create_backup = options.fetch(:backup, true)
|
16
|
+
|
17
|
+
unless File.exist?(@gemfile_path)
|
18
|
+
raise "Gemfile not found at #{@gemfile_path}. Auto-fix requires a Gemfile."
|
19
|
+
end
|
20
|
+
|
21
|
+
unless File.exist?(@lockfile_path)
|
22
|
+
raise "Gemfile.lock not found at #{@lockfile_path}. Run 'bundle install' first."
|
23
|
+
end
|
24
|
+
|
25
|
+
fixes = plan_fixes(vulnerable_dependencies)
|
26
|
+
|
27
|
+
if fixes.empty?
|
28
|
+
return {status: :no_fixes_needed, message: "No automatic fixes available."}
|
29
|
+
end
|
30
|
+
|
31
|
+
if dry_run
|
32
|
+
return {status: :dry_run, fixes: fixes, message: "Dry run completed. #{fixes.length} fixes planned."}
|
33
|
+
end
|
34
|
+
|
35
|
+
if interactive && !confirm_fixes(fixes)
|
36
|
+
return {status: :cancelled, message: "Fix operation cancelled by user."}
|
37
|
+
end
|
38
|
+
|
39
|
+
create_lockfile_backup if create_backup
|
40
|
+
|
41
|
+
applied_fixes = apply_fixes(fixes)
|
42
|
+
|
43
|
+
{
|
44
|
+
status: :completed,
|
45
|
+
fixes: applied_fixes,
|
46
|
+
message: "Applied #{applied_fixes.length} fixes successfully."
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def plan_fixes(vulnerable_dependencies)
|
53
|
+
fixes = []
|
54
|
+
|
55
|
+
vulnerable_dependencies.each do |vuln_dep|
|
56
|
+
dependency = vuln_dep.dependency
|
57
|
+
vulnerability = vuln_dep.vulnerability
|
58
|
+
|
59
|
+
# Extract the recommended version from the fix suggestion
|
60
|
+
recommended_version = extract_version_from_fix(vuln_dep.recommended_fix)
|
61
|
+
|
62
|
+
next unless recommended_version
|
63
|
+
|
64
|
+
# Check if the recommended version is available and safe
|
65
|
+
if version_available_and_safe?(dependency.name, recommended_version)
|
66
|
+
fixes << {
|
67
|
+
gem_name: dependency.name,
|
68
|
+
current_version: dependency.version,
|
69
|
+
target_version: recommended_version,
|
70
|
+
vulnerability_id: vulnerability.id,
|
71
|
+
severity: vulnerability.severity
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
fixes
|
77
|
+
end
|
78
|
+
|
79
|
+
def extract_version_from_fix(fix_command)
|
80
|
+
# Extract version from commands like "bundle update nokogiri --to 1.18.9"
|
81
|
+
match = fix_command.match(/--to\s+([^\s]+)/)
|
82
|
+
match ? match[1] : nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def version_available_and_safe?(gem_name, version)
|
86
|
+
# Check if the version exists on RubyGems
|
87
|
+
# This is a simplified check - in production, you might want more robust validation
|
88
|
+
return false if version.nil? || version.empty?
|
89
|
+
|
90
|
+
# Basic semantic version validation
|
91
|
+
version.match?(/^\d+\.\d+(\.\d+)?/)
|
92
|
+
end
|
93
|
+
|
94
|
+
def confirm_fixes(fixes)
|
95
|
+
puts "\nš§ Planned Fixes:"
|
96
|
+
puts "=" * 50
|
97
|
+
|
98
|
+
fixes.each do |fix|
|
99
|
+
severity_emoji = severity_emoji(fix[:severity])
|
100
|
+
puts "#{severity_emoji} #{fix[:gem_name]}: #{fix[:current_version]} ā #{fix[:target_version]}"
|
101
|
+
puts " Fixes: #{fix[:vulnerability_id]}"
|
102
|
+
end
|
103
|
+
|
104
|
+
puts "\nā ļø This will modify your Gemfile.lock and may require bundle install."
|
105
|
+
print "Do you want to proceed? (y/N): "
|
106
|
+
|
107
|
+
response = $stdin.gets.chomp.downcase
|
108
|
+
response == "y" || response == "yes"
|
109
|
+
end
|
110
|
+
|
111
|
+
def severity_emoji(severity)
|
112
|
+
case severity.to_s.downcase
|
113
|
+
when /critical/
|
114
|
+
"š“"
|
115
|
+
when /high/
|
116
|
+
"š "
|
117
|
+
when /medium/
|
118
|
+
"š”"
|
119
|
+
else
|
120
|
+
"š¢"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_lockfile_backup
|
125
|
+
return if @backup_created
|
126
|
+
|
127
|
+
backup_path = "#{@lockfile_path}.backup.#{Time.now.strftime("%Y%m%d_%H%M%S")}"
|
128
|
+
FileUtils.cp(@lockfile_path, backup_path)
|
129
|
+
@backup_created = true
|
130
|
+
puts "š¦ Created backup: #{backup_path}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def apply_fixes(fixes)
|
134
|
+
applied_fixes = []
|
135
|
+
|
136
|
+
fixes.each do |fix|
|
137
|
+
if apply_single_fix(fix)
|
138
|
+
applied_fixes << fix
|
139
|
+
puts "ā
Updated #{fix[:gem_name]} to #{fix[:target_version]}"
|
140
|
+
else
|
141
|
+
puts "ā Failed to update #{fix[:gem_name]}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Run bundle install to update the lockfile
|
146
|
+
if applied_fixes.any?
|
147
|
+
puts "\nš Running bundle install to update lockfile..."
|
148
|
+
system("bundle install")
|
149
|
+
end
|
150
|
+
|
151
|
+
applied_fixes
|
152
|
+
end
|
153
|
+
|
154
|
+
def apply_single_fix(fix)
|
155
|
+
# Use bundler to update the specific gem
|
156
|
+
command = "bundle update #{fix[:gem_name]} --conservative"
|
157
|
+
|
158
|
+
# Execute the bundle update command
|
159
|
+
system(command, out: File::NULL, err: File::NULL)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/gem_guard/cli.rb
CHANGED
@@ -190,6 +190,87 @@ module GemGuard
|
|
190
190
|
end
|
191
191
|
end
|
192
192
|
|
193
|
+
desc "fix", "Automatically fix vulnerable dependencies"
|
194
|
+
option :lockfile, type: :string, desc: "Path to Gemfile.lock"
|
195
|
+
option :gemfile, type: :string, desc: "Path to Gemfile"
|
196
|
+
option :dry_run, type: :boolean, desc: "Show planned fixes without applying them"
|
197
|
+
option :interactive, type: :boolean, desc: "Ask for confirmation before applying fixes"
|
198
|
+
option :no_backup, type: :boolean, desc: "Skip creating backup of Gemfile.lock"
|
199
|
+
option :config, type: :string, desc: "Config file path"
|
200
|
+
def fix
|
201
|
+
config = Config.new(options[:config] || ".gemguard.yml")
|
202
|
+
|
203
|
+
lockfile_path = options[:lockfile] || config.lockfile_path
|
204
|
+
gemfile_path = options[:gemfile] || "Gemfile"
|
205
|
+
dry_run = options[:dry_run] || false
|
206
|
+
interactive = options[:interactive] || false
|
207
|
+
create_backup = !options[:no_backup]
|
208
|
+
|
209
|
+
unless File.exist?(lockfile_path)
|
210
|
+
puts "Error: #{lockfile_path} not found"
|
211
|
+
exit EXIT_ERROR
|
212
|
+
end
|
213
|
+
|
214
|
+
unless File.exist?(gemfile_path)
|
215
|
+
puts "Error: #{gemfile_path} not found. Auto-fix requires a Gemfile."
|
216
|
+
exit EXIT_ERROR
|
217
|
+
end
|
218
|
+
|
219
|
+
begin
|
220
|
+
# First, scan for vulnerabilities
|
221
|
+
dependencies = Parser.new.parse(lockfile_path)
|
222
|
+
vulnerabilities = VulnerabilityFetcher.new.fetch_for(dependencies)
|
223
|
+
analysis = Analyzer.new.analyze(dependencies, vulnerabilities)
|
224
|
+
|
225
|
+
if analysis.vulnerable_dependencies.empty?
|
226
|
+
puts "ā
No vulnerabilities found. Nothing to fix!"
|
227
|
+
exit EXIT_SUCCESS
|
228
|
+
end
|
229
|
+
|
230
|
+
# Apply fixes
|
231
|
+
auto_fixer = AutoFixer.new(lockfile_path, gemfile_path)
|
232
|
+
result = auto_fixer.fix_vulnerabilities(
|
233
|
+
analysis.vulnerable_dependencies,
|
234
|
+
dry_run: dry_run,
|
235
|
+
interactive: interactive,
|
236
|
+
backup: create_backup
|
237
|
+
)
|
238
|
+
|
239
|
+
case result[:status]
|
240
|
+
when :no_fixes_needed
|
241
|
+
puts "ā¹ļø #{result[:message]}"
|
242
|
+
exit EXIT_SUCCESS
|
243
|
+
when :dry_run
|
244
|
+
puts "š Dry Run Results:"
|
245
|
+
puts "=" * 40
|
246
|
+
result[:fixes].each do |fix|
|
247
|
+
puts "#{fix[:gem_name]}: #{fix[:current_version]} ā #{fix[:target_version]}"
|
248
|
+
puts " Fixes: #{fix[:vulnerability_id]} (#{fix[:severity]})"
|
249
|
+
end
|
250
|
+
puts "\n#{result[:message]}"
|
251
|
+
puts "Run without --dry-run to apply these fixes."
|
252
|
+
exit EXIT_SUCCESS
|
253
|
+
when :cancelled
|
254
|
+
puts "ā #{result[:message]}"
|
255
|
+
exit EXIT_SUCCESS
|
256
|
+
when :completed
|
257
|
+
puts "š #{result[:message]}"
|
258
|
+
puts "\nš Applied Fixes:"
|
259
|
+
result[:fixes].each do |fix|
|
260
|
+
puts "ā
#{fix[:gem_name]}: #{fix[:current_version]} ā #{fix[:target_version]}"
|
261
|
+
end
|
262
|
+
puts "\nš” Run 'gem_guard scan' to verify fixes."
|
263
|
+
exit EXIT_SUCCESS
|
264
|
+
else
|
265
|
+
puts "ā Unexpected error during fix operation"
|
266
|
+
exit EXIT_ERROR
|
267
|
+
end
|
268
|
+
rescue => e
|
269
|
+
puts "Error: #{e.message}"
|
270
|
+
exit EXIT_ERROR
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
193
274
|
desc "version", "Show gem_guard version"
|
194
275
|
def version
|
195
276
|
puts GemGuard::VERSION
|
data/lib/gem_guard/version.rb
CHANGED
data/lib/gem_guard.rb
CHANGED
@@ -3,10 +3,11 @@ require_relative "gem_guard/parser"
|
|
3
3
|
require_relative "gem_guard/vulnerability_fetcher"
|
4
4
|
require_relative "gem_guard/analyzer"
|
5
5
|
require_relative "gem_guard/reporter"
|
6
|
+
require_relative "gem_guard/cli"
|
7
|
+
require_relative "gem_guard/config"
|
6
8
|
require_relative "gem_guard/sbom_generator"
|
7
9
|
require_relative "gem_guard/typosquat_checker"
|
8
|
-
require_relative "gem_guard/
|
9
|
-
require_relative "gem_guard/cli"
|
10
|
+
require_relative "gem_guard/auto_fixer"
|
10
11
|
|
11
12
|
module GemGuard
|
12
13
|
class Error < StandardError; end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gem_guard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilbur Suero
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- gem_guard.gemspec
|
100
100
|
- lib/gem_guard.rb
|
101
101
|
- lib/gem_guard/analyzer.rb
|
102
|
+
- lib/gem_guard/auto_fixer.rb
|
102
103
|
- lib/gem_guard/cli.rb
|
103
104
|
- lib/gem_guard/config.rb
|
104
105
|
- lib/gem_guard/parser.rb
|
@@ -112,6 +113,7 @@ files:
|
|
112
113
|
- templates/github-actions.yml
|
113
114
|
- templates/gitlab-ci.yml
|
114
115
|
- test_nokogiri.lock
|
116
|
+
- test_nokogiri.lock.backup.20250810_002252
|
115
117
|
homepage: https://github.com/wilburhimself/gem_guard
|
116
118
|
licenses:
|
117
119
|
- MIT
|