typosquatting 0.3.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59d9c744171a8ac32d88c7218078d24ce9a27da2822b03c5127569a9cf91be46
4
- data.tar.gz: eb928d00e9d2f3eb5c195628c9a7bc8eaf33e02bc04ce07eab98449b48e3f12c
3
+ metadata.gz: 1b77954d544fc39b144352e1dfa75ab18f52701b1c8e9d7b49f940158c10f38f
4
+ data.tar.gz: 3dec0264d6573615efebeb2a6d21e42a1c610e36a5e62449f35d5cee653bc748
5
5
  SHA512:
6
- metadata.gz: ef4a6f706d3bd5a53d603c1c7d124fd317241ecff5ec898dea1df810ae5e65173986ab3d329ddb7300c89c2608c797b99369425eeb7910cb40f7574de99dfd00
7
- data.tar.gz: 93643869152bc1c8ee092a0289c5c49d8615f69adb849f1f5343d8f11748b425f48b6a1e6028223432cf1eecb725fae24181e9d218297657a9fa61e1309675ef
6
+ metadata.gz: bd100e0d02cda3102f1a65c971a9466deb707f3fc54dc277e188a55d0b8bc91990088469c766f3fa1105950e289152295f0980d392070f8b6eac29f985787b25
7
+ data.tar.gz: cd6b58996822828dc412f8c88baddc792ff80e8ae3b69247f0b1e9481738c2718e630495e7ec4d1b9c8023eaf497c8aafe7f1b4b5b6f3ce866e66d4fbf928a06
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2026-01-04
4
+
5
+ - Add bulk lookup for SBOM checking to reduce API calls
6
+ - Test against Ruby 3.4 and 4.0 in CI
7
+
8
+ ## [0.4.0] - 2026-01-02
9
+
10
+ - Skip intra-namespace typosquats for scoped packages (npm, composer, golang) since namespace owners control all packages under their namespace
11
+ - Add Dockerfile for running without Ruby installed
12
+
3
13
  ## [0.3.0] - 2025-12-17
4
14
 
5
15
  - Add `discover` command to find existing similar packages by edit distance using prefix/postfix API
data/Dockerfile ADDED
@@ -0,0 +1,5 @@
1
+ FROM ruby:3.4-alpine
2
+
3
+ RUN gem install typosquatting
4
+
5
+ ENTRYPOINT ["typosquatting"]
data/README.md CHANGED
@@ -31,6 +31,19 @@ Or add to your Gemfile:
31
31
  gem "typosquatting"
32
32
  ```
33
33
 
34
+ Or run with Docker:
35
+
36
+ ```bash
37
+ docker build -t typosquatting .
38
+ docker run --rm typosquatting generate requests -e pypi
39
+ ```
40
+
41
+ For commands that read local files (like `sbom`), mount your directory:
42
+
43
+ ```bash
44
+ docker run --rm -v $PWD:/src typosquatting sbom /src/bom.json
45
+ ```
46
+
34
47
  ## CLI Usage
35
48
 
36
49
  ```bash
@@ -242,6 +255,26 @@ Full API documentation: [packages.ecosyste.ms/docs](https://packages.ecosyste.ms
242
255
 
243
256
  The [ecosyste-ms/typosquatting-dataset](https://github.com/ecosyste-ms/typosquatting-dataset) contains 143 confirmed typosquatting attacks from security research, mapping malicious packages to their targets with classification and source attribution. Useful for testing detection tools and understanding real attack patterns.
244
257
 
258
+ ## Research
259
+
260
+ The `research/` directory contains a script to scan "critical" packages (high OpenSSF criticality score) for potential typosquats:
261
+
262
+ ```bash
263
+ # Scan critical RubyGems packages
264
+ ruby research/critical_packages.rb rubygems.org
265
+
266
+ # Scan npm
267
+ ruby research/critical_packages.rb npmjs.org
268
+
269
+ # Include all algorithms (default is high-confidence only)
270
+ ruby research/critical_packages.rb rubygems.org --all
271
+
272
+ # Limit to first N packages for testing
273
+ ruby research/critical_packages.rb rubygems.org --limit=100
274
+ ```
275
+
276
+ The script generates variants using all library algorithms, checks which exist on the registry, and outputs a CSV with download counts, creation dates, repository URLs, and package status. It filters out packages that predate the target (can't be typosquats), packages with high download ratios (likely legitimate), and flags packages that have been removed (confirmed typosquats).
277
+
245
278
  ## Development
246
279
 
247
280
  ```bash
@@ -40,6 +40,10 @@ module Typosquatting
40
40
  false
41
41
  end
42
42
 
43
+ def namespace_controls_members?
44
+ true
45
+ end
46
+
43
47
  def parse_namespace(name)
44
48
  [nil, name]
45
49
  end
@@ -85,19 +85,21 @@ module Typosquatting
85
85
  end
86
86
  end
87
87
 
88
- name_algorithms.each do |algorithm|
89
- name_variants = algorithm.generate(name)
90
- name_variants.each do |name_variant|
91
- full_name = rebuild_namespaced_name(namespace, name_variant)
92
- next if full_name == package_name
93
- next unless ecosystem.valid_name?(full_name)
94
- next if same_after_normalisation?(package_name, full_name)
95
-
96
- results << Variant.new(
97
- name: full_name,
98
- algorithm: algorithm.name,
99
- original: package_name
100
- )
88
+ unless ecosystem.namespace_controls_members?
89
+ name_algorithms.each do |algorithm|
90
+ name_variants = algorithm.generate(name)
91
+ name_variants.each do |name_variant|
92
+ full_name = rebuild_namespaced_name(namespace, name_variant)
93
+ next if full_name == package_name
94
+ next unless ecosystem.valid_name?(full_name)
95
+ next if same_after_normalisation?(package_name, full_name)
96
+
97
+ results << Variant.new(
98
+ name: full_name,
99
+ algorithm: algorithm.name,
100
+ original: package_name
101
+ )
102
+ end
101
103
  end
102
104
  end
103
105
 
@@ -47,20 +47,80 @@ module Typosquatting
47
47
  package_names.map { |name| results.find { |n, _| n == name }&.last }
48
48
  end
49
49
 
50
+ def bulk_lookup(package_names)
51
+ return [] if package_names.empty?
52
+
53
+ purls = package_names.map { |name| build_purl(name).to_s }
54
+
55
+ all_packages = []
56
+ purls.each_slice(100) do |batch|
57
+ response = post_json("/packages/bulk_lookup", { purls: batch })
58
+ all_packages.concat(response || [])
59
+ end
60
+
61
+ packages_by_purl = {}
62
+ all_packages.each do |pkg|
63
+ purl = pkg["purl"]
64
+ next unless purl
65
+
66
+ packages_by_purl[purl] ||= []
67
+ packages_by_purl[purl] << pkg
68
+ end
69
+
70
+ package_names.map do |name|
71
+ purl = build_purl(name).to_s
72
+ packages = packages_by_purl[purl] || []
73
+
74
+ Result.new(
75
+ name: name,
76
+ purl: purl,
77
+ packages: packages,
78
+ ecosystem: ecosystem.purl_type
79
+ )
80
+ end
81
+ end
82
+
50
83
  def registries
51
84
  response = fetch("/registries?ecosystem=#{URI.encode_www_form_component(ecosystem.purl_type)}")
52
85
  response&.map { |r| Registry.new(r) } || []
53
86
  end
54
87
 
55
- def list_names(registry:, prefix: nil, postfix: nil)
88
+ def list_names(registry:, prefix: nil, postfix: nil, critical: nil, page: nil, per_page: nil)
56
89
  params = []
57
90
  params << "prefix=#{URI.encode_www_form_component(prefix)}" if prefix
58
91
  params << "postfix=#{URI.encode_www_form_component(postfix)}" if postfix
92
+ params << "critical=true" if critical
93
+ params << "page=#{page}" if page
94
+ params << "per_page=#{per_page}" if per_page
59
95
  query = params.empty? ? "" : "?#{params.join("&")}"
60
96
 
61
97
  fetch("/registries/#{URI.encode_www_form_component(registry)}/package_names#{query}") || []
62
98
  end
63
99
 
100
+ def list_all_names(registry:, prefix: nil, postfix: nil, critical: nil, per_page: 100)
101
+ all_names = []
102
+ page = 1
103
+
104
+ loop do
105
+ names = list_names(
106
+ registry: registry,
107
+ prefix: prefix,
108
+ postfix: postfix,
109
+ critical: critical,
110
+ page: page,
111
+ per_page: per_page
112
+ )
113
+ break if names.empty?
114
+
115
+ all_names.concat(names)
116
+ break if names.length < per_page
117
+
118
+ page += 1
119
+ end
120
+
121
+ all_names
122
+ end
123
+
64
124
  def discover(package_name, max_distance: 2)
65
125
  registry = registries.first
66
126
  return [] unless registry
@@ -219,6 +279,32 @@ module Typosquatting
219
279
 
220
280
  raise
221
281
  end
282
+
283
+ def post_json(path, body)
284
+ uri = URI("#{API_BASE}#{path}")
285
+ request = Net::HTTP::Post.new(uri)
286
+ request["User-Agent"] = USER_AGENT
287
+ request["Accept"] = "application/json"
288
+ request["Content-Type"] = "application/json"
289
+ request.body = JSON.generate(body)
290
+
291
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
292
+ http.request(request)
293
+ end
294
+
295
+ case response
296
+ when Net::HTTPSuccess
297
+ JSON.parse(response.body)
298
+ when Net::HTTPNotFound
299
+ []
300
+ else
301
+ raise APIError, "API request failed: #{response.code} #{response.message}"
302
+ end
303
+ rescue StandardError => e
304
+ raise APIError, "API request failed: #{e.message}" unless e.is_a?(APIError)
305
+
306
+ raise
307
+ end
222
308
  end
223
309
 
224
310
  class APIError < StandardError; end
@@ -79,20 +79,23 @@ module Typosquatting
79
79
  lookup = Lookup.new(ecosystem: ecosystem)
80
80
 
81
81
  variants = generator.generate(package_name)
82
- suspicions = []
82
+ return [] if variants.empty?
83
83
 
84
- variants.each do |variant|
85
- result = lookup.check(variant.name)
86
- next unless result.exists?
84
+ variant_names = variants.map(&:name)
85
+ results = lookup.bulk_lookup(variant_names)
87
86
 
88
- suspicions << Suspicion.new(
87
+ results_by_name = results.to_h { |r| [r.name, r] }
88
+
89
+ variants.filter_map do |variant|
90
+ result = results_by_name[variant.name]
91
+ next unless result&.exists?
92
+
93
+ Suspicion.new(
89
94
  name: variant.name,
90
95
  algorithm: variant.algorithm,
91
96
  registries: result.registries
92
97
  )
93
98
  end
94
-
95
- suspicions
96
99
  end
97
100
  end
98
101
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typosquatting
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -0,0 +1,74 @@
1
+ # Typosquatting Research Tools
2
+
3
+ Scripts for analyzing potential typosquats across package registries.
4
+
5
+ ## critical_packages.rb
6
+
7
+ Scans critical packages (high OpenSSF criticality score) from a registry for potential typosquats using our detection algorithms. Results are written to a timestamped CSV file.
8
+
9
+ ```bash
10
+ # Scan rubygems.org critical packages (high-confidence algorithms only)
11
+ ruby research/critical_packages.rb rubygems.org
12
+
13
+ # Include all algorithm matches
14
+ ruby research/critical_packages.rb rubygems.org --all
15
+
16
+ # Limit to first N packages (useful for testing)
17
+ ruby research/critical_packages.rb rubygems.org --limit=100
18
+ ```
19
+
20
+ Supported registries: rubygems.org, npmjs.org, pypi.org, crates.io, packagist.org, hex.pm, pub.dev, proxy.golang.org, repo1.maven.org, nuget.org
21
+
22
+ ## Algorithms
23
+
24
+ By default, only high-confidence algorithms are used (less likely to produce false positives):
25
+
26
+ - homoglyph - lookalike characters (l vs 1, O vs 0)
27
+ - repetition - doubled characters (lodash vs llodash)
28
+ - replacement - adjacent keyboard keys (lodash vs lodazh)
29
+ - transposition - swapped adjacent characters (lodash vs lodasj)
30
+ - omission - dropped characters (lodash vs lodas)
31
+
32
+ Use `--all` to include all 17 algorithms.
33
+
34
+ ## Filters
35
+
36
+ The script applies several filters to reduce false positives:
37
+
38
+ - **Short names**: Packages under 5 characters are skipped (too many false positives)
39
+ - **Higher downloads**: Packages with more downloads than the critical package are skipped (not typosquats)
40
+ - **Popular packages**: Packages with >= 1% of the critical package's downloads are skipped (likely legitimate)
41
+ - **Predates target**: Packages created before the critical package are skipped (can't be typosquats)
42
+
43
+ ## CSV Output
44
+
45
+ Output files are named `{registry}_{timestamp}.csv` with these columns:
46
+
47
+ | Column | Description |
48
+ |--------|-------------|
49
+ | critical_package | The critical package being checked |
50
+ | critical_downloads | Total downloads of the critical package |
51
+ | critical_created | First release date of the critical package |
52
+ | critical_repo | Repository URL of the critical package |
53
+ | potential_typosquat | A similarly named package that exists |
54
+ | algorithm | Which detection algorithm matched |
55
+ | squat_downloads | Total downloads of the potential typosquat |
56
+ | download_ratio | Squat downloads as percentage of critical downloads |
57
+ | squat_created | First release date of the potential typosquat |
58
+ | squat_status | Package status (empty = active, "removed" = yanked) |
59
+ | squat_repo | Repository URL of the potential typosquat |
60
+ | squat_description | Package description |
61
+
62
+ ## Interpreting Results
63
+
64
+ Signs of a real typosquat:
65
+ - `squat_status` is "removed" (already yanked by registry)
66
+ - No repository URL
67
+ - Very low download ratio
68
+ - Description is empty or generic
69
+ - Created shortly after the critical package became popular
70
+
71
+ Signs of a false positive:
72
+ - Has a legitimate repository with real code
73
+ - Description describes unrelated functionality
74
+ - Reasonable download count for its purpose
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "typosquatting"
6
+ require "csv"
7
+
8
+ class CriticalPackageScanner
9
+ SHORT_NAME_THRESHOLD = 5
10
+ POPULAR_RATIO_THRESHOLD = 1.0 # Skip squats with >= 1% of critical package downloads
11
+
12
+ REGISTRY_MAP = {
13
+ "rubygems.org" => "rubygems",
14
+ "npmjs.org" => "npm",
15
+ "pypi.org" => "pypi",
16
+ "crates.io" => "cargo",
17
+ "packagist.org" => "composer",
18
+ "hex.pm" => "hex",
19
+ "pub.dev" => "pub",
20
+ "proxy.golang.org" => "golang",
21
+ "repo1.maven.org" => "maven",
22
+ "nuget.org" => "nuget"
23
+ }.freeze
24
+
25
+ # High confidence algorithms that indicate likely intentional typosquatting
26
+ HIGH_CONFIDENCE_ALGORITHMS = %w[
27
+ homoglyph
28
+ repetition
29
+ replacement
30
+ transposition
31
+ omission
32
+ ].freeze
33
+
34
+ attr_reader :registry, :results, :errors, :high_confidence_only, :limit
35
+
36
+ def initialize(registry:, high_confidence_only: true, limit: nil)
37
+ @registry = registry
38
+ @high_confidence_only = high_confidence_only
39
+ @limit = limit
40
+ @results = []
41
+ @errors = []
42
+ @prefix_cache = {}
43
+ end
44
+
45
+ def run
46
+ packages = fetch_critical_packages
47
+ puts "Found #{packages.length} critical packages for #{registry}"
48
+ puts
49
+
50
+ packages.each_with_index do |package, index|
51
+ scan_package(package, index + 1, packages.length)
52
+ end
53
+
54
+ write_csv
55
+ print_summary
56
+ end
57
+
58
+ def fetch_critical_packages
59
+ packages = lookup.list_all_names(registry: registry, critical: true, per_page: 1000)
60
+ limit ? packages.first(limit) : packages
61
+ end
62
+
63
+ def scan_package(package_name, current, total)
64
+ print "\r[#{current}/#{total}] Scanning #{package_name.ljust(40)}"
65
+
66
+ # Skip short names - too many false positives
67
+ return if package_name.length < SHORT_NAME_THRESHOLD
68
+
69
+ # Generate typosquatting variants using our algorithms
70
+ variants = generator.generate(package_name)
71
+ return if variants.empty?
72
+
73
+ # Fetch details for the critical package first (needed for download/date comparison)
74
+ critical_details = fetch_package_details(package_name)
75
+ @current_critical_downloads = critical_details&.dig("downloads") || 0
76
+ @current_critical_created = critical_details&.dig("first_release_published_at")
77
+
78
+ # Check which variants exist on the registry
79
+ existing = check_variants_exist(package_name, variants)
80
+ return if existing.empty?
81
+
82
+ results << {
83
+ package: package_name,
84
+ critical_details: critical_details,
85
+ matches: existing
86
+ }
87
+ rescue Typosquatting::APIError => e
88
+ errors << { package: package_name, error: e.message }
89
+ rescue StandardError => e
90
+ errors << { package: package_name, error: e.message }
91
+ end
92
+
93
+ def check_variants_exist(package_name, variants)
94
+ # Filter to high-confidence algorithms if requested
95
+ if high_confidence_only
96
+ variants = variants.select { |v| HIGH_CONFIDENCE_ALGORITHMS.include?(v.algorithm) }
97
+ end
98
+
99
+ # Group variants by prefix for efficient lookup
100
+ variants_by_prefix = variants.group_by { |v| v.name[0, 3] }
101
+
102
+ existing = []
103
+ variants_by_prefix.each do |prefix, prefix_variants|
104
+ @prefix_cache[prefix] ||= lookup.list_names(registry: registry, prefix: prefix)
105
+ existing_set = @prefix_cache[prefix].map(&:downcase).to_set
106
+
107
+ prefix_variants.each do |variant|
108
+ if existing_set.include?(variant.name.downcase) && variant.name.downcase != package_name.downcase
109
+ # Fetch package details
110
+ details = fetch_package_details(variant.name)
111
+ squat_downloads = details&.dig("downloads") || 0
112
+ squat_created = details&.dig("first_release_published_at")
113
+
114
+ # Skip if squat has more downloads than critical package - not a squat
115
+ next if squat_downloads > @current_critical_downloads
116
+
117
+ # Skip if squat is too popular (likely legitimate)
118
+ if @current_critical_downloads > 0
119
+ ratio = squat_downloads.to_f / @current_critical_downloads * 100
120
+ next if ratio >= POPULAR_RATIO_THRESHOLD
121
+ end
122
+
123
+ # Skip if squat predates the critical package (can't be a typosquat)
124
+ if squat_created && @current_critical_created
125
+ next if squat_created < @current_critical_created
126
+ end
127
+
128
+ existing << {
129
+ variant: variant,
130
+ description: details&.dig("description"),
131
+ repository_url: details&.dig("repository_url"),
132
+ downloads: squat_downloads,
133
+ first_release: squat_created,
134
+ status: details&.dig("status")
135
+ }
136
+ end
137
+ end
138
+ end
139
+
140
+ existing
141
+ end
142
+
143
+ def fetch_package_details(package_name)
144
+ result = lookup.check(package_name)
145
+ result.packages.first
146
+ rescue StandardError
147
+ nil
148
+ end
149
+
150
+ def generator
151
+ @generator ||= Typosquatting::Generator.new(ecosystem: ecosystem_for_registry)
152
+ end
153
+
154
+ def lookup
155
+ @lookup ||= Typosquatting::Lookup.new(ecosystem: ecosystem_for_registry)
156
+ end
157
+
158
+ def ecosystem_for_registry
159
+ REGISTRY_MAP[registry] || "rubygems"
160
+ end
161
+
162
+ def output_filename
163
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
164
+ "#{registry.gsub(".", "_")}_#{timestamp}.csv"
165
+ end
166
+
167
+ def write_csv
168
+ return if results.empty?
169
+
170
+ filename = output_filename
171
+ filepath = File.join(__dir__, filename)
172
+
173
+ CSV.open(filepath, "w") do |csv|
174
+ csv << [
175
+ "critical_package", "critical_downloads", "critical_created", "critical_repo",
176
+ "potential_typosquat", "algorithm", "squat_downloads", "download_ratio", "squat_created", "squat_status", "squat_repo", "squat_description"
177
+ ]
178
+
179
+ results.each do |result|
180
+ critical = result[:critical_details]
181
+ critical_downloads = critical&.dig("downloads") || 0
182
+ result[:matches].each do |match|
183
+ squat_downloads = match[:downloads] || 0
184
+ ratio = critical_downloads > 0 ? (squat_downloads.to_f / critical_downloads * 100).round(4) : 0
185
+
186
+ csv << [
187
+ result[:package],
188
+ critical_downloads,
189
+ critical&.dig("first_release_published_at")&.split("T")&.first,
190
+ critical&.dig("repository_url"),
191
+ match[:variant].name,
192
+ match[:variant].algorithm,
193
+ squat_downloads,
194
+ "#{ratio}%",
195
+ match[:first_release]&.split("T")&.first,
196
+ match[:status],
197
+ match[:repository_url],
198
+ match[:description]&.gsub(/\s+/, " ")&.strip
199
+ ]
200
+ end
201
+ end
202
+ end
203
+
204
+ puts "\n\nResults written to #{filepath}"
205
+ end
206
+
207
+ def print_summary
208
+ puts "\n"
209
+ puts "=" * 60
210
+ puts "Results for #{registry}"
211
+ puts "=" * 60
212
+ puts
213
+
214
+ if results.empty?
215
+ puts "No potential typosquats found."
216
+ else
217
+ puts "Found #{results.length} critical packages with potential typosquats"
218
+ puts "Total potential typosquats: #{results.sum { |r| r[:matches].length }}"
219
+
220
+ # Algorithm breakdown
221
+ algo_counts = Hash.new(0)
222
+ results.each do |result|
223
+ result[:matches].each { |m| algo_counts[m[:variant].algorithm] += 1 }
224
+ end
225
+
226
+ puts "\nBy algorithm:"
227
+ algo_counts.sort_by { |_, count| -count }.each do |algo, count|
228
+ puts " #{algo}: #{count}"
229
+ end
230
+
231
+ # Flag suspicious packages (no repo, low downloads)
232
+ suspicious = []
233
+ results.each do |result|
234
+ result[:matches].each do |match|
235
+ if match[:repository_url].nil? || match[:repository_url].to_s.empty?
236
+ suspicious << "#{match[:variant].name} (no repo, #{match[:downloads] || 0} downloads)"
237
+ end
238
+ end
239
+ end
240
+
241
+ if suspicious.any?
242
+ puts "\nSuspicious (no repository):"
243
+ suspicious.first(10).each { |s| puts " #{s}" }
244
+ puts " ... and #{suspicious.length - 10} more" if suspicious.length > 10
245
+ end
246
+
247
+ # Flag removed/yanked packages (confirmed typosquats)
248
+ removed = []
249
+ results.each do |result|
250
+ result[:matches].each do |match|
251
+ if match[:status] == "removed"
252
+ removed << "#{match[:variant].name} (targeting #{result[:package]})"
253
+ end
254
+ end
255
+ end
256
+
257
+ if removed.any?
258
+ puts "\nConfirmed (already yanked):"
259
+ removed.first(10).each { |s| puts " #{s}" }
260
+ puts " ... and #{removed.length - 10} more" if removed.length > 10
261
+ end
262
+ end
263
+
264
+ return if errors.empty?
265
+
266
+ puts "\n" + "=" * 60
267
+ puts "Errors (#{errors.length}):"
268
+ puts "=" * 60
269
+ errors.each do |error|
270
+ puts " #{error[:package]}: #{error[:error]}"
271
+ end
272
+ end
273
+ end
274
+
275
+ if __FILE__ == $PROGRAM_NAME
276
+ registry = ARGV[0] || "rubygems.org"
277
+ high_confidence_only = !ARGV.include?("--all")
278
+ limit = ARGV.find { |a| a.start_with?("--limit=") }&.split("=")&.last&.to_i
279
+
280
+ scanner = CriticalPackageScanner.new(registry: registry, high_confidence_only: high_confidence_only, limit: limit)
281
+ scanner.run
282
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typosquatting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -49,6 +49,7 @@ extra_rdoc_files: []
49
49
  files:
50
50
  - CHANGELOG.md
51
51
  - CODE_OF_CONDUCT.md
52
+ - Dockerfile
52
53
  - LICENSE
53
54
  - README.md
54
55
  - Rakefile
@@ -89,6 +90,8 @@ files:
89
90
  - lib/typosquatting/lookup.rb
90
91
  - lib/typosquatting/sbom.rb
91
92
  - lib/typosquatting/version.rb
93
+ - research/README.md
94
+ - research/critical_packages.rb
92
95
  - sig/typosquatting.rbs
93
96
  homepage: https://github.com/andrew/typosquatting
94
97
  licenses: