typosquatting 0.4.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: fc18d0104e41766e5b0b8603d786cde1ff74f7b65360509a8d880732b1d5f6ca
4
- data.tar.gz: fb48d984d14196d0ccdb2765ee14d986f614d9d1285c329fe6b57d8fe1cb35a6
3
+ metadata.gz: 1b77954d544fc39b144352e1dfa75ab18f52701b1c8e9d7b49f940158c10f38f
4
+ data.tar.gz: 3dec0264d6573615efebeb2a6d21e42a1c610e36a5e62449f35d5cee653bc748
5
5
  SHA512:
6
- metadata.gz: 0b9103d71382bbdfb8af88843a4c73d0abf99a65da6b494027c21d3327eb31f8261545c95514dd0bd4b35b7f681c54d70e4a1abb819d9ac020fcceebd96784d5
7
- data.tar.gz: 122e21bf9b46b6379ea2d925998797161020500e56d440f916a43f995faf4865bad5d60cd5077e2e2d33ebc6cd4aa9046af9e419ac02a39729a156fb016376cf
6
+ metadata.gz: bd100e0d02cda3102f1a65c971a9466deb707f3fc54dc277e188a55d0b8bc91990088469c766f3fa1105950e289152295f0980d392070f8b6eac29f985787b25
7
+ data.tar.gz: cd6b58996822828dc412f8c88baddc792ff80e8ae3b69247f0b1e9481738c2718e630495e7ec4d1b9c8023eaf497c8aafe7f1b4b5b6f3ce866e66d4fbf928a06
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## [0.4.0] - 2026-01-02
4
9
 
5
10
  - Skip intra-namespace typosquats for scoped packages (npm, composer, golang) since namespace owners control all packages under their namespace
@@ -47,6 +47,39 @@ 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) } || []
@@ -246,6 +279,32 @@ module Typosquatting
246
279
 
247
280
  raise
248
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
249
308
  end
250
309
 
251
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.4.0"
4
+ VERSION = "0.5.0"
5
5
  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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt