rubion 0.3.3 → 0.3.5

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