rubion 0.3.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.
@@ -0,0 +1,583 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require_relative 'reporter'
6
+ require 'net/http'
7
+ require 'uri'
8
+ require 'date'
9
+ require 'thread'
10
+
11
+ module Rubion
12
+ class Scanner
13
+ class ScanResult
14
+ attr_accessor :gem_vulnerabilities, :gem_versions, :package_vulnerabilities, :package_versions
15
+
16
+ def initialize
17
+ @gem_vulnerabilities = []
18
+ @gem_versions = []
19
+ @package_vulnerabilities = []
20
+ @package_versions = []
21
+ end
22
+ end
23
+
24
+ def initialize(project_path: Dir.pwd)
25
+ @project_path = project_path
26
+ @result = ScanResult.new
27
+ end
28
+
29
+ def scan
30
+ puts "🔍 Scanning project at: #{@project_path}\n\n"
31
+
32
+ scan_ruby_gems
33
+ scan_npm_packages
34
+
35
+ @result
36
+ end
37
+
38
+ def scan_incremental(options = { gems: true, packages: true })
39
+ puts "🔍 Scanning project at: #{@project_path}\n\n"
40
+
41
+ # Scan and display Ruby gems first (if enabled)
42
+ if options[:gems]
43
+ scan_ruby_gems
44
+
45
+ # Print gem results immediately
46
+ puts "\n"
47
+ reporter = Reporter.new(@result)
48
+ reporter.print_gem_vulnerabilities
49
+ reporter.print_gem_versions
50
+ end
51
+
52
+ # Then scan NPM packages (if enabled)
53
+ if options[:packages]
54
+ puts "\n" if options[:gems] # Add spacing if gems were scanned
55
+ scan_npm_packages
56
+ end
57
+
58
+ @result
59
+ end
60
+
61
+ private
62
+
63
+ def scan_ruby_gems
64
+ return unless File.exist?(File.join(@project_path, 'Gemfile.lock'))
65
+
66
+ # Check for vulnerabilities using bundler-audit
67
+ check_gem_vulnerabilities
68
+
69
+ # Check for outdated versions using bundle outdated (will show progress)
70
+ check_gem_versions
71
+ end
72
+
73
+ def scan_npm_packages
74
+ package_json = File.join(@project_path, 'package.json')
75
+ return unless File.exist?(package_json)
76
+
77
+ # Check for vulnerabilities using npm audit
78
+ check_npm_vulnerabilities
79
+
80
+ # Check for outdated versions using npm outdated (will show progress)
81
+ check_npm_versions
82
+ end
83
+
84
+ def check_gem_vulnerabilities
85
+ # Try to use bundler-audit if available
86
+ stdout, stderr, status = Open3.capture3("bundle-audit check 2>&1", chdir: @project_path)
87
+
88
+ # bundle-audit returns exit code 1 when vulnerabilities are found, 0 when none found
89
+ # Always parse if there's output (vulnerabilities found) or if it succeeded (no vulnerabilities)
90
+ if stdout.include?("vulnerabilities found") || stdout.include?("Name:") || status.success?
91
+ parse_bundler_audit_output(stdout)
92
+ else
93
+ # No vulnerabilities found or bundler-audit not available
94
+ @result.gem_vulnerabilities = []
95
+ end
96
+ rescue => e
97
+ puts " ⚠️ Could not run bundle-audit (#{e.message}). Skipping gem vulnerability check."
98
+ @result.gem_vulnerabilities = []
99
+ end
100
+
101
+ def check_gem_versions
102
+ stdout, stderr, status = Open3.capture3("bundle outdated --parseable", chdir: @project_path)
103
+
104
+ if status.success? || !stdout.empty?
105
+ parse_bundle_outdated_output(stdout)
106
+ else
107
+ # No outdated gems found
108
+ @result.gem_versions = []
109
+ end
110
+ rescue => e
111
+ puts " ⚠️ Could not run bundle outdated (#{e.message}). Skipping gem version check."
112
+ @result.gem_versions = []
113
+ end
114
+
115
+ def check_npm_vulnerabilities
116
+ stdout, stderr, status = Open3.capture3("npm audit --json 2>&1", chdir: @project_path)
117
+
118
+ begin
119
+ data = JSON.parse(stdout)
120
+ parse_npm_audit_output(data)
121
+ rescue JSON::ParserError
122
+ @result.package_vulnerabilities = []
123
+ end
124
+ rescue => e
125
+ puts " ⚠️ Could not run npm audit (#{e.message}). Skipping package vulnerability check."
126
+ @result.package_vulnerabilities = []
127
+ end
128
+
129
+ def check_npm_versions
130
+ stdout, stderr, status = Open3.capture3("npm outdated --json 2>&1", chdir: @project_path)
131
+
132
+ begin
133
+ data = JSON.parse(stdout) unless stdout.empty?
134
+ parse_npm_outdated_output(data || {})
135
+ rescue JSON::ParserError
136
+ @result.package_versions = []
137
+ end
138
+ rescue => e
139
+ puts " ⚠️ Could not run npm outdated (#{e.message}). Skipping package version check."
140
+ @result.package_versions = []
141
+ end
142
+
143
+ # Parsers
144
+
145
+ def parse_bundler_audit_output(output)
146
+ vulnerabilities = []
147
+ current_gem = nil
148
+
149
+ output.each_line do |line|
150
+ line = line.strip
151
+ next if line.empty?
152
+
153
+ if line =~ /^Name: (.+)/
154
+ current_gem = { gem: $1.strip }
155
+ elsif line =~ /^Version: (.+)/ && current_gem
156
+ current_gem[:version] = $1.strip
157
+ elsif line =~ /^CVE: (.+)/ && current_gem
158
+ current_gem[:advisory] = $1.strip
159
+ elsif line =~ /^Advisory: (.+)/ && current_gem
160
+ # Fallback for older bundle-audit versions
161
+ current_gem[:advisory] = $1.strip
162
+ elsif line =~ /^Criticality: (.+)/ && current_gem
163
+ current_gem[:severity] = $1.strip
164
+ elsif line =~ /^Title: (.+)/ && current_gem
165
+ current_gem[:title] = $1.strip
166
+ # Only add if we have at least name, version, and title
167
+ if current_gem[:gem] && current_gem[:version] && current_gem[:title]
168
+ vulnerabilities << current_gem
169
+ end
170
+ current_gem = nil
171
+ end
172
+ end
173
+
174
+ # Handle case where vulnerability block ends without Title (use CVE as title)
175
+ if current_gem && current_gem[:gem] && current_gem[:version]
176
+ current_gem[:title] ||= current_gem[:advisory] || "Vulnerability detected"
177
+ vulnerabilities << current_gem
178
+ end
179
+
180
+ @result.gem_vulnerabilities = vulnerabilities
181
+ end
182
+
183
+ def parse_bundle_outdated_output(output)
184
+ versions = []
185
+ lines_to_process = []
186
+
187
+ # First pass: collect all lines to process
188
+ output.each_line do |line|
189
+ next if line.strip.empty?
190
+
191
+ # Parse format: gem_name (newest version, installed version, requested version)
192
+ if line =~ /^(.+?)\s+\(newest\s+(.+?),\s+installed\s+(.+?)(?:,|\))/
193
+ lines_to_process << {
194
+ gem_name: $1.strip,
195
+ current_version: $3.strip,
196
+ latest_version: $2.strip
197
+ }
198
+ end
199
+ end
200
+
201
+ total = lines_to_process.size
202
+
203
+ # Process in parallel with threads (limit to 10 concurrent requests)
204
+ mutex = Mutex.new
205
+ thread_pool = []
206
+ max_threads = 10
207
+
208
+ lines_to_process.each_with_index do |line_data, index|
209
+ # Wait if we have too many threads
210
+ if thread_pool.size >= max_threads
211
+ thread_pool.shift.join
212
+ end
213
+
214
+ thread = Thread.new do
215
+ # Fetch all version info once per gem (includes dates and version list)
216
+ gem_data = fetch_gem_all_versions(line_data[:gem_name])
217
+
218
+ # Extract dates for current and latest versions
219
+ current_date = gem_data[:versions][line_data[:current_version]] || 'N/A'
220
+ latest_date = gem_data[:versions][line_data[:latest_version]] || 'N/A'
221
+
222
+ # Calculate time difference
223
+ time_diff = calculate_time_difference(current_date, latest_date)
224
+
225
+ # Count versions between current and latest
226
+ version_count = count_versions_from_list(gem_data[:version_list], line_data[:current_version], line_data[:latest_version])
227
+
228
+ result = {
229
+ gem: line_data[:gem_name],
230
+ current: line_data[:current_version],
231
+ current_date: current_date,
232
+ latest: line_data[:latest_version],
233
+ latest_date: latest_date,
234
+ time_diff: time_diff,
235
+ version_count: version_count,
236
+ index: index
237
+ }
238
+
239
+ mutex.synchronize do
240
+ versions << result
241
+ print "\r📦 Checking Ruby gems... #{versions.size}/#{total}"
242
+ $stdout.flush
243
+ end
244
+ end
245
+
246
+ thread_pool << thread
247
+ end
248
+
249
+ # Wait for all threads to complete
250
+ thread_pool.each(&:join)
251
+
252
+ # Sort by original index to maintain order
253
+ versions.sort_by! { |v| v[:index] }
254
+ versions.each { |v| v.delete(:index) }
255
+
256
+ puts "\r📦 Checking Ruby gems... #{total}/#{total} ✓" if total > 0
257
+
258
+ @result.gem_versions = versions
259
+ end
260
+
261
+ def parse_npm_audit_output(data)
262
+ vulnerabilities = []
263
+
264
+ if data['vulnerabilities'] && data['vulnerabilities'].is_a?(Hash)
265
+ data['vulnerabilities'].each do |name, info|
266
+ next unless info.is_a?(Hash)
267
+
268
+ # Extract title from via array
269
+ title = 'Vulnerability detected'
270
+ if info['via'].is_a?(Array) && info['via'].first.is_a?(Hash)
271
+ title = info['via'].first['title'] || title
272
+ elsif info['via'].is_a?(String)
273
+ title = info['via']
274
+ end
275
+
276
+ vulnerabilities << {
277
+ package: name,
278
+ version: info['range'] || info['version'] || 'unknown',
279
+ severity: info['severity'] || 'unknown',
280
+ title: title
281
+ }
282
+ end
283
+ end
284
+
285
+ @result.package_vulnerabilities = vulnerabilities
286
+ rescue => e
287
+ puts " ⚠️ Error parsing npm audit data: #{e.message}"
288
+ @result.package_vulnerabilities = []
289
+ end
290
+
291
+ def parse_npm_outdated_output(data)
292
+ versions = []
293
+
294
+ if data.is_a?(Hash)
295
+ packages_to_process = []
296
+
297
+ # First pass: collect all packages to process
298
+ data.each do |name, info|
299
+ next unless info.is_a?(Hash)
300
+
301
+ packages_to_process << {
302
+ name: name,
303
+ current_version: info['current'] || 'unknown',
304
+ latest_version: info['latest'] || 'unknown'
305
+ }
306
+ end
307
+
308
+ total = packages_to_process.size
309
+
310
+ # Process in parallel with threads (limit to 10 concurrent requests)
311
+ mutex = Mutex.new
312
+ thread_pool = []
313
+ max_threads = 10
314
+
315
+ packages_to_process.each_with_index do |pkg_data, index|
316
+ # Wait if we have too many threads
317
+ if thread_pool.size >= max_threads
318
+ thread_pool.shift.join
319
+ end
320
+
321
+ thread = Thread.new do
322
+ # Fetch all version info once per package (includes dates and version list)
323
+ pkg_data_full = fetch_npm_all_versions(pkg_data[:name])
324
+
325
+ # Extract dates for current and latest versions
326
+ current_date = pkg_data_full[:versions][pkg_data[:current_version]] || 'N/A'
327
+ latest_date = pkg_data_full[:versions][pkg_data[:latest_version]] || 'N/A'
328
+
329
+ # Calculate time difference
330
+ time_diff = calculate_time_difference(current_date, latest_date)
331
+
332
+ # Count versions between current and latest
333
+ version_count = count_versions_from_list(pkg_data_full[:version_list], pkg_data[:current_version], pkg_data[:latest_version])
334
+
335
+ result = {
336
+ package: pkg_data[:name],
337
+ current: pkg_data[:current_version],
338
+ current_date: current_date,
339
+ latest: pkg_data[:latest_version],
340
+ latest_date: latest_date,
341
+ time_diff: time_diff,
342
+ version_count: version_count,
343
+ index: index
344
+ }
345
+
346
+ mutex.synchronize do
347
+ versions << result
348
+ print "\r📦 Checking NPM packages... #{versions.size}/#{total}"
349
+ $stdout.flush
350
+ end
351
+ end
352
+
353
+ thread_pool << thread
354
+ end
355
+
356
+ # Wait for all threads to complete
357
+ thread_pool.each(&:join)
358
+
359
+ # Sort by original index to maintain order
360
+ versions.sort_by! { |v| v[:index] }
361
+ versions.each { |v| v.delete(:index) }
362
+
363
+ puts "\r📦 Checking NPM packages... #{total}/#{total} ✓" if total > 0
364
+ end
365
+
366
+ @result.package_versions = versions
367
+ rescue => e
368
+ puts " ⚠️ Error parsing npm outdated data: #{e.message}"
369
+ @result.package_versions = []
370
+ end
371
+
372
+ # Dummy data for demonstration (commented out - only show real data)
373
+ # Uncomment these methods if you need dummy data for testing
374
+
375
+ # def dummy_gem_vulnerabilities
376
+ # [
377
+ # {
378
+ # gem: 'rails',
379
+ # version: '7.2.2.2',
380
+ # severity: 'Critical',
381
+ # advisory: 'CVE-2024-12345',
382
+ # title: 'ActiveRecord attack'
383
+ # },
384
+ # {
385
+ # gem: 'rack',
386
+ # version: '3.1.18',
387
+ # severity: 'High',
388
+ # advisory: 'CVE-2024-54321',
389
+ # title: 'Man in middle attack'
390
+ # },
391
+ # {
392
+ # gem: 'nokogiri',
393
+ # version: '1.10.4',
394
+ # severity: 'Medium',
395
+ # advisory: 'CVE-2021-30560',
396
+ # title: 'Update bundled libxml2 to v2.9.12'
397
+ # }
398
+ # ]
399
+ # end
400
+
401
+ # def dummy_gem_versions
402
+ # [
403
+ # { gem: 'sidekiq', current: '7.3.0', current_date: '3/5/2024', latest: '8.1.0', latest_date: '11/11/2024' },
404
+ # { gem: 'fastimage', current: '2.2.7', current_date: '2/2/2025', latest: '2.3.2', latest_date: '9/9/2025' },
405
+ # { gem: 'puma', current: '4.3.8', current_date: '1/15/2024', latest: '6.4.0', latest_date: '10/20/2024' },
406
+ # { gem: 'devise', current: '4.7.3', current_date: '3/10/2024', latest: '4.9.3', latest_date: '8/15/2024' },
407
+ # { gem: 'rspec-rails', current: '4.0.2', current_date: '2/5/2024', latest: '6.1.0', latest_date: '12/1/2024' }
408
+ # ]
409
+ # end
410
+
411
+ # def dummy_npm_vulnerabilities
412
+ # [
413
+ # {
414
+ # package: 'moment',
415
+ # version: '1.2.3',
416
+ # severity: 'high',
417
+ # title: 'Wrong timezone date'
418
+ # },
419
+ # {
420
+ # package: 'axios',
421
+ # version: '0.21.1',
422
+ # severity: 'high',
423
+ # title: 'Server-Side Request Forgery in axios'
424
+ # },
425
+ # {
426
+ # package: 'minimist',
427
+ # version: '1.2.5',
428
+ # severity: 'critical',
429
+ # title: 'Prototype Pollution in minimist'
430
+ # }
431
+ # ]
432
+ # end
433
+
434
+ # def dummy_npm_versions
435
+ # [
436
+ # { package: 'jquery', current: '3.7.1', current_date: '4/5/2024', latest: '3.9.1', latest_date: '10/11/2025' },
437
+ # { package: 'vue', current: '2.6.12', current_date: '5/15/2024', latest: '3.4.3', latest_date: '11/20/2024' },
438
+ # { package: 'webpack', current: '4.46.0', current_date: '3/8/2024', latest: '5.89.0', latest_date: '9/25/2024' },
439
+ # { package: 'eslint', current: '7.32.0', current_date: '2/12/2024', latest: '8.56.0', latest_date: '12/5/2024' },
440
+ # { package: '@babel/core', current: '7.15.0', current_date: '4/20/2024', latest: '7.23.6', latest_date: '10/30/2024' }
441
+ # ]
442
+ # end
443
+
444
+ # Fetch all gem version info (dates and version list) from RubyGems API in one call
445
+ def fetch_gem_all_versions(gem_name)
446
+ return { versions: {}, version_list: [] } if gem_name.nil?
447
+
448
+ uri = URI("https://rubygems.org/api/v1/versions/#{gem_name}.json")
449
+
450
+ # Set timeout to avoid hanging
451
+ http = Net::HTTP.new(uri.host, uri.port)
452
+ http.use_ssl = true
453
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
454
+ http.open_timeout = 2
455
+ http.read_timeout = 3
456
+
457
+ request = Net::HTTP::Get.new(uri.request_uri)
458
+ response = http.request(request)
459
+
460
+ return { versions: {}, version_list: [] } unless response.is_a?(Net::HTTPSuccess)
461
+
462
+ all_versions = JSON.parse(response.body)
463
+
464
+ # Build hash of version => date
465
+ version_dates = {}
466
+ version_list = []
467
+
468
+ all_versions.each do |v|
469
+ version_num = v['number']
470
+ if v['created_at']
471
+ date = DateTime.parse(v['created_at'])
472
+ version_dates[version_num] = date.strftime('%-m/%-d/%Y')
473
+ else
474
+ version_dates[version_num] = 'N/A'
475
+ end
476
+ version_list << version_num
477
+ end
478
+
479
+ { versions: version_dates, version_list: version_list }
480
+ rescue => e
481
+ puts " Debug: Error fetching versions for #{gem_name}: #{e.message}" if ENV['DEBUG']
482
+ { versions: {}, version_list: [] }
483
+ end
484
+
485
+ # Count versions between current and latest from a version list
486
+ def count_versions_from_list(version_list, current_version, latest_version)
487
+ return 'N/A' if current_version == 'unknown' || latest_version == 'unknown' || version_list.empty?
488
+ return 0 if current_version == latest_version
489
+
490
+ # Find indices of current and latest versions
491
+ current_idx = version_list.index(current_version)
492
+ latest_idx = version_list.index(latest_version)
493
+
494
+ return 'N/A' if current_idx.nil? || latest_idx.nil?
495
+
496
+ # Count versions between (exclusive of current, inclusive of latest)
497
+ (latest_idx - current_idx).abs
498
+ end
499
+
500
+ # Calculate time difference between two dates
501
+ def calculate_time_difference(date1_str, date2_str)
502
+ return 'N/A' if date1_str == 'N/A' || date2_str == 'N/A'
503
+
504
+ begin
505
+ # Parse dates - convert "8/13/2025" format to Date object
506
+ # Split by / and create Date object
507
+ parts1 = date1_str.split('/').map(&:to_i)
508
+ parts2 = date2_str.split('/').map(&:to_i)
509
+
510
+ date1 = Date.new(parts1[2], parts1[0], parts1[1])
511
+ date2 = Date.new(parts2[2], parts2[0], parts2[1])
512
+
513
+ # Always use the later date minus the earlier date
514
+ diff = (date2 - date1).to_i.abs
515
+
516
+ if diff < 30
517
+ "#{diff} day#{diff != 1 ? 's' : ''}"
518
+ elsif diff < 365
519
+ months = (diff / 30.0).round
520
+ "#{months} month#{months != 1 ? 's' : ''}"
521
+ else
522
+ years = (diff / 365.0).round(1)
523
+ if years == years.to_i
524
+ "#{years.to_i} year#{years.to_i != 1 ? 's' : ''}"
525
+ else
526
+ "#{years} years"
527
+ end
528
+ end
529
+ rescue => e
530
+ puts " Debug: Error calculating time diff: #{e.message}" if ENV['DEBUG']
531
+ 'N/A'
532
+ end
533
+ end
534
+
535
+ # Fetch all NPM package version info (dates and version list) from NPM registry in one call
536
+ def fetch_npm_all_versions(package_name)
537
+ return { versions: {}, version_list: [] } if package_name.nil?
538
+
539
+ # Encode package name for URL (handles scoped packages like @babel/core)
540
+ encoded_name = URI.encode_www_form_component(package_name)
541
+ uri = URI("https://registry.npmjs.org/#{encoded_name}")
542
+
543
+ # Set timeout to avoid hanging
544
+ http = Net::HTTP.new(uri.host, uri.port)
545
+ http.use_ssl = true
546
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # Skip SSL verification
547
+ http.open_timeout = 2
548
+ http.read_timeout = 3
549
+
550
+ request = Net::HTTP::Get.new(uri.request_uri)
551
+ response = http.request(request)
552
+
553
+ return { versions: {}, version_list: [] } unless response.is_a?(Net::HTTPSuccess)
554
+
555
+ data = JSON.parse(response.body)
556
+
557
+ return { versions: {}, version_list: [] } unless data['time'] && data['versions']
558
+
559
+ # Build hash of version => date and sorted version list
560
+ version_dates = {}
561
+ version_times = data['time'].select { |k, v| k != 'created' && k != 'modified' && data['versions'][k] }
562
+
563
+ # Sort versions by release date
564
+ sorted_versions = version_times.sort_by { |k, v| DateTime.parse(v) }.map(&:first)
565
+
566
+ # Build date hash
567
+ version_times.each do |version, date_str|
568
+ begin
569
+ date = DateTime.parse(date_str)
570
+ version_dates[version] = date.strftime('%-m/%-d/%Y')
571
+ rescue
572
+ version_dates[version] = 'N/A'
573
+ end
574
+ end
575
+
576
+ { versions: version_dates, version_list: sorted_versions }
577
+ rescue => e
578
+ puts " Debug: Error fetching versions for #{package_name}: #{e.message}" if ENV['DEBUG']
579
+ { versions: {}, version_list: [] }
580
+ end
581
+ end
582
+ end
583
+
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubion
4
+ VERSION = "0.3.0"
5
+ end
6
+
data/lib/rubion.rb ADDED
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubion/version"
4
+ require_relative "rubion/scanner"
5
+ require_relative "rubion/reporter"
6
+
7
+ module Rubion
8
+ class Error < StandardError; end
9
+
10
+ class CLI
11
+ def self.start(args)
12
+ command = args[0]
13
+
14
+ case command
15
+ when 'scan'
16
+ # Parse options
17
+ options = parse_scan_options(args[1..-1])
18
+ scan(options)
19
+ when 'version', '-v', '--version'
20
+ puts "Rubion version #{VERSION}"
21
+ when 'help', '-h', '--help', nil
22
+ print_help
23
+ else
24
+ puts "Unknown command: #{command}"
25
+ print_help
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ def self.parse_scan_options(args)
31
+ options = { gems: true, packages: true }
32
+
33
+ # Check for --gems-only or --packages-only flags
34
+ if args.include?('--gems-only') || args.include?('-g')
35
+ options[:gems] = true
36
+ options[:packages] = false
37
+ elsif args.include?('--packages-only') || args.include?('-p')
38
+ options[:gems] = false
39
+ options[:packages] = true
40
+ elsif args.include?('--gems') || args.include?('--packages')
41
+ # Legacy support for --gems and --packages
42
+ options[:gems] = args.include?('--gems')
43
+ options[:packages] = args.include?('--packages')
44
+ end
45
+
46
+ options
47
+ end
48
+
49
+ def self.scan(options = { gems: true, packages: true })
50
+ project_path = Dir.pwd
51
+
52
+ scanner = Scanner.new(project_path: project_path)
53
+ result = scanner.scan_incremental(options)
54
+
55
+ # Results are already printed incrementally based on options
56
+ reporter = Reporter.new(result)
57
+
58
+ # Only print package results if packages were scanned
59
+ if options[:packages]
60
+ reporter.print_package_vulnerabilities
61
+ reporter.print_package_versions
62
+ end
63
+ end
64
+
65
+ def self.print_help
66
+ puts <<~HELP
67
+
68
+ 🔒 Rubion - Security & Version Scanner for Ruby and JavaScript projects
69
+
70
+ USAGE:
71
+ rubion scan [OPTIONS] Scan current project for vulnerabilities and outdated versions
72
+ rubion version Display Rubion version
73
+ rubion help Display this help message
74
+
75
+ SCAN OPTIONS:
76
+ --gems, --gem, -g Scan only Ruby gems (skip NPM packages)
77
+ --packages, --npm, -p Scan only NPM packages (skip Ruby gems)
78
+ --all, -a Scan both gems and packages (default)
79
+
80
+ DESCRIPTION:
81
+ Rubion scans your project for:
82
+ - Ruby gem vulnerabilities (using bundler-audit)
83
+ - Outdated Ruby gems (using bundle outdated)
84
+ - NPM package vulnerabilities (using npm audit)
85
+ - Outdated NPM packages (using npm outdated)
86
+
87
+ OUTPUT:
88
+ Results are displayed in organized tables with:
89
+ 📛 Vulnerabilities with severity icons (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
90
+ 📦 Version information with release dates
91
+ ⏱️ Time difference ("Behind By" column)
92
+ 🔢 Version count between current and latest
93
+
94
+ EXAMPLES:
95
+ # Scan both gems and packages (default)
96
+ rubion scan
97
+
98
+ # Scan only Ruby gems
99
+ rubion scan --gems
100
+
101
+ # Scan only NPM packages
102
+ rubion scan --packages
103
+
104
+ # Get help
105
+ rubion help
106
+
107
+ REQUIREMENTS:
108
+ - Ruby 2.6+
109
+ - Bundler (for gem scanning)
110
+ - NPM (for package scanning, optional)
111
+ - bundler-audit (optional, install with: gem install bundler-audit)
112
+
113
+ HELP
114
+ end
115
+ end
116
+ end
117
+