typosquatting 0.1.0 → 0.2.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: e50bfd6b6ae458a3c588600cf0ae4e3fe7a551bf801d0133dca2c88d5df423d6
4
- data.tar.gz: d9f0abe8dd964b970e0f760f807b4b76a2f71d3ad9a05b32dcf5b19ce1438f76
3
+ metadata.gz: 57ce19f59014bac56c5922b53d59c794ef8b55c3ff13cde363db2bef133aff23
4
+ data.tar.gz: facd26dd6b71803eadfb0c2395f7a0a1249d25992c38d5cf07a15a255de91d69
5
5
  SHA512:
6
- metadata.gz: 9cf712d35089a972dd4b9cd47139cb0b793a901f2665de3027629c0f6f39faa1216ef2df4092ee04bc21c89b5c9e2f44e4b26dcbf95598c9b64a151793b3f6be
7
- data.tar.gz: 4bb644fb9af9173051c6de93b5d0b5e88f3dac01ff6873dba102dfb2a72d64e0acda77daf7116597a1a93945e050a04f9603c18a039f6faca0761be51e310142
6
+ metadata.gz: f1de5348e69ee48a5eadcd7fccc310b5f7224f888e84a69bac5ba5fd6bd7d01a64638650834e39ad9f7c55083ec6dd9b3613ed83aefe6019724325a5fcf3b07d
7
+ data.tar.gz: 40fe04d1f03d7917bac5663a5a22f90b0bfef24307f9656d9401cb77dbff710f332de4c15c7165a564f81ca6a701ea783fd26ceea335b09309260d1526dc87ef
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-12-17
4
+
5
+ - Add GitHub Actions ecosystem for CI/CD workflow typosquatting detection
6
+ - Add namespace-aware variant generation for ecosystems with owner/vendor (Go, Composer, npm scoped packages)
7
+ - Add bitflip algorithm for bitsquatting attacks
8
+ - Add adjacent_insertion algorithm for inserting adjacent keyboard characters
9
+ - Add double_hit algorithm for replacing consecutive identical characters with adjacent keys
10
+ - Add length-aware algorithm filtering to reduce false positives for short package names (under 5 chars)
11
+ - Add combosquatting algorithm for common package suffixes (-js, -py, -cli, -lite, etc.)
12
+
3
13
  ## [0.1.0] - 2025-12-16
4
14
 
5
15
  - Initial release
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Detect potential typosquatting packages across package ecosystems. Generate typosquat variants of package names and check if they exist on package registries.
4
4
 
5
- Supports PyPI, npm, RubyGems, Cargo, Go, Maven, NuGet, Composer, Hex, and Pub.
5
+ Supports PyPI, npm, RubyGems, Cargo, Go, Maven, NuGet, Composer, Hex, Pub, and GitHub Actions.
6
6
 
7
7
  ## When to use this
8
8
 
@@ -17,6 +17,8 @@ This tool helps you:
17
17
 
18
18
  False positives are common. A package named `request` isn't necessarily a typosquat of `requests`. Use the output as a starting point for investigation, not as a definitive verdict.
19
19
 
20
+ Short package names (under 5 characters) produce more false positives because many legitimate short packages exist. By default, the generator uses only high-confidence algorithms (homoglyph, repetition, replacement, transposition) for short names. Use `--no-length-filter` to disable this and run all algorithms regardless of name length.
21
+
20
22
  ## Installation
21
23
 
22
24
  ```bash
@@ -53,6 +55,9 @@ typosquatting check requests -e pypi --dry-run
53
55
  # Check for dependency confusion risks
54
56
  typosquatting confusion com.company:internal-lib -e maven
55
57
 
58
+ # Check GitHub Actions for typosquats
59
+ typosquatting check actions/checkout -e github_actions
60
+
56
61
  # Check multiple packages from a file
57
62
  typosquatting confusion -e maven --file internal-packages.txt
58
63
 
@@ -158,6 +163,7 @@ Use these identifiers with the `-e` / `--ecosystem` flag:
158
163
  | `composer` | Packagist | No | `-` `_` `.` | `vendor/package` format |
159
164
  | `hex` | hex.pm | No | `_` | Underscore only, no hyphens |
160
165
  | `pub` | pub.dev | No | `_` | Underscore only, 2-64 chars |
166
+ | `github_actions` | GitHub | No | `-` `_` `.` | `owner/repo` format, targets CI/CD workflows |
161
167
 
162
168
  ## Algorithms
163
169
 
@@ -177,6 +183,10 @@ Use these names with the `-a` / `--algorithms` flag (comma-separated):
177
183
  | `plural` | Singularize/pluralize | `request` -> `requests` |
178
184
  | `misspelling` | Common typos | `library` -> `libary` |
179
185
  | `numeral` | Number/word swap | `lib2` -> `libtwo` |
186
+ | `bitflip` | Single-bit errors (bitsquatting) | `google` -> `coogle` |
187
+ | `adjacent_insertion` | Insert adjacent keyboard key | `google` -> `googhle` |
188
+ | `double_hit` | Replace double chars with adjacent | `google` -> `giigle` |
189
+ | `combosquatting` | Add common package suffixes | `lodash` -> `lodash-js` |
180
190
 
181
191
  ## SBOM Support
182
192
 
@@ -194,6 +204,10 @@ Package lookups use the [ecosyste.ms](https://packages.ecosyste.ms) API. Request
194
204
 
195
205
  Be mindful when checking many packages. The `--dry-run` flag shows what would be checked without making API calls.
196
206
 
207
+ ## Dataset
208
+
209
+ 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.
210
+
197
211
  ## Development
198
212
 
199
213
  ```bash
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typosquatting
4
+ module Algorithms
5
+ class AdjacentInsertion < Base
6
+ KEYBOARD_ADJACENT = Replacement::KEYBOARD_ADJACENT
7
+
8
+ def generate(package_name)
9
+ variants = []
10
+
11
+ package_name.each_char.with_index do |char, i|
12
+ adjacent = KEYBOARD_ADJACENT[char.downcase] || []
13
+ adjacent.each do |adj_char|
14
+ variants << package_name[0..i] + adj_char + package_name[(i + 1)..]
15
+ variants << package_name[0...i] + adj_char + package_name[i..]
16
+ end
17
+ end
18
+
19
+ variants.uniq
20
+ end
21
+ end
22
+ end
23
+ end
@@ -26,7 +26,11 @@ module Typosquatting
26
26
  WordOrder.new,
27
27
  Plural.new,
28
28
  Misspelling.new,
29
- Numeral.new
29
+ Numeral.new,
30
+ Bitflip.new,
31
+ AdjacentInsertion.new,
32
+ DoubleHit.new,
33
+ Combosquatting.new
30
34
  ]
31
35
  end
32
36
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typosquatting
4
+ module Algorithms
5
+ class Bitflip < Base
6
+ VALID_CHARS = (("a".."z").to_a + ("0".."9").to_a + %w[- _]).freeze
7
+
8
+ def generate(package_name)
9
+ variants = []
10
+
11
+ package_name.each_char.with_index do |char, i|
12
+ flipped = bitflip_char(char)
13
+ flipped.each do |new_char|
14
+ next unless VALID_CHARS.include?(new_char)
15
+
16
+ variant = package_name[0...i] + new_char + package_name[(i + 1)..]
17
+ variants << variant
18
+ end
19
+ end
20
+
21
+ variants.uniq
22
+ end
23
+
24
+ def bitflip_char(char)
25
+ byte = char.ord
26
+ results = []
27
+
28
+ 8.times do |bit|
29
+ flipped_byte = byte ^ (1 << bit)
30
+ next if flipped_byte > 127 || flipped_byte < 32
31
+
32
+ results << flipped_byte.chr
33
+ end
34
+
35
+ results.reject { |c| c == char }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typosquatting
4
+ module Algorithms
5
+ class Combosquatting < Base
6
+ SUFFIXES = %w[
7
+ js .js -js
8
+ py -py -python python
9
+ -node node- -npm npm-
10
+ -cli -api -core -utils -util -lib -pkg
11
+ -lite -dev -test -beta -alpha
12
+ -compat -legacy -next -new -v2
13
+ -simd -fast -async
14
+ s -s
15
+ ].freeze
16
+
17
+ PREFIXES = %w[
18
+ py- python-
19
+ node- npm-
20
+ go-
21
+ js-
22
+ my- the- a-
23
+ ].freeze
24
+
25
+ def generate(package_name)
26
+ variants = []
27
+
28
+ SUFFIXES.each do |suffix|
29
+ variants << "#{package_name}#{suffix}"
30
+ end
31
+
32
+ PREFIXES.each do |prefix|
33
+ variants << "#{prefix}#{package_name}"
34
+ end
35
+
36
+ variants.uniq
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typosquatting
4
+ module Algorithms
5
+ class DoubleHit < Base
6
+ KEYBOARD_ADJACENT = Replacement::KEYBOARD_ADJACENT
7
+
8
+ def generate(package_name)
9
+ variants = []
10
+
11
+ (package_name.length - 1).times do |i|
12
+ next unless package_name[i] == package_name[i + 1]
13
+
14
+ char = package_name[i].downcase
15
+ adjacent = KEYBOARD_ADJACENT[char] || []
16
+
17
+ adjacent.each do |adj_char|
18
+ variant = package_name[0...i] + adj_char + adj_char + package_name[(i + 2)..]
19
+ variants << variant
20
+ end
21
+ end
22
+
23
+ variants.uniq
24
+ end
25
+ end
26
+ end
27
+ end
@@ -36,13 +36,14 @@ module Typosquatting
36
36
  end
37
37
 
38
38
  def generate(args)
39
- options = { format: "text", verbose: false }
39
+ options = { format: "text", verbose: false, length_filtering: true }
40
40
  parser = OptionParser.new do |opts|
41
41
  opts.banner = "Usage: typosquatting generate PACKAGE -e ECOSYSTEM [options]"
42
42
  opts.on("-e", "--ecosystem ECOSYSTEM", "Package ecosystem (required)") { |v| options[:ecosystem] = v }
43
43
  opts.on("-f", "--format FORMAT", "Output format (text, json, csv)") { |v| options[:format] = v }
44
44
  opts.on("-v", "--verbose", "Show algorithm for each variant") { options[:verbose] = true }
45
45
  opts.on("-a", "--algorithms LIST", "Comma-separated list of algorithms to use") { |v| options[:algorithms] = v }
46
+ opts.on("--no-length-filter", "Disable length-based algorithm filtering for short names") { options[:length_filtering] = false }
46
47
  end
47
48
  parser.parse!(args)
48
49
 
@@ -55,14 +56,14 @@ module Typosquatting
55
56
 
56
57
  ecosystem = Ecosystems::Base.get(options[:ecosystem])
57
58
  algorithms = select_algorithms(options[:algorithms])
58
- generator = Generator.new(ecosystem: ecosystem, algorithms: algorithms)
59
+ generator = Generator.new(ecosystem: ecosystem, algorithms: algorithms, length_filtering: options[:length_filtering])
59
60
  variants = generator.generate(package)
60
61
 
61
62
  output_variants(variants, options)
62
63
  end
63
64
 
64
65
  def check(args)
65
- options = { format: "text", verbose: false, existing_only: false, dry_run: false }
66
+ options = { format: "text", verbose: false, existing_only: false, dry_run: false, length_filtering: true }
66
67
  parser = OptionParser.new do |opts|
67
68
  opts.banner = "Usage: typosquatting check PACKAGE -e ECOSYSTEM [options]"
68
69
  opts.on("-e", "--ecosystem ECOSYSTEM", "Package ecosystem (required)") { |v| options[:ecosystem] = v }
@@ -71,6 +72,7 @@ module Typosquatting
71
72
  opts.on("-a", "--algorithms LIST", "Comma-separated list of algorithms to use") { |v| options[:algorithms] = v }
72
73
  opts.on("--existing-only", "Only show packages that exist") { options[:existing_only] = true }
73
74
  opts.on("--dry-run", "Show variants without making API calls") { options[:dry_run] = true }
75
+ opts.on("--no-length-filter", "Disable length-based algorithm filtering for short names") { options[:length_filtering] = false }
74
76
  end
75
77
  parser.parse!(args)
76
78
 
@@ -83,7 +85,7 @@ module Typosquatting
83
85
 
84
86
  ecosystem = Ecosystems::Base.get(options[:ecosystem])
85
87
  algorithms = select_algorithms(options[:algorithms])
86
- generator = Generator.new(ecosystem: ecosystem, algorithms: algorithms)
88
+ generator = Generator.new(ecosystem: ecosystem, algorithms: algorithms, length_filtering: options[:length_filtering])
87
89
  variants = generator.generate(package)
88
90
 
89
91
  if options[:dry_run]
@@ -177,16 +179,17 @@ module Typosquatting
177
179
  def ecosystems
178
180
  puts "Supported ecosystems:"
179
181
  puts ""
180
- puts " pypi - Python Package Index"
181
- puts " npm - Node Package Manager"
182
- puts " gem - RubyGems"
183
- puts " cargo - Rust packages"
184
- puts " golang - Go modules"
185
- puts " maven - Java/JVM packages"
186
- puts " nuget - .NET packages"
187
- puts " composer - PHP packages"
188
- puts " hex - Erlang/Elixir packages"
189
- puts " pub - Dart packages"
182
+ puts " pypi - Python Package Index"
183
+ puts " npm - Node Package Manager"
184
+ puts " gem - RubyGems"
185
+ puts " cargo - Rust packages"
186
+ puts " golang - Go modules"
187
+ puts " maven - Java/JVM packages"
188
+ puts " nuget - .NET packages"
189
+ puts " composer - PHP packages"
190
+ puts " hex - Erlang/Elixir packages"
191
+ puts " pub - Dart packages"
192
+ puts " github_actions - GitHub Actions"
190
193
  end
191
194
 
192
195
  def algorithms
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typosquatting
4
+ module Ecosystems
5
+ class GithubActions < Base
6
+ def initialize
7
+ super
8
+ @name = "github_actions"
9
+ @purl_type = "github"
10
+ end
11
+
12
+ def name_pattern
13
+ /\A[a-zA-Z0-9][a-zA-Z0-9-]*\/[a-zA-Z0-9._-]+\z/
14
+ end
15
+
16
+ def allowed_characters
17
+ /[a-zA-Z0-9._-]/
18
+ end
19
+
20
+ def allowed_delimiters
21
+ %w[- _ .]
22
+ end
23
+
24
+ def case_sensitive?
25
+ false
26
+ end
27
+
28
+ def supports_namespaces?
29
+ true
30
+ end
31
+
32
+ def normalise(name)
33
+ name.downcase.sub(/@.*$/, "")
34
+ end
35
+
36
+ def parse_namespace(name)
37
+ clean_name = name.sub(/@.*$/, "")
38
+ parts = clean_name.split("/", 2)
39
+ if parts.length == 2
40
+ [parts[0], parts[1]]
41
+ else
42
+ [nil, name]
43
+ end
44
+ end
45
+
46
+ def valid_name?(name)
47
+ return false if name.nil? || name.empty?
48
+
49
+ clean_name = name.sub(/@.*$/, "")
50
+ owner, repo = parse_namespace(clean_name)
51
+
52
+ return false if owner.nil? || repo.nil?
53
+ return false if owner.empty? || repo.empty?
54
+
55
+ return false unless valid_owner?(owner)
56
+ return false unless valid_repo?(repo)
57
+
58
+ true
59
+ end
60
+
61
+ def format_name(owner, repo)
62
+ "#{owner}/#{repo}"
63
+ end
64
+
65
+ def valid_owner?(owner)
66
+ return false if owner.length > 39
67
+ return false if owner.start_with?("-")
68
+ return false if owner.end_with?("-")
69
+ return false if owner.include?("--")
70
+
71
+ !!(owner =~ /\A[a-zA-Z0-9][a-zA-Z0-9-]*\z/)
72
+ end
73
+
74
+ def valid_repo?(repo)
75
+ return false if repo.length > 100
76
+ return false if repo.start_with?(".")
77
+
78
+ !!(repo =~ /\A[a-zA-Z0-9._-]+\z/)
79
+ end
80
+ end
81
+
82
+ Base.register(GithubActions.new)
83
+ end
84
+ end
@@ -49,6 +49,10 @@ module Typosquatting
49
49
 
50
50
  !!(name =~ name_pattern)
51
51
  end
52
+
53
+ def format_name(namespace, name)
54
+ "#{namespace}/#{name}"
55
+ end
52
56
  end
53
57
 
54
58
  Base.register(Golang.new)
@@ -59,6 +59,14 @@ module Typosquatting
59
59
 
60
60
  true
61
61
  end
62
+
63
+ def format_name(namespace, name)
64
+ if namespace
65
+ "#{namespace}/#{name}"
66
+ else
67
+ name
68
+ end
69
+ end
62
70
  end
63
71
 
64
72
  Base.register(Npm.new)
@@ -2,17 +2,47 @@
2
2
 
3
3
  module Typosquatting
4
4
  class Generator
5
- attr_reader :ecosystem, :algorithms
5
+ SHORT_NAME_THRESHOLD = 5
6
6
 
7
- def initialize(ecosystem:, algorithms: nil)
7
+ HIGH_CONFIDENCE_ALGORITHMS = %w[
8
+ homoglyph
9
+ repetition
10
+ replacement
11
+ transposition
12
+ ].freeze
13
+
14
+ attr_reader :ecosystem, :algorithms, :length_filtering
15
+
16
+ def initialize(ecosystem:, algorithms: nil, length_filtering: true)
8
17
  @ecosystem = ecosystem.is_a?(String) ? Ecosystems::Base.get(ecosystem) : ecosystem
9
18
  @algorithms = algorithms || Algorithms::Base.all
19
+ @length_filtering = length_filtering
10
20
  end
11
21
 
12
22
  def generate(package_name)
13
23
  results = []
14
24
 
15
- algorithms.each do |algorithm|
25
+ if ecosystem.supports_namespaces?
26
+ results.concat(generate_namespace_aware(package_name))
27
+ else
28
+ results.concat(generate_simple(package_name))
29
+ end
30
+
31
+ dedupe_by_normalised_name(results)
32
+ end
33
+
34
+ def algorithms_for_length(name_length)
35
+ return algorithms unless length_filtering
36
+ return algorithms if name_length >= SHORT_NAME_THRESHOLD
37
+
38
+ algorithms.select { |a| HIGH_CONFIDENCE_ALGORITHMS.include?(a.name) }
39
+ end
40
+
41
+ def generate_simple(package_name)
42
+ results = []
43
+ active_algorithms = algorithms_for_length(package_name.length)
44
+
45
+ active_algorithms.each do |algorithm|
16
46
  variants = algorithm.generate(package_name)
17
47
  variants.each do |variant|
18
48
  next if variant == package_name
@@ -27,7 +57,59 @@ module Typosquatting
27
57
  end
28
58
  end
29
59
 
30
- dedupe_by_normalised_name(results)
60
+ results
61
+ end
62
+
63
+ def generate_namespace_aware(package_name)
64
+ namespace, name = ecosystem.parse_namespace(package_name)
65
+ results = []
66
+
67
+ return generate_simple(package_name) if namespace.nil?
68
+
69
+ namespace_algorithms = algorithms_for_length(namespace.length)
70
+ name_algorithms = algorithms_for_length(name.length)
71
+
72
+ namespace_algorithms.each do |algorithm|
73
+ namespace_variants = algorithm.generate(namespace)
74
+ namespace_variants.each do |ns_variant|
75
+ full_name = rebuild_namespaced_name(ns_variant, name)
76
+ next if full_name == package_name
77
+ next unless ecosystem.valid_name?(full_name)
78
+ next if same_after_normalisation?(package_name, full_name)
79
+
80
+ results << Variant.new(
81
+ name: full_name,
82
+ algorithm: algorithm.name,
83
+ original: package_name
84
+ )
85
+ end
86
+ end
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
+ )
101
+ end
102
+ end
103
+
104
+ results
105
+ end
106
+
107
+ def rebuild_namespaced_name(namespace, name)
108
+ if ecosystem.respond_to?(:format_name)
109
+ ecosystem.format_name(namespace, name)
110
+ else
111
+ "#{namespace}/#{name}"
112
+ end
31
113
  end
32
114
 
33
115
  Variant = Struct.new(:name, :algorithm, :original, keyword_init: true) do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typosquatting
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/typosquatting.rb CHANGED
@@ -15,6 +15,10 @@ require_relative "typosquatting/algorithms/word_order"
15
15
  require_relative "typosquatting/algorithms/plural"
16
16
  require_relative "typosquatting/algorithms/misspelling"
17
17
  require_relative "typosquatting/algorithms/numeral"
18
+ require_relative "typosquatting/algorithms/bitflip"
19
+ require_relative "typosquatting/algorithms/adjacent_insertion"
20
+ require_relative "typosquatting/algorithms/double_hit"
21
+ require_relative "typosquatting/algorithms/combosquatting"
18
22
 
19
23
  require_relative "typosquatting/ecosystems/base"
20
24
  require_relative "typosquatting/ecosystems/pypi"
@@ -27,6 +31,7 @@ require_relative "typosquatting/ecosystems/nuget"
27
31
  require_relative "typosquatting/ecosystems/composer"
28
32
  require_relative "typosquatting/ecosystems/hex"
29
33
  require_relative "typosquatting/ecosystems/pub"
34
+ require_relative "typosquatting/ecosystems/github_actions"
30
35
 
31
36
  require_relative "typosquatting/generator"
32
37
  require_relative "typosquatting/lookup"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -55,8 +55,12 @@ files:
55
55
  - exe/typosquatting
56
56
  - lib/typosquatting.rb
57
57
  - lib/typosquatting/algorithms/addition.rb
58
+ - lib/typosquatting/algorithms/adjacent_insertion.rb
58
59
  - lib/typosquatting/algorithms/base.rb
60
+ - lib/typosquatting/algorithms/bitflip.rb
61
+ - lib/typosquatting/algorithms/combosquatting.rb
59
62
  - lib/typosquatting/algorithms/delimiter.rb
63
+ - lib/typosquatting/algorithms/double_hit.rb
60
64
  - lib/typosquatting/algorithms/homoglyph.rb
61
65
  - lib/typosquatting/algorithms/misspelling.rb
62
66
  - lib/typosquatting/algorithms/numeral.rb
@@ -72,6 +76,7 @@ files:
72
76
  - lib/typosquatting/ecosystems/base.rb
73
77
  - lib/typosquatting/ecosystems/cargo.rb
74
78
  - lib/typosquatting/ecosystems/composer.rb
79
+ - lib/typosquatting/ecosystems/github_actions.rb
75
80
  - lib/typosquatting/ecosystems/golang.rb
76
81
  - lib/typosquatting/ecosystems/hex.rb
77
82
  - lib/typosquatting/ecosystems/maven.rb