rubion 0.3.3 → 0.3.4
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/rubion/reporter.rb +12 -3
- data/lib/rubion/scanner.rb +198 -143
- data/lib/rubion/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca049f036597e496afc3e38e4aa0005588d7d5ea544242566b8bb3b4b88ae5a1
|
|
4
|
+
data.tar.gz: 8f3c8b61f3cd5aee897220695adca2c70c334ec0c9fff8991421fe13df08caa5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fc7ed3e75f7693842cad49a84f23f485191d85348216f5f4bb8a9194cb8d879a3c1212b83e891f9e4238468dced1303f1c6813bdf5c784679b87025cb21be1a
|
|
7
|
+
data.tar.gz: 7770cc5710d9ab0386e1fc17aa7d9ce9e8ec5690d1c8429959caf9bdbcdccb257e129c2d6b6d86e50b5d577eb5ba403504c648757c185345cb9fe99f9d124cb4
|
data/lib/rubion/reporter.rb
CHANGED
|
@@ -82,8 +82,11 @@ module Rubion
|
|
|
82
82
|
t.headings = ['Name', 'Current', 'Date', 'Latest', 'Date', 'Behind By(Time)', 'Behind By(Versions)']
|
|
83
83
|
|
|
84
84
|
versions.each do |gem|
|
|
85
|
+
# Add ✅ prefix for direct dependencies
|
|
86
|
+
gem_name = gem[:direct] ? "#{gem[:gem]} ✅ " : gem[:gem]
|
|
87
|
+
|
|
85
88
|
t.add_row [
|
|
86
|
-
|
|
89
|
+
gem_name,
|
|
87
90
|
gem[:current],
|
|
88
91
|
gem[:current_date] || 'N/A',
|
|
89
92
|
gem[:latest],
|
|
@@ -139,8 +142,11 @@ module Rubion
|
|
|
139
142
|
t.headings = ['Name', 'Current', 'Date', 'Latest', 'Date', 'Behind By(Time)', 'Behind By(Versions)']
|
|
140
143
|
|
|
141
144
|
versions.each do |pkg|
|
|
145
|
+
# Add ✅ prefix for direct dependencies
|
|
146
|
+
package_name = pkg[:direct] ? "✅ #{pkg[:package]}" : pkg[:package]
|
|
147
|
+
|
|
142
148
|
t.add_row [
|
|
143
|
-
|
|
149
|
+
package_name,
|
|
144
150
|
pkg[:current],
|
|
145
151
|
pkg[:current_date] || 'N/A',
|
|
146
152
|
pkg[:latest],
|
|
@@ -248,7 +254,10 @@ module Rubion
|
|
|
248
254
|
sorted = versions.sort_by do |item|
|
|
249
255
|
case normalized_column
|
|
250
256
|
when 'name'
|
|
251
|
-
|
|
257
|
+
# Remove ✅ prefix for sorting
|
|
258
|
+
name = item[name_key_sym].to_s
|
|
259
|
+
name = name.sub(/^✅\s+/, '') if name.start_with?('✅')
|
|
260
|
+
name.downcase
|
|
252
261
|
when 'current'
|
|
253
262
|
parse_version_for_sort(item[:current])
|
|
254
263
|
when 'date'
|
data/lib/rubion/scanner.rb
CHANGED
|
@@ -6,7 +6,6 @@ require_relative 'reporter'
|
|
|
6
6
|
require 'net/http'
|
|
7
7
|
require 'uri'
|
|
8
8
|
require 'date'
|
|
9
|
-
require 'thread'
|
|
10
9
|
|
|
11
10
|
module Rubion
|
|
12
11
|
class Scanner
|
|
@@ -26,37 +25,39 @@ module Rubion
|
|
|
26
25
|
@result = ScanResult.new
|
|
27
26
|
@package_manager = package_manager
|
|
28
27
|
@package_manager_detected = false
|
|
28
|
+
@direct_gems = nil
|
|
29
|
+
@direct_packages = nil
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def scan
|
|
32
33
|
puts "🔍 Scanning project at: #{@project_path}\n\n"
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
scan_ruby_gems
|
|
35
36
|
scan_npm_packages
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
@result
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def scan_incremental(options = { gems: true, packages: true, sort_by:
|
|
41
|
+
def scan_incremental(options = { gems: true, packages: true, sort_by: 'Behind By(Time)', sort_desc: true })
|
|
41
42
|
puts "🔍 Scanning project at: #{@project_path}\n\n"
|
|
42
|
-
|
|
43
|
+
|
|
43
44
|
# Scan and display Ruby gems first (if enabled)
|
|
44
45
|
if options[:gems]
|
|
45
46
|
scan_ruby_gems
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
# Print gem results immediately
|
|
48
49
|
puts "\n"
|
|
49
50
|
reporter = Reporter.new(@result, sort_by: options[:sort_by], sort_desc: options[:sort_desc])
|
|
50
51
|
reporter.print_gem_vulnerabilities
|
|
51
52
|
reporter.print_gem_versions
|
|
52
53
|
end
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
# Then scan NPM packages (if enabled)
|
|
55
56
|
if options[:packages]
|
|
56
|
-
puts "\n" if options[:gems]
|
|
57
|
+
puts "\n" if options[:gems] # Add spacing if gems were scanned
|
|
57
58
|
scan_npm_packages
|
|
58
59
|
end
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
@result
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -64,10 +65,10 @@ module Rubion
|
|
|
64
65
|
|
|
65
66
|
def scan_ruby_gems
|
|
66
67
|
return unless File.exist?(File.join(@project_path, 'Gemfile.lock'))
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
# Check for vulnerabilities using bundler-audit
|
|
69
70
|
check_gem_vulnerabilities
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
# Check for outdated versions using bundle outdated (will show progress)
|
|
72
73
|
check_gem_versions
|
|
73
74
|
end
|
|
@@ -75,86 +76,86 @@ module Rubion
|
|
|
75
76
|
def scan_npm_packages
|
|
76
77
|
package_json = File.join(@project_path, 'package.json')
|
|
77
78
|
return unless File.exist?(package_json)
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
# Detect package manager if not already set
|
|
80
81
|
unless @package_manager_detected
|
|
81
|
-
@package_manager
|
|
82
|
+
@package_manager ||= detect_package_manager
|
|
82
83
|
@package_manager_detected = true
|
|
83
84
|
end
|
|
84
|
-
|
|
85
|
+
|
|
85
86
|
unless @package_manager
|
|
86
|
-
puts
|
|
87
|
+
puts ' ⚠️ Neither npm nor yarn is available. Skipping package scanning.'
|
|
87
88
|
return
|
|
88
89
|
end
|
|
89
|
-
|
|
90
|
+
|
|
90
91
|
# Check for vulnerabilities using package manager audit
|
|
91
92
|
check_npm_vulnerabilities
|
|
92
|
-
|
|
93
|
+
|
|
93
94
|
# Check for outdated versions using package manager outdated (will show progress)
|
|
94
95
|
check_npm_versions
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
def check_gem_vulnerabilities
|
|
98
99
|
# Try to use bundler-audit if available
|
|
99
|
-
stdout, stderr, status = Open3.capture3(
|
|
100
|
-
|
|
100
|
+
stdout, stderr, status = Open3.capture3('bundle-audit check 2>&1', chdir: @project_path)
|
|
101
|
+
|
|
101
102
|
# bundle-audit returns exit code 1 when vulnerabilities are found, 0 when none found
|
|
102
103
|
# Always parse if there's output (vulnerabilities found) or if it succeeded (no vulnerabilities)
|
|
103
|
-
if stdout.include?(
|
|
104
|
+
if stdout.include?('vulnerabilities found') || stdout.include?('Name:') || status.success?
|
|
104
105
|
parse_bundler_audit_output(stdout)
|
|
105
106
|
else
|
|
106
107
|
# No vulnerabilities found or bundler-audit not available
|
|
107
108
|
@result.gem_vulnerabilities = []
|
|
108
109
|
end
|
|
109
|
-
rescue => e
|
|
110
|
+
rescue StandardError => e
|
|
110
111
|
puts " ⚠️ Could not run bundle-audit (#{e.message}). Skipping gem vulnerability check."
|
|
111
112
|
@result.gem_vulnerabilities = []
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
def check_gem_versions
|
|
115
|
-
stdout, stderr, status = Open3.capture3(
|
|
116
|
-
|
|
116
|
+
stdout, stderr, status = Open3.capture3('bundle outdated --parseable', chdir: @project_path)
|
|
117
|
+
|
|
117
118
|
if status.success? || !stdout.empty?
|
|
118
119
|
parse_bundle_outdated_output(stdout)
|
|
119
120
|
else
|
|
120
121
|
# No outdated gems found
|
|
121
122
|
@result.gem_versions = []
|
|
122
123
|
end
|
|
123
|
-
rescue => e
|
|
124
|
+
rescue StandardError => e
|
|
124
125
|
puts " ⚠️ Could not run bundle outdated (#{e.message}). Skipping gem version check."
|
|
125
126
|
@result.gem_versions = []
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def check_npm_vulnerabilities
|
|
129
130
|
return unless @package_manager
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
command = "#{@package_manager} audit --json 2>&1"
|
|
132
133
|
stdout, stderr, status = Open3.capture3(command, chdir: @project_path)
|
|
133
|
-
|
|
134
|
+
|
|
134
135
|
begin
|
|
135
136
|
data = JSON.parse(stdout)
|
|
136
137
|
parse_npm_audit_output(data)
|
|
137
138
|
rescue JSON::ParserError
|
|
138
139
|
@result.package_vulnerabilities = []
|
|
139
140
|
end
|
|
140
|
-
rescue => e
|
|
141
|
+
rescue StandardError => e
|
|
141
142
|
puts " ⚠️ Could not run #{@package_manager} audit (#{e.message}). Skipping package vulnerability check."
|
|
142
143
|
@result.package_vulnerabilities = []
|
|
143
144
|
end
|
|
144
145
|
|
|
145
146
|
def check_npm_versions
|
|
146
147
|
return unless @package_manager
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
command = "#{@package_manager} outdated --json 2>&1"
|
|
149
150
|
stdout, stderr, status = Open3.capture3(command, chdir: @project_path)
|
|
150
|
-
|
|
151
|
+
|
|
151
152
|
begin
|
|
152
153
|
data = JSON.parse(stdout) unless stdout.empty?
|
|
153
154
|
parse_npm_outdated_output(data || {})
|
|
154
155
|
rescue JSON::ParserError
|
|
155
156
|
@result.package_versions = []
|
|
156
157
|
end
|
|
157
|
-
rescue => e
|
|
158
|
+
rescue StandardError => e
|
|
158
159
|
puts " ⚠️ Could not run #{@package_manager} outdated (#{e.message}). Skipping package version check."
|
|
159
160
|
@result.package_versions = []
|
|
160
161
|
end
|
|
@@ -164,86 +165,86 @@ module Rubion
|
|
|
164
165
|
def parse_bundler_audit_output(output)
|
|
165
166
|
vulnerabilities = []
|
|
166
167
|
current_gem = nil
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
output.each_line do |line|
|
|
169
170
|
line = line.strip
|
|
170
171
|
next if line.empty?
|
|
171
|
-
|
|
172
|
+
|
|
172
173
|
if line =~ /^Name: (.+)/
|
|
173
|
-
current_gem = { gem:
|
|
174
|
+
current_gem = { gem: ::Regexp.last_match(1).strip }
|
|
174
175
|
elsif line =~ /^Version: (.+)/ && current_gem
|
|
175
|
-
current_gem[:version] =
|
|
176
|
+
current_gem[:version] = ::Regexp.last_match(1).strip
|
|
176
177
|
elsif line =~ /^CVE: (.+)/ && current_gem
|
|
177
|
-
current_gem[:advisory] =
|
|
178
|
+
current_gem[:advisory] = ::Regexp.last_match(1).strip
|
|
178
179
|
elsif line =~ /^Advisory: (.+)/ && current_gem
|
|
179
180
|
# Fallback for older bundle-audit versions
|
|
180
|
-
current_gem[:advisory] =
|
|
181
|
+
current_gem[:advisory] = ::Regexp.last_match(1).strip
|
|
181
182
|
elsif line =~ /^Criticality: (.+)/ && current_gem
|
|
182
|
-
current_gem[:severity] =
|
|
183
|
+
current_gem[:severity] = ::Regexp.last_match(1).strip
|
|
183
184
|
elsif line =~ /^Title: (.+)/ && current_gem
|
|
184
|
-
current_gem[:title] =
|
|
185
|
+
current_gem[:title] = ::Regexp.last_match(1).strip
|
|
185
186
|
# Only add if we have at least name, version, and title
|
|
186
|
-
if current_gem[:gem] && current_gem[:version] && current_gem[:title]
|
|
187
|
-
vulnerabilities << current_gem
|
|
188
|
-
end
|
|
187
|
+
vulnerabilities << current_gem if current_gem[:gem] && current_gem[:version] && current_gem[:title]
|
|
189
188
|
current_gem = nil
|
|
190
189
|
end
|
|
191
190
|
end
|
|
192
|
-
|
|
191
|
+
|
|
193
192
|
# Handle case where vulnerability block ends without Title (use CVE as title)
|
|
194
193
|
if current_gem && current_gem[:gem] && current_gem[:version]
|
|
195
|
-
current_gem[:title] ||= current_gem[:advisory] ||
|
|
194
|
+
current_gem[:title] ||= current_gem[:advisory] || 'Vulnerability detected'
|
|
196
195
|
vulnerabilities << current_gem
|
|
197
196
|
end
|
|
198
|
-
|
|
197
|
+
|
|
199
198
|
@result.gem_vulnerabilities = vulnerabilities
|
|
200
199
|
end
|
|
201
200
|
|
|
202
201
|
def parse_bundle_outdated_output(output)
|
|
203
202
|
versions = []
|
|
204
203
|
lines_to_process = []
|
|
205
|
-
|
|
204
|
+
|
|
206
205
|
# First pass: collect all lines to process
|
|
207
206
|
output.each_line do |line|
|
|
208
207
|
next if line.strip.empty?
|
|
209
|
-
|
|
208
|
+
|
|
210
209
|
# Parse format: gem_name (newest version, installed version, requested version)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
210
|
+
next unless line =~ /^(.+?)\s+\(newest\s+(.+?),\s+installed\s+(.+?)(?:,|\))/
|
|
211
|
+
|
|
212
|
+
lines_to_process << {
|
|
213
|
+
gem_name: ::Regexp.last_match(1).strip,
|
|
214
|
+
current_version: ::Regexp.last_match(3).strip,
|
|
215
|
+
latest_version: ::Regexp.last_match(2).strip
|
|
216
|
+
}
|
|
218
217
|
end
|
|
219
|
-
|
|
218
|
+
|
|
220
219
|
total = lines_to_process.size
|
|
221
|
-
|
|
220
|
+
|
|
222
221
|
# Process in parallel with threads (limit to 10 concurrent requests)
|
|
223
222
|
mutex = Mutex.new
|
|
224
223
|
thread_pool = []
|
|
225
224
|
max_threads = 10
|
|
226
|
-
|
|
225
|
+
|
|
227
226
|
lines_to_process.each_with_index do |line_data, index|
|
|
228
227
|
# Wait if we have too many threads
|
|
229
|
-
if thread_pool.size >= max_threads
|
|
230
|
-
|
|
231
|
-
end
|
|
232
|
-
|
|
228
|
+
thread_pool.shift.join if thread_pool.size >= max_threads
|
|
229
|
+
|
|
233
230
|
thread = Thread.new do
|
|
234
231
|
# Fetch all version info once per gem (includes dates and version list)
|
|
235
232
|
gem_data = fetch_gem_all_versions(line_data[:gem_name])
|
|
236
|
-
|
|
233
|
+
|
|
237
234
|
# Extract dates for current and latest versions
|
|
238
235
|
current_date = gem_data[:versions][line_data[:current_version]] || 'N/A'
|
|
239
236
|
latest_date = gem_data[:versions][line_data[:latest_version]] || 'N/A'
|
|
240
|
-
|
|
237
|
+
|
|
241
238
|
# Calculate time difference
|
|
242
239
|
time_diff = calculate_time_difference(current_date, latest_date)
|
|
243
|
-
|
|
240
|
+
|
|
244
241
|
# Count versions between current and latest
|
|
245
|
-
version_count = count_versions_from_list(gem_data[:version_list], line_data[:current_version],
|
|
246
|
-
|
|
242
|
+
version_count = count_versions_from_list(gem_data[:version_list], line_data[:current_version],
|
|
243
|
+
line_data[:latest_version])
|
|
244
|
+
|
|
245
|
+
# Check if this is a direct dependency
|
|
246
|
+
direct_dependency = is_direct_gem?(line_data[:gem_name])
|
|
247
|
+
|
|
247
248
|
result = {
|
|
248
249
|
gem: line_data[:gem_name],
|
|
249
250
|
current: line_data[:current_version],
|
|
@@ -252,38 +253,39 @@ module Rubion
|
|
|
252
253
|
latest_date: latest_date,
|
|
253
254
|
time_diff: time_diff,
|
|
254
255
|
version_count: version_count,
|
|
256
|
+
direct: direct_dependency,
|
|
255
257
|
index: index
|
|
256
258
|
}
|
|
257
|
-
|
|
259
|
+
|
|
258
260
|
mutex.synchronize do
|
|
259
261
|
versions << result
|
|
260
262
|
print "\r📦 Checking Ruby gems... #{versions.size}/#{total}"
|
|
261
263
|
$stdout.flush
|
|
262
264
|
end
|
|
263
265
|
end
|
|
264
|
-
|
|
266
|
+
|
|
265
267
|
thread_pool << thread
|
|
266
268
|
end
|
|
267
|
-
|
|
269
|
+
|
|
268
270
|
# Wait for all threads to complete
|
|
269
271
|
thread_pool.each(&:join)
|
|
270
|
-
|
|
272
|
+
|
|
271
273
|
# Sort by original index to maintain order
|
|
272
274
|
versions.sort_by! { |v| v[:index] }
|
|
273
275
|
versions.each { |v| v.delete(:index) }
|
|
274
|
-
|
|
276
|
+
|
|
275
277
|
puts "\r📦 Checking Ruby gems... #{total}/#{total} ✓" if total > 0
|
|
276
|
-
|
|
278
|
+
|
|
277
279
|
@result.gem_versions = versions
|
|
278
280
|
end
|
|
279
281
|
|
|
280
282
|
def parse_npm_audit_output(data)
|
|
281
283
|
vulnerabilities = []
|
|
282
|
-
|
|
284
|
+
|
|
283
285
|
if data['vulnerabilities'] && data['vulnerabilities'].is_a?(Hash)
|
|
284
286
|
data['vulnerabilities'].each do |name, info|
|
|
285
287
|
next unless info.is_a?(Hash)
|
|
286
|
-
|
|
288
|
+
|
|
287
289
|
# Extract title from via array
|
|
288
290
|
title = 'Vulnerability detected'
|
|
289
291
|
if info['via'].is_a?(Array) && info['via'].first.is_a?(Hash)
|
|
@@ -291,7 +293,7 @@ module Rubion
|
|
|
291
293
|
elsif info['via'].is_a?(String)
|
|
292
294
|
title = info['via']
|
|
293
295
|
end
|
|
294
|
-
|
|
296
|
+
|
|
295
297
|
vulnerabilities << {
|
|
296
298
|
package: name,
|
|
297
299
|
version: info['range'] || info['version'] || 'unknown',
|
|
@@ -300,57 +302,59 @@ module Rubion
|
|
|
300
302
|
}
|
|
301
303
|
end
|
|
302
304
|
end
|
|
303
|
-
|
|
305
|
+
|
|
304
306
|
@result.package_vulnerabilities = vulnerabilities
|
|
305
|
-
rescue => e
|
|
307
|
+
rescue StandardError => e
|
|
306
308
|
puts " ⚠️ Error parsing npm audit data: #{e.message}"
|
|
307
309
|
@result.package_vulnerabilities = []
|
|
308
310
|
end
|
|
309
311
|
|
|
310
312
|
def parse_npm_outdated_output(data)
|
|
311
313
|
versions = []
|
|
312
|
-
|
|
314
|
+
|
|
313
315
|
if data.is_a?(Hash)
|
|
314
316
|
packages_to_process = []
|
|
315
|
-
|
|
317
|
+
|
|
316
318
|
# First pass: collect all packages to process
|
|
317
319
|
data.each do |name, info|
|
|
318
320
|
next unless info.is_a?(Hash)
|
|
319
|
-
|
|
321
|
+
|
|
320
322
|
packages_to_process << {
|
|
321
323
|
name: name,
|
|
322
324
|
current_version: info['current'] || 'unknown',
|
|
323
325
|
latest_version: info['latest'] || 'unknown'
|
|
324
326
|
}
|
|
325
327
|
end
|
|
326
|
-
|
|
328
|
+
|
|
327
329
|
total = packages_to_process.size
|
|
328
|
-
|
|
330
|
+
|
|
329
331
|
# Process in parallel with threads (limit to 10 concurrent requests)
|
|
330
332
|
mutex = Mutex.new
|
|
331
333
|
thread_pool = []
|
|
332
334
|
max_threads = 10
|
|
333
|
-
|
|
335
|
+
|
|
334
336
|
packages_to_process.each_with_index do |pkg_data, index|
|
|
335
337
|
# Wait if we have too many threads
|
|
336
|
-
if thread_pool.size >= max_threads
|
|
337
|
-
|
|
338
|
-
end
|
|
339
|
-
|
|
338
|
+
thread_pool.shift.join if thread_pool.size >= max_threads
|
|
339
|
+
|
|
340
340
|
thread = Thread.new do
|
|
341
341
|
# Fetch all version info once per package (includes dates and version list)
|
|
342
342
|
pkg_data_full = fetch_npm_all_versions(pkg_data[:name])
|
|
343
|
-
|
|
343
|
+
|
|
344
344
|
# Extract dates for current and latest versions
|
|
345
345
|
current_date = pkg_data_full[:versions][pkg_data[:current_version]] || 'N/A'
|
|
346
346
|
latest_date = pkg_data_full[:versions][pkg_data[:latest_version]] || 'N/A'
|
|
347
|
-
|
|
347
|
+
|
|
348
348
|
# Calculate time difference
|
|
349
349
|
time_diff = calculate_time_difference(current_date, latest_date)
|
|
350
|
-
|
|
350
|
+
|
|
351
351
|
# Count versions between current and latest
|
|
352
|
-
version_count = count_versions_from_list(pkg_data_full[:version_list], pkg_data[:current_version],
|
|
353
|
-
|
|
352
|
+
version_count = count_versions_from_list(pkg_data_full[:version_list], pkg_data[:current_version],
|
|
353
|
+
pkg_data[:latest_version])
|
|
354
|
+
|
|
355
|
+
# Check if this is a direct dependency
|
|
356
|
+
direct_dependency = is_direct_package?(pkg_data[:name])
|
|
357
|
+
|
|
354
358
|
result = {
|
|
355
359
|
package: pkg_data[:name],
|
|
356
360
|
current: pkg_data[:current_version],
|
|
@@ -359,31 +363,32 @@ module Rubion
|
|
|
359
363
|
latest_date: latest_date,
|
|
360
364
|
time_diff: time_diff,
|
|
361
365
|
version_count: version_count,
|
|
366
|
+
direct: direct_dependency,
|
|
362
367
|
index: index
|
|
363
368
|
}
|
|
364
|
-
|
|
369
|
+
|
|
365
370
|
mutex.synchronize do
|
|
366
371
|
versions << result
|
|
367
372
|
print "\r📦 Checking NPM packages... #{versions.size}/#{total}"
|
|
368
373
|
$stdout.flush
|
|
369
374
|
end
|
|
370
375
|
end
|
|
371
|
-
|
|
376
|
+
|
|
372
377
|
thread_pool << thread
|
|
373
378
|
end
|
|
374
|
-
|
|
379
|
+
|
|
375
380
|
# Wait for all threads to complete
|
|
376
381
|
thread_pool.each(&:join)
|
|
377
|
-
|
|
382
|
+
|
|
378
383
|
# Sort by original index to maintain order
|
|
379
384
|
versions.sort_by! { |v| v[:index] }
|
|
380
385
|
versions.each { |v| v.delete(:index) }
|
|
381
|
-
|
|
386
|
+
|
|
382
387
|
puts "\r📦 Checking NPM packages... #{total}/#{total} ✓" if total > 0
|
|
383
388
|
end
|
|
384
|
-
|
|
389
|
+
|
|
385
390
|
@result.package_versions = versions
|
|
386
|
-
rescue => e
|
|
391
|
+
rescue StandardError => e
|
|
387
392
|
puts " ⚠️ Error parsing npm outdated data: #{e.message}"
|
|
388
393
|
@result.package_versions = []
|
|
389
394
|
end
|
|
@@ -463,27 +468,27 @@ module Rubion
|
|
|
463
468
|
# Fetch all gem version info (dates and version list) from RubyGems API in one call
|
|
464
469
|
def fetch_gem_all_versions(gem_name)
|
|
465
470
|
return { versions: {}, version_list: [] } if gem_name.nil?
|
|
466
|
-
|
|
471
|
+
|
|
467
472
|
uri = URI("https://rubygems.org/api/v1/versions/#{gem_name}.json")
|
|
468
|
-
|
|
473
|
+
|
|
469
474
|
# Set timeout to avoid hanging
|
|
470
475
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
471
476
|
http.use_ssl = true
|
|
472
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
477
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
|
|
473
478
|
http.open_timeout = 2
|
|
474
479
|
http.read_timeout = 3
|
|
475
|
-
|
|
480
|
+
|
|
476
481
|
request = Net::HTTP::Get.new(uri.request_uri)
|
|
477
482
|
response = http.request(request)
|
|
478
|
-
|
|
483
|
+
|
|
479
484
|
return { versions: {}, version_list: [] } unless response.is_a?(Net::HTTPSuccess)
|
|
480
|
-
|
|
485
|
+
|
|
481
486
|
all_versions = JSON.parse(response.body)
|
|
482
|
-
|
|
487
|
+
|
|
483
488
|
# Build hash of version => date
|
|
484
489
|
version_dates = {}
|
|
485
490
|
version_list = []
|
|
486
|
-
|
|
491
|
+
|
|
487
492
|
all_versions.each do |v|
|
|
488
493
|
version_num = v['number']
|
|
489
494
|
if v['created_at']
|
|
@@ -494,9 +499,9 @@ module Rubion
|
|
|
494
499
|
end
|
|
495
500
|
version_list << version_num
|
|
496
501
|
end
|
|
497
|
-
|
|
502
|
+
|
|
498
503
|
{ versions: version_dates, version_list: version_list }
|
|
499
|
-
rescue => e
|
|
504
|
+
rescue StandardError => e
|
|
500
505
|
puts " Debug: Error fetching versions for #{gem_name}: #{e.message}" if ENV['DEBUG']
|
|
501
506
|
{ versions: {}, version_list: [] }
|
|
502
507
|
end
|
|
@@ -505,13 +510,13 @@ module Rubion
|
|
|
505
510
|
def count_versions_from_list(version_list, current_version, latest_version)
|
|
506
511
|
return 'N/A' if current_version == 'unknown' || latest_version == 'unknown' || version_list.empty?
|
|
507
512
|
return 0 if current_version == latest_version
|
|
508
|
-
|
|
513
|
+
|
|
509
514
|
# Find indices of current and latest versions
|
|
510
515
|
current_idx = version_list.index(current_version)
|
|
511
516
|
latest_idx = version_list.index(latest_version)
|
|
512
|
-
|
|
517
|
+
|
|
513
518
|
return 'N/A' if current_idx.nil? || latest_idx.nil?
|
|
514
|
-
|
|
519
|
+
|
|
515
520
|
# Count versions between (exclusive of current, inclusive of latest)
|
|
516
521
|
(latest_idx - current_idx).abs
|
|
517
522
|
end
|
|
@@ -519,33 +524,33 @@ module Rubion
|
|
|
519
524
|
# Calculate time difference between two dates
|
|
520
525
|
def calculate_time_difference(date1_str, date2_str)
|
|
521
526
|
return 'N/A' if date1_str == 'N/A' || date2_str == 'N/A'
|
|
522
|
-
|
|
527
|
+
|
|
523
528
|
begin
|
|
524
529
|
# Parse dates - convert "8/13/2025" format to Date object
|
|
525
530
|
# Split by / and create Date object
|
|
526
531
|
parts1 = date1_str.split('/').map(&:to_i)
|
|
527
532
|
parts2 = date2_str.split('/').map(&:to_i)
|
|
528
|
-
|
|
533
|
+
|
|
529
534
|
date1 = Date.new(parts1[2], parts1[0], parts1[1])
|
|
530
535
|
date2 = Date.new(parts2[2], parts2[0], parts2[1])
|
|
531
|
-
|
|
536
|
+
|
|
532
537
|
# Always use the later date minus the earlier date
|
|
533
538
|
diff = (date2 - date1).to_i.abs
|
|
534
|
-
|
|
539
|
+
|
|
535
540
|
if diff < 30
|
|
536
|
-
"#{diff} day#{diff != 1
|
|
541
|
+
"#{diff} day#{'s' if diff != 1}"
|
|
537
542
|
elsif diff < 365
|
|
538
543
|
months = (diff / 30.0).round
|
|
539
|
-
"#{months} month#{months != 1
|
|
544
|
+
"#{months} month#{'s' if months != 1}"
|
|
540
545
|
else
|
|
541
546
|
years = (diff / 365.0).round(1)
|
|
542
547
|
if years == years.to_i
|
|
543
|
-
"#{years.to_i} year#{years.to_i != 1
|
|
548
|
+
"#{years.to_i} year#{'s' if years.to_i != 1}"
|
|
544
549
|
else
|
|
545
550
|
"#{years} years"
|
|
546
551
|
end
|
|
547
552
|
end
|
|
548
|
-
rescue => e
|
|
553
|
+
rescue StandardError => e
|
|
549
554
|
puts " Debug: Error calculating time diff: #{e.message}" if ENV['DEBUG']
|
|
550
555
|
'N/A'
|
|
551
556
|
end
|
|
@@ -555,7 +560,7 @@ module Rubion
|
|
|
555
560
|
def detect_package_manager
|
|
556
561
|
npm_available = check_command_available('npm')
|
|
557
562
|
yarn_available = check_command_available('yarn')
|
|
558
|
-
|
|
563
|
+
|
|
559
564
|
if npm_available && yarn_available
|
|
560
565
|
# Both available - prompt user
|
|
561
566
|
prompt_package_manager_choice
|
|
@@ -567,28 +572,81 @@ module Rubion
|
|
|
567
572
|
nil
|
|
568
573
|
end
|
|
569
574
|
end
|
|
570
|
-
|
|
575
|
+
|
|
571
576
|
# Check if a command is available in the system
|
|
572
577
|
def check_command_available(command)
|
|
573
578
|
_, _, status = Open3.capture3("which #{command} 2>&1")
|
|
574
579
|
status.success?
|
|
575
|
-
rescue
|
|
580
|
+
rescue StandardError
|
|
576
581
|
false
|
|
577
582
|
end
|
|
578
|
-
|
|
583
|
+
|
|
584
|
+
# Parse Gemfile to get direct dependencies
|
|
585
|
+
def parse_gemfile
|
|
586
|
+
return @direct_gems if @direct_gems
|
|
587
|
+
|
|
588
|
+
gemfile_path = File.join(@project_path, 'Gemfile')
|
|
589
|
+
return [] unless File.exist?(gemfile_path)
|
|
590
|
+
|
|
591
|
+
direct_gems = []
|
|
592
|
+
content = File.read(gemfile_path)
|
|
593
|
+
|
|
594
|
+
# Match gem declarations: gem 'name', gem "name", gem('name'), gem("name")
|
|
595
|
+
# Also handle version constraints and options
|
|
596
|
+
content.scan(/^\s*gem\s+['"]([^'"]+)['"]/) do |match|
|
|
597
|
+
gem_name = match[0]
|
|
598
|
+
direct_gems << gem_name unless gem_name.nil?
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
@direct_gems = direct_gems.uniq
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Check if a gem is a direct dependency
|
|
605
|
+
def is_direct_gem?(gem_name)
|
|
606
|
+
parse_gemfile.include?(gem_name)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Parse package.json to get direct dependencies
|
|
610
|
+
def parse_package_json
|
|
611
|
+
return @direct_packages if @direct_packages
|
|
612
|
+
|
|
613
|
+
package_json_path = File.join(@project_path, 'package.json')
|
|
614
|
+
return [] unless File.exist?(package_json_path)
|
|
615
|
+
|
|
616
|
+
begin
|
|
617
|
+
content = File.read(package_json_path)
|
|
618
|
+
data = JSON.parse(content)
|
|
619
|
+
|
|
620
|
+
# Get dependencies from dependencies, devDependencies, peerDependencies, optionalDependencies
|
|
621
|
+
direct_packages = []
|
|
622
|
+
%w[dependencies devDependencies peerDependencies optionalDependencies].each do |key|
|
|
623
|
+
direct_packages.concat(data[key].keys) if data[key].is_a?(Hash)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
@direct_packages = direct_packages.uniq
|
|
627
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
628
|
+
@direct_packages = []
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Check if a package is a direct dependency
|
|
633
|
+
def is_direct_package?(package_name)
|
|
634
|
+
parse_package_json.include?(package_name)
|
|
635
|
+
end
|
|
636
|
+
|
|
579
637
|
# Prompt user to choose between npm and yarn when both are available
|
|
580
638
|
def prompt_package_manager_choice
|
|
581
639
|
puts "\n Both npm and yarn are available. Which would you like to use?"
|
|
582
|
-
print " Enter '
|
|
583
|
-
|
|
640
|
+
print " Enter 'n' for npm or 'y' for yarn (default: npm): "
|
|
641
|
+
|
|
584
642
|
choice = $stdin.gets.chomp.strip.downcase
|
|
585
|
-
|
|
586
|
-
if choice.empty? || choice == 'npm'
|
|
643
|
+
|
|
644
|
+
if choice.empty? || choice == 'n' || choice == 'npm'
|
|
587
645
|
'npm'
|
|
588
|
-
elsif
|
|
646
|
+
elsif %w[y yarn].include?(choice)
|
|
589
647
|
'yarn'
|
|
590
648
|
else
|
|
591
|
-
puts
|
|
649
|
+
puts ' ⚠️ Invalid choice. Using npm as default.'
|
|
592
650
|
'npm'
|
|
593
651
|
end
|
|
594
652
|
end
|
|
@@ -604,7 +662,7 @@ module Rubion
|
|
|
604
662
|
# Set timeout to avoid hanging
|
|
605
663
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
606
664
|
http.use_ssl = true
|
|
607
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
665
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
|
|
608
666
|
http.open_timeout = 2
|
|
609
667
|
http.read_timeout = 3
|
|
610
668
|
|
|
@@ -614,31 +672,28 @@ module Rubion
|
|
|
614
672
|
return { versions: {}, version_list: [] } unless response.is_a?(Net::HTTPSuccess)
|
|
615
673
|
|
|
616
674
|
data = JSON.parse(response.body)
|
|
617
|
-
|
|
675
|
+
|
|
618
676
|
return { versions: {}, version_list: [] } unless data['time'] && data['versions']
|
|
619
|
-
|
|
677
|
+
|
|
620
678
|
# Build hash of version => date and sorted version list
|
|
621
679
|
version_dates = {}
|
|
622
680
|
version_times = data['time'].select { |k, v| k != 'created' && k != 'modified' && data['versions'][k] }
|
|
623
|
-
|
|
681
|
+
|
|
624
682
|
# Sort versions by release date
|
|
625
683
|
sorted_versions = version_times.sort_by { |k, v| DateTime.parse(v) }.map(&:first)
|
|
626
|
-
|
|
684
|
+
|
|
627
685
|
# Build date hash
|
|
628
686
|
version_times.each do |version, date_str|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
version_dates[version] = 'N/A'
|
|
634
|
-
end
|
|
687
|
+
date = DateTime.parse(date_str)
|
|
688
|
+
version_dates[version] = date.strftime('%-m/%-d/%Y')
|
|
689
|
+
rescue StandardError
|
|
690
|
+
version_dates[version] = 'N/A'
|
|
635
691
|
end
|
|
636
|
-
|
|
692
|
+
|
|
637
693
|
{ versions: version_dates, version_list: sorted_versions }
|
|
638
|
-
rescue => e
|
|
694
|
+
rescue StandardError => e
|
|
639
695
|
puts " Debug: Error fetching versions for #{package_name}: #{e.message}" if ENV['DEBUG']
|
|
640
696
|
{ versions: {}, version_list: [] }
|
|
641
697
|
end
|
|
642
698
|
end
|
|
643
699
|
end
|
|
644
|
-
|
data/lib/rubion/version.rb
CHANGED