gem_guard 0.1.6 → 0.1.10

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.
data/lib/gem_guard/cli.rb CHANGED
@@ -1,25 +1,65 @@
1
1
  require "thor"
2
+ require "stringio"
2
3
 
3
4
  module GemGuard
4
5
  class CLI < Thor
6
+ # Exit codes for CI/CD integration
7
+ EXIT_SUCCESS = 0
8
+ EXIT_VULNERABILITIES_FOUND = 1
9
+ EXIT_ERROR = 2
10
+
5
11
  desc "scan", "Scan dependencies for known vulnerabilities"
6
- option :format, type: :string, default: "table", desc: "Output format (table, json)"
7
- option :lockfile, type: :string, default: "Gemfile.lock", desc: "Path to Gemfile.lock"
12
+ option :format, type: :string, desc: "Output format (table, json)"
13
+ option :lockfile, type: :string, desc: "Path to Gemfile.lock"
14
+ option :config, type: :string, default: ".gemguard.yml", desc: "Path to config file"
15
+ option :fail_on_vulnerabilities, type: :boolean, desc: "Exit with code 1 if vulnerabilities found"
16
+ option :severity_threshold, type: :string, desc: "Minimum severity level (low, medium, high, critical)"
17
+ option :output, type: :string, desc: "Output file path"
8
18
  def scan
9
- lockfile_path = options[:lockfile]
19
+ config = Config.new(options[:config])
20
+
21
+ # Override config with CLI options
22
+ lockfile_path = options[:lockfile] || config.lockfile_path
23
+ format = options[:format] || config.output_format
24
+ fail_on_vulns = options[:fail_on_vulnerabilities].nil? ? config.fail_on_vulnerabilities? : options[:fail_on_vulnerabilities]
25
+ severity_threshold = options[:severity_threshold] || config.severity_threshold
26
+ output_file = options[:output] || config.output_file
10
27
 
11
28
  unless File.exist?(lockfile_path)
12
29
  puts "Error: #{lockfile_path} not found"
13
- exit 1
30
+ exit EXIT_ERROR
14
31
  end
15
32
 
16
- dependencies = Parser.new.parse(lockfile_path)
17
- vulnerabilities = VulnerabilityFetcher.new.fetch_for(dependencies)
18
- analysis = Analyzer.new.analyze(dependencies, vulnerabilities)
33
+ begin
34
+ dependencies = Parser.new.parse(lockfile_path)
35
+ vulnerabilities = VulnerabilityFetcher.new.fetch_for(dependencies)
36
+
37
+ # Filter vulnerabilities based on config
38
+ filtered_vulnerabilities = filter_vulnerabilities(vulnerabilities, config)
39
+
40
+ analysis = Analyzer.new.analyze(dependencies, filtered_vulnerabilities)
19
41
 
20
- Reporter.new.report(analysis, format: options[:format])
42
+ # Filter analysis based on severity threshold
43
+ filtered_analysis = filter_analysis_by_severity(analysis, severity_threshold, config)
21
44
 
22
- exit 1 if analysis.has_vulnerabilities?
45
+ if output_file
46
+ output_content = capture_report_output(filtered_analysis, format)
47
+ File.write(output_file, output_content)
48
+ puts "Report written to #{output_file}"
49
+ else
50
+ Reporter.new.report(filtered_analysis, format: format)
51
+ end
52
+
53
+ # Exit with appropriate code for CI/CD
54
+ if filtered_analysis.has_vulnerabilities? && fail_on_vulns
55
+ exit EXIT_VULNERABILITIES_FOUND
56
+ else
57
+ exit EXIT_SUCCESS
58
+ end
59
+ rescue => e
60
+ puts "Error: #{e.message}"
61
+ exit EXIT_ERROR
62
+ end
23
63
  end
24
64
 
25
65
  desc "sbom", "Generate Software Bill of Materials (SBOM)"
@@ -58,9 +98,210 @@ module GemGuard
58
98
  end
59
99
  end
60
100
 
101
+ desc "typosquat", "Check for potential typosquat dependencies"
102
+ option :lockfile, type: :string, default: "Gemfile.lock", desc: "Path to Gemfile.lock"
103
+ option :format, type: :string, default: "table", desc: "Output format (table, json)"
104
+ option :output, type: :string, desc: "Output file path"
105
+ option :config, type: :string, desc: "Config file path"
106
+ def typosquat
107
+ config = Config.new(options[:config] || ".gemguard.yml")
108
+
109
+ lockfile_path = options[:lockfile] || config.lockfile_path
110
+ format = options[:format] || config.output_format
111
+ output_file = options[:output] || config.output_file
112
+
113
+ unless File.exist?(lockfile_path)
114
+ puts "Error: #{lockfile_path} not found"
115
+ exit EXIT_ERROR
116
+ end
117
+
118
+ begin
119
+ dependencies = Parser.new.parse(lockfile_path)
120
+ checker = TyposquatChecker.new
121
+ suspicious_gems = checker.check_dependencies(dependencies)
122
+
123
+ if output_file
124
+ output_content = format_typosquat_output(suspicious_gems, format)
125
+ File.write(output_file, output_content)
126
+ puts "Typosquat report written to #{output_file}"
127
+ else
128
+ display_typosquat_results(suspicious_gems, format)
129
+ end
130
+
131
+ # Exit with appropriate code
132
+ if suspicious_gems.any? { |sg| sg[:risk_level] == "critical" || sg[:risk_level] == "high" }
133
+ exit EXIT_VULNERABILITIES_FOUND
134
+ else
135
+ exit EXIT_SUCCESS
136
+ end
137
+ rescue => e
138
+ puts "Error: #{e.message}"
139
+ exit EXIT_ERROR
140
+ end
141
+ end
142
+
143
+ desc "config", "Manage configuration"
144
+ option :init, type: :boolean, desc: "Initialize default config file"
145
+ option :show, type: :boolean, desc: "Show current configuration"
146
+ option :path, type: :string, default: ".gemguard.yml", desc: "Config file path"
147
+ def config
148
+ config_file = Config.new(options[:path])
149
+
150
+ if options[:init]
151
+ if File.exist?(options[:path])
152
+ puts "Config file #{options[:path]} already exists"
153
+ exit EXIT_ERROR
154
+ end
155
+
156
+ # Create default config file
157
+ default_config = {
158
+ "lockfile" => "Gemfile.lock",
159
+ "format" => "table",
160
+ "fail_on_vulnerabilities" => true,
161
+ "severity_threshold" => "low",
162
+ "ignore_vulnerabilities" => [],
163
+ "ignore_gems" => [],
164
+ "output_file" => nil,
165
+ "project_name" => config_file.send(:detect_project_name),
166
+ "sbom" => {
167
+ "format" => "spdx",
168
+ "include_dev_dependencies" => false
169
+ },
170
+ "scan" => {
171
+ "sources" => ["osv", "ruby_advisory_db"],
172
+ "timeout" => 30
173
+ }
174
+ }
175
+
176
+ File.write(options[:path], YAML.dump(default_config))
177
+ puts "Created #{options[:path]} with default configuration"
178
+ elsif options[:show]
179
+ if config_file.exists?
180
+ puts File.read(options[:path])
181
+ else
182
+ puts "No config file found at #{options[:path]}"
183
+ puts "Run 'gem_guard config --init' to create one"
184
+ end
185
+ else
186
+ puts "Usage: gem_guard config [--init|--show] [--path PATH]"
187
+ puts " --init Create a new .gemguard.yml config file"
188
+ puts " --show Display current configuration"
189
+ puts " --path Specify config file path (default: .gemguard.yml)"
190
+ end
191
+ end
192
+
61
193
  desc "version", "Show gem_guard version"
62
194
  def version
63
195
  puts GemGuard::VERSION
64
196
  end
197
+
198
+ private
199
+
200
+ def filter_vulnerabilities(vulnerabilities, config)
201
+ vulnerabilities.reject do |vuln|
202
+ config.should_ignore_vulnerability?(vuln.id)
203
+ end
204
+ end
205
+
206
+ def filter_analysis_by_severity(analysis, severity_threshold, config)
207
+ return analysis unless severity_threshold
208
+
209
+ filtered_vulnerable_deps = analysis.vulnerable_dependencies.select do |vuln_dep|
210
+ config.meets_severity_threshold?(extract_severity_level(vuln_dep.vulnerability.severity))
211
+ end
212
+
213
+ # Create new analysis with filtered vulnerabilities
214
+ GemGuard::Analysis.new(filtered_vulnerable_deps)
215
+ end
216
+
217
+ def extract_severity_level(severity_string)
218
+ return "unknown" if severity_string.nil? || severity_string.empty?
219
+
220
+ # Extract severity from CVSS string or direct severity
221
+ case severity_string.downcase
222
+ when /critical/
223
+ "critical"
224
+ when /high/
225
+ "high"
226
+ when /medium/
227
+ "medium"
228
+ when /low/
229
+ "low"
230
+ else
231
+ "unknown"
232
+ end
233
+ end
234
+
235
+ def capture_report_output(analysis, format)
236
+ output = StringIO.new
237
+ old_stdout = $stdout
238
+ $stdout = output
239
+
240
+ Reporter.new.report(analysis, format: format)
241
+
242
+ $stdout = old_stdout
243
+ output.string
244
+ end
245
+
246
+ def display_typosquat_results(suspicious_gems, format)
247
+ if suspicious_gems.empty?
248
+ puts "No potential typosquat dependencies found."
249
+ return
250
+ end
251
+
252
+ case format.downcase
253
+ when "json"
254
+ puts JSON.pretty_generate(suspicious_gems)
255
+ else
256
+ display_typosquat_table(suspicious_gems)
257
+ end
258
+ end
259
+
260
+ def display_typosquat_table(suspicious_gems)
261
+ puts "\n🚨 Potential Typosquat Dependencies Found:"
262
+ puts "=" * 80
263
+
264
+ suspicious_gems.each do |gem_info|
265
+ risk_emoji = case gem_info[:risk_level]
266
+ when "critical" then "🔴"
267
+ when "high" then "🟠"
268
+ when "medium" then "🟡"
269
+ else "🟢"
270
+ end
271
+
272
+ puts "\n#{risk_emoji} #{gem_info[:gem_name]} (#{gem_info[:version]})"
273
+ puts " Suspected target: #{gem_info[:suspected_target]}"
274
+ puts " Similarity: #{(gem_info[:similarity_score] * 100).round(1)}%"
275
+ puts " Risk level: #{gem_info[:risk_level].upcase}"
276
+ puts " Target downloads: #{number_with_commas(gem_info[:target_downloads])}"
277
+ end
278
+
279
+ puts "\n" + "=" * 80
280
+ puts "💡 Review these dependencies carefully. Consider:"
281
+ puts " • Verifying the gem's legitimacy on rubygems.org"
282
+ puts " • Checking the gem's source code repository"
283
+ puts " • Looking for official documentation or endorsements"
284
+ puts " • Comparing with the suspected target gem"
285
+ end
286
+
287
+ def format_typosquat_output(suspicious_gems, format)
288
+ case format.downcase
289
+ when "json"
290
+ JSON.pretty_generate(suspicious_gems)
291
+ else
292
+ output = StringIO.new
293
+ old_stdout = $stdout
294
+ $stdout = output
295
+
296
+ display_typosquat_table(suspicious_gems)
297
+
298
+ $stdout = old_stdout
299
+ output.string
300
+ end
301
+ end
302
+
303
+ def number_with_commas(number)
304
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
305
+ end
65
306
  end
66
307
  end
@@ -0,0 +1,193 @@
1
+ require "yaml"
2
+
3
+ module GemGuard
4
+ class Config
5
+ DEFAULT_CONFIG = {
6
+ "lockfile" => "Gemfile.lock",
7
+ "format" => "table",
8
+ "fail_on_vulnerabilities" => true,
9
+ "severity_threshold" => "low",
10
+ "ignore_vulnerabilities" => [],
11
+ "ignore_gems" => [],
12
+ "output_file" => nil,
13
+ "project_name" => nil,
14
+ "sbom" => {
15
+ "format" => "spdx",
16
+ "include_dev_dependencies" => false
17
+ },
18
+ "scan" => {
19
+ "sources" => ["osv", "ruby_advisory_db"],
20
+ "timeout" => 30
21
+ }
22
+ }.freeze
23
+
24
+ SEVERITY_LEVELS = %w[low medium high critical].freeze
25
+
26
+ def initialize(config_path = ".gemguard.yml")
27
+ @config_path = config_path
28
+ @config = load_config
29
+ end
30
+
31
+ def get(key)
32
+ keys = key.split(".")
33
+ value = @config
34
+
35
+ keys.each do |k|
36
+ value = value[k] if value.is_a?(Hash)
37
+ end
38
+
39
+ value
40
+ end
41
+
42
+ def set(key, value)
43
+ keys = key.split(".")
44
+ target = @config
45
+
46
+ keys[0..-2].each do |k|
47
+ target[k] ||= {}
48
+ target = target[k]
49
+ end
50
+
51
+ target[keys.last] = value
52
+ end
53
+
54
+ def save
55
+ File.write(@config_path, YAML.dump(@config))
56
+ end
57
+
58
+ def exists?
59
+ File.exist?(@config_path)
60
+ end
61
+
62
+ def lockfile_path
63
+ get("lockfile")
64
+ end
65
+
66
+ def output_format
67
+ get("format")
68
+ end
69
+
70
+ def fail_on_vulnerabilities?
71
+ get("fail_on_vulnerabilities")
72
+ end
73
+
74
+ def severity_threshold
75
+ get("severity_threshold")
76
+ end
77
+
78
+ def ignored_vulnerabilities
79
+ get("ignore_vulnerabilities") || []
80
+ end
81
+
82
+ def ignored_gems
83
+ get("ignore_gems") || []
84
+ end
85
+
86
+ def output_file
87
+ get("output_file")
88
+ end
89
+
90
+ def project_name
91
+ get("project_name") || detect_project_name
92
+ end
93
+
94
+ def sbom_format
95
+ get("sbom.format")
96
+ end
97
+
98
+ def include_dev_dependencies?
99
+ get("sbom.include_dev_dependencies")
100
+ end
101
+
102
+ def vulnerability_sources
103
+ get("scan.sources")
104
+ end
105
+
106
+ def scan_timeout
107
+ get("scan.timeout")
108
+ end
109
+
110
+ def should_ignore_vulnerability?(vulnerability_id)
111
+ ignored_vulnerabilities.include?(vulnerability_id)
112
+ end
113
+
114
+ def should_ignore_gem?(gem_name)
115
+ ignored_gems.include?(gem_name)
116
+ end
117
+
118
+ def meets_severity_threshold?(severity)
119
+ return true if severity.nil? || severity.empty?
120
+
121
+ severity_index = SEVERITY_LEVELS.index(severity.downcase)
122
+ threshold_index = SEVERITY_LEVELS.index(severity_threshold.downcase)
123
+
124
+ return true if severity_index.nil? || threshold_index.nil?
125
+
126
+ severity_index >= threshold_index
127
+ end
128
+
129
+ private
130
+
131
+ def load_config
132
+ if File.exist?(@config_path)
133
+ user_config = YAML.load_file(@config_path) || {}
134
+ deep_merge(deep_dup(DEFAULT_CONFIG), user_config)
135
+ else
136
+ deep_dup(DEFAULT_CONFIG)
137
+ end
138
+ rescue Psych::SyntaxError => e
139
+ puts "Warning: Invalid YAML in #{@config_path}: #{e.message}"
140
+ puts "Using default configuration."
141
+ deep_dup(DEFAULT_CONFIG)
142
+ end
143
+
144
+ def deep_dup(obj)
145
+ case obj
146
+ when Hash
147
+ obj.each_with_object({}) { |(key, value), hash| hash[key] = deep_dup(value) }
148
+ when Array
149
+ obj.map { |item| deep_dup(item) }
150
+ else
151
+ begin
152
+ obj.dup
153
+ rescue
154
+ obj
155
+ end
156
+ end
157
+ end
158
+
159
+ def deep_merge(hash1, hash2)
160
+ result = hash1.dup
161
+
162
+ hash2.each do |key, value|
163
+ result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
164
+ deep_merge(result[key], value)
165
+ else
166
+ value
167
+ end
168
+ end
169
+
170
+ result
171
+ end
172
+
173
+ def detect_project_name
174
+ # Try to detect project name from various sources
175
+ if File.exist?("Gemfile")
176
+ gemfile_content = File.read("Gemfile")
177
+ if gemfile_content =~ /gem\s+['"]([^'"]+)['"]/
178
+ return $1
179
+ end
180
+ end
181
+
182
+ if File.exist?("*.gemspec")
183
+ gemspec_files = Dir.glob("*.gemspec")
184
+ unless gemspec_files.empty?
185
+ return File.basename(gemspec_files.first, ".gemspec")
186
+ end
187
+ end
188
+
189
+ # Fallback to directory name
190
+ File.basename(Dir.pwd)
191
+ end
192
+ end
193
+ end
@@ -16,7 +16,9 @@ module GemGuard
16
16
  )
17
17
  end
18
18
 
19
- dependencies
19
+ # Deduplicate dependencies by name to handle platform-specific gems
20
+ # (e.g., nokogiri-arm64-darwin, nokogiri-x86_64-darwin, etc.)
21
+ dependencies.uniq { |dep| dep.name }
20
22
  end
21
23
 
22
24
  private
@@ -0,0 +1,157 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "uri"
4
+
5
+ module GemGuard
6
+ class TyposquatChecker
7
+ POPULAR_GEMS_CACHE_TTL = 3600 # 1 hour
8
+ SIMILARITY_THRESHOLD = 0.8
9
+ MIN_POPULAR_GEM_DOWNLOADS = 1_000_000
10
+
11
+ def initialize
12
+ @popular_gems_cache = nil
13
+ @cache_timestamp = nil
14
+ end
15
+
16
+ def check_dependencies(dependencies)
17
+ suspicious_gems = []
18
+ popular_gems = fetch_popular_gems
19
+
20
+ dependencies.each do |dependency|
21
+ suspicious_match = find_suspicious_match(dependency.name, popular_gems)
22
+ if suspicious_match
23
+ suspicious_gems << {
24
+ gem_name: dependency.name,
25
+ version: dependency.version,
26
+ suspected_target: suspicious_match[:name],
27
+ similarity_score: suspicious_match[:similarity],
28
+ target_downloads: suspicious_match[:downloads],
29
+ risk_level: calculate_risk_level(suspicious_match[:similarity])
30
+ }
31
+ end
32
+ end
33
+
34
+ suspicious_gems
35
+ end
36
+
37
+ private
38
+
39
+ def fetch_popular_gems
40
+ return @popular_gems_cache if cache_valid?
41
+
42
+ begin
43
+ # Use fallback popular gems list for now since RubyGems API structure is complex
44
+ # In a production environment, you might want to use a different data source
45
+ # or scrape the RubyGems.org popular page
46
+ @popular_gems_cache = fallback_popular_gems
47
+ @cache_timestamp = Time.now
48
+ @popular_gems_cache
49
+ rescue
50
+ # Silently fall back to hardcoded list - no need to warn user
51
+ fallback_popular_gems
52
+ end
53
+ end
54
+
55
+ def cache_valid?
56
+ @popular_gems_cache && @cache_timestamp &&
57
+ (Time.now - @cache_timestamp) < POPULAR_GEMS_CACHE_TTL
58
+ end
59
+
60
+ def find_suspicious_match(gem_name, popular_gems)
61
+ return nil if popular_gems.any? { |pg| pg[:name] == gem_name }
62
+
63
+ best_match = nil
64
+ highest_similarity = 0
65
+
66
+ popular_gems.each do |popular_gem|
67
+ similarity = calculate_similarity(gem_name, popular_gem[:name])
68
+
69
+ if similarity >= SIMILARITY_THRESHOLD && similarity > highest_similarity
70
+ highest_similarity = similarity
71
+ best_match = {
72
+ name: popular_gem[:name],
73
+ similarity: similarity,
74
+ downloads: popular_gem[:downloads]
75
+ }
76
+ end
77
+ end
78
+
79
+ best_match
80
+ end
81
+
82
+ def calculate_similarity(str1, str2)
83
+ return 0.0 if str1.nil? || str2.nil? || str1.empty? || str2.empty?
84
+ return 1.0 if str1 == str2
85
+
86
+ # Use Levenshtein distance for similarity calculation
87
+ levenshtein_similarity(str1.downcase, str2.downcase)
88
+ end
89
+
90
+ def levenshtein_similarity(str1, str2)
91
+ distance = levenshtein_distance(str1, str2)
92
+ max_length = [str1.length, str2.length].max
93
+ return 1.0 if max_length == 0
94
+
95
+ 1.0 - (distance.to_f / max_length)
96
+ end
97
+
98
+ def levenshtein_distance(str1, str2)
99
+ matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
100
+
101
+ (0..str1.length).each { |i| matrix[i][0] = i }
102
+ (0..str2.length).each { |j| matrix[0][j] = j }
103
+
104
+ (1..str1.length).each do |i|
105
+ (1..str2.length).each do |j|
106
+ cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
107
+ matrix[i][j] = [
108
+ matrix[i - 1][j] + 1, # deletion
109
+ matrix[i][j - 1] + 1, # insertion
110
+ matrix[i - 1][j - 1] + cost # substitution
111
+ ].min
112
+ end
113
+ end
114
+
115
+ matrix[str1.length][str2.length]
116
+ end
117
+
118
+ def calculate_risk_level(similarity)
119
+ case similarity
120
+ when 0.95..1.0
121
+ "critical"
122
+ when 0.9..0.95
123
+ "high"
124
+ when 0.85..0.9
125
+ "medium"
126
+ else
127
+ "low"
128
+ end
129
+ end
130
+
131
+ def fallback_popular_gems
132
+ # Hardcoded list of very popular Ruby gems as fallback
133
+ [
134
+ {name: "rails", downloads: 100_000_000},
135
+ {name: "bundler", downloads: 90_000_000},
136
+ {name: "rake", downloads: 80_000_000},
137
+ {name: "json", downloads: 70_000_000},
138
+ {name: "minitest", downloads: 60_000_000},
139
+ {name: "thread_safe", downloads: 50_000_000},
140
+ {name: "tzinfo", downloads: 45_000_000},
141
+ {name: "concurrent-ruby", downloads: 40_000_000},
142
+ {name: "i18n", downloads: 35_000_000},
143
+ {name: "activesupport", downloads: 30_000_000},
144
+ {name: "activerecord", downloads: 25_000_000},
145
+ {name: "actionpack", downloads: 20_000_000},
146
+ {name: "actionview", downloads: 18_000_000},
147
+ {name: "activemodel", downloads: 15_000_000},
148
+ {name: "rspec", downloads: 12_000_000},
149
+ {name: "puma", downloads: 10_000_000},
150
+ {name: "nokogiri", downloads: 8_000_000},
151
+ {name: "thor", downloads: 7_000_000},
152
+ {name: "sass", downloads: 6_000_000},
153
+ {name: "devise", downloads: 5_000_000}
154
+ ]
155
+ end
156
+ end
157
+ end
@@ -1,3 +1,3 @@
1
1
  module GemGuard
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.10"
3
3
  end
@@ -15,7 +15,31 @@ module GemGuard
15
15
  vulnerabilities.concat(fetch_ruby_advisory_vulnerabilities(dependency))
16
16
  end
17
17
 
18
- vulnerabilities.uniq { |vuln| vuln.id }
18
+ # Deduplicate vulnerabilities by ID and gem name, merging affected/fixed versions
19
+ deduplicated = {}
20
+ vulnerabilities.each do |vuln|
21
+ key = [vuln.id, vuln.gem_name]
22
+ if deduplicated[key]
23
+ # Merge affected and fixed versions from duplicate entries
24
+ existing = deduplicated[key]
25
+ merged_affected = (existing.affected_versions + vuln.affected_versions).uniq
26
+ merged_fixed = (existing.fixed_versions + vuln.fixed_versions).uniq
27
+
28
+ deduplicated[key] = Vulnerability.new(
29
+ id: existing.id,
30
+ gem_name: existing.gem_name,
31
+ affected_versions: merged_affected,
32
+ fixed_versions: merged_fixed,
33
+ severity: existing.severity,
34
+ summary: existing.summary,
35
+ details: existing.details
36
+ )
37
+ else
38
+ deduplicated[key] = vuln
39
+ end
40
+ end
41
+
42
+ deduplicated.values
19
43
  end
20
44
 
21
45
  private
data/lib/gem_guard.rb CHANGED
@@ -4,6 +4,8 @@ require_relative "gem_guard/vulnerability_fetcher"
4
4
  require_relative "gem_guard/analyzer"
5
5
  require_relative "gem_guard/reporter"
6
6
  require_relative "gem_guard/sbom_generator"
7
+ require_relative "gem_guard/typosquat_checker"
8
+ require_relative "gem_guard/config"
7
9
  require_relative "gem_guard/cli"
8
10
 
9
11
  module GemGuard