typosquatting 0.4.0 → 0.5.1

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: f791ab61b6b1a907c25e8ecba6f3e6a2f341e07f5232125297405093e4841cef
4
+ data.tar.gz: 8cea179d8c952ca8b93fb42b76d060f02ec312500b1f4a2ffd2e372e2535385b
5
5
  SHA512:
6
- metadata.gz: 0b9103d71382bbdfb8af88843a4c73d0abf99a65da6b494027c21d3327eb31f8261545c95514dd0bd4b35b7f681c54d70e4a1abb819d9ac020fcceebd96784d5
7
- data.tar.gz: 122e21bf9b46b6379ea2d925998797161020500e56d440f916a43f995faf4865bad5d60cd5077e2e2d33ebc6cd4aa9046af9e419ac02a39729a156fb016376cf
6
+ metadata.gz: cdb27c59163382c8615a3e5783c519e578271be0d52a16beddbf8458a8cf1b62e582e8eec297f8d1cf28c67840bac27e1533cb018b59ed8df3e2af46dca1c80a
7
+ data.tar.gz: 5298442b1f8cafc97fa7c20b321c2cffd6d3a638a5e04b9749047c5341927259eb325e3199d056902e4741b34e51d0e25f431973e4c90841874e19b5d8ce8936
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2026-01-04
4
+
5
+ - Filter duplicate packages in SBOM checking to avoid redundant results and API calls
6
+
7
+ ## [0.5.0] - 2026-01-04
8
+
9
+ - Add bulk lookup for SBOM checking to reduce API calls
10
+ - Test against Ruby 3.4 and 4.0 in CI
11
+
3
12
  ## [0.4.0] - 2026-01-02
4
13
 
5
14
  - 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
@@ -12,8 +12,28 @@ module Typosquatting
12
12
  end
13
13
 
14
14
  def check
15
+ unique_packages = extract_unique_packages
15
16
  results = []
16
17
 
18
+ unique_packages.each do |key, pkg_info|
19
+ suspicions = find_typosquat_matches(pkg_info[:name], pkg_info[:ecosystem])
20
+ next if suspicions.empty?
21
+
22
+ results << SBOMResult.new(
23
+ name: pkg_info[:name],
24
+ version: nil,
25
+ ecosystem: pkg_info[:ecosystem_type],
26
+ purl: pkg_info[:purl],
27
+ suspicions: suspicions
28
+ )
29
+ end
30
+
31
+ results
32
+ end
33
+
34
+ def extract_unique_packages
35
+ packages = {}
36
+
17
37
  sbom.packages.each do |pkg|
18
38
  purl_string = extract_purl(pkg)
19
39
  next unless purl_string
@@ -31,19 +51,17 @@ module Typosquatting
31
51
  end
32
52
 
33
53
  package_name = purl.namespace ? "#{purl.namespace}/#{purl.name}" : purl.name
34
- suspicions = find_typosquat_matches(package_name, ecosystem)
35
- next if suspicions.empty?
54
+ key = "#{purl.type}:#{package_name}"
36
55
 
37
- results << SBOMResult.new(
56
+ packages[key] ||= {
38
57
  name: package_name,
39
- version: purl.version,
40
- ecosystem: purl.type,
41
- purl: purl_string,
42
- suspicions: suspicions
43
- )
58
+ ecosystem: ecosystem,
59
+ ecosystem_type: purl.type,
60
+ purl: purl_string
61
+ }
44
62
  end
45
63
 
46
- results
64
+ packages
47
65
  end
48
66
 
49
67
  SBOMResult = Struct.new(:name, :version, :ecosystem, :purl, :suspicions, keyword_init: true) do
@@ -79,20 +97,23 @@ module Typosquatting
79
97
  lookup = Lookup.new(ecosystem: ecosystem)
80
98
 
81
99
  variants = generator.generate(package_name)
82
- suspicions = []
100
+ return [] if variants.empty?
101
+
102
+ variant_names = variants.map(&:name)
103
+ results = lookup.bulk_lookup(variant_names)
83
104
 
84
- variants.each do |variant|
85
- result = lookup.check(variant.name)
86
- next unless result.exists?
105
+ results_by_name = results.to_h { |r| [r.name, r] }
87
106
 
88
- suspicions << Suspicion.new(
107
+ variants.filter_map do |variant|
108
+ result = results_by_name[variant.name]
109
+ next unless result&.exists?
110
+
111
+ Suspicion.new(
89
112
  name: variant.name,
90
113
  algorithm: variant.algorithm,
91
114
  registries: result.registries
92
115
  )
93
116
  end
94
-
95
- suspicions
96
117
  end
97
118
  end
98
119
  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.1"
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt