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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +15 -1
- data/lib/typosquatting/algorithms/adjacent_insertion.rb +23 -0
- data/lib/typosquatting/algorithms/base.rb +5 -1
- data/lib/typosquatting/algorithms/bitflip.rb +39 -0
- data/lib/typosquatting/algorithms/combosquatting.rb +40 -0
- data/lib/typosquatting/algorithms/double_hit.rb +27 -0
- data/lib/typosquatting/cli.rb +17 -14
- data/lib/typosquatting/ecosystems/github_actions.rb +84 -0
- data/lib/typosquatting/ecosystems/golang.rb +4 -0
- data/lib/typosquatting/ecosystems/npm.rb +8 -0
- data/lib/typosquatting/generator.rb +86 -4
- data/lib/typosquatting/version.rb +1 -1
- data/lib/typosquatting.rb +5 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57ce19f59014bac56c5922b53d59c794ef8b55c3ff13cde363db2bef133aff23
|
|
4
|
+
data.tar.gz: facd26dd6b71803eadfb0c2395f7a0a1249d25992c38d5cf07a15a255de91d69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
@@ -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
|
data/lib/typosquatting/cli.rb
CHANGED
|
@@ -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
|
|
181
|
-
puts " npm
|
|
182
|
-
puts " gem
|
|
183
|
-
puts " cargo
|
|
184
|
-
puts " golang
|
|
185
|
-
puts " maven
|
|
186
|
-
puts " nuget
|
|
187
|
-
puts " composer
|
|
188
|
-
puts " hex
|
|
189
|
-
puts " pub
|
|
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
|
|
@@ -2,17 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
module Typosquatting
|
|
4
4
|
class Generator
|
|
5
|
-
|
|
5
|
+
SHORT_NAME_THRESHOLD = 5
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
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.
|
|
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
|