sorbet-hibp 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +263 -0
- data/lib/haveibeenpwned/client.rb +137 -0
- data/lib/haveibeenpwned/errors.rb +24 -0
- data/lib/haveibeenpwned/pwned_passwords.rb +69 -0
- data/lib/haveibeenpwned/version.rb +5 -0
- data/lib/haveibeenpwned.rb +9 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a0cdf85cd575436f967204205717c5aa3ab0fba4f40c462668d52fe45ba2f9f7
|
|
4
|
+
data.tar.gz: 3bc67bbbbc5e7bdf548565bd3a75555ac2463319f2b7df72d087450f04931575
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91c94864621aae1bdf326280868269020bb4d654145ec487b050ba11f33c553ab8071995c625eb658094e76a64e06dd339a3ec0f573b71e361525a8982bca07a
|
|
7
|
+
data.tar.gz: 19a0d5de08648c463fe3131bc609fac8934ab740f981fb753d9da402b4c71bd92781ca07f7d96619221c59cb8907605b5f998b6db13e005710d8e146a7a7dbf7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sorbeet Payments OU
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# HaveIBeenPwned
|
|
2
|
+
|
|
3
|
+
A simple, clean Ruby wrapper for the Have I Been Pwned API v3.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'haveibeenpwned'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install haveibeenpwned
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Initialize the Client
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'haveibeenpwned'
|
|
31
|
+
|
|
32
|
+
client = HaveIBeenPwned::Client.new(
|
|
33
|
+
api_key: 'your-api-key-here',
|
|
34
|
+
user_agent: 'MyApp/1.0'
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Get your API key from [haveibeenpwned.com/API/Key](https://haveibeenpwned.com/API/Key).
|
|
39
|
+
|
|
40
|
+
### Breach Lookups
|
|
41
|
+
|
|
42
|
+
#### Get all breaches for an account
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
breaches = client.breached_account('test@example.com')
|
|
46
|
+
# => [{"Name"=>"Adobe"}, {"Name"=>"LinkedIn"}]
|
|
47
|
+
|
|
48
|
+
# Get full breach data
|
|
49
|
+
breaches = client.breached_account('test@example.com', truncate_response: false)
|
|
50
|
+
|
|
51
|
+
# Filter by domain
|
|
52
|
+
breaches = client.breached_account('test@example.com', domain: 'adobe.com')
|
|
53
|
+
|
|
54
|
+
# Exclude unverified breaches
|
|
55
|
+
breaches = client.breached_account('test@example.com', include_unverified: false)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Get all breached email addresses for a domain
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# Requires domain verification in your HIBP dashboard
|
|
62
|
+
breached = client.breached_domain('example.com')
|
|
63
|
+
# => {"alias1"=>["Adobe"], "alias2"=>["Adobe", "LinkedIn"]}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Get all breaches in the system
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
all_breaches = client.breaches
|
|
70
|
+
# => [{"Name"=>"Adobe", "Title"=>"Adobe", ...}, ...]
|
|
71
|
+
|
|
72
|
+
# Filter by domain
|
|
73
|
+
adobe_breaches = client.breaches(domain: 'adobe.com')
|
|
74
|
+
|
|
75
|
+
# Filter spam lists
|
|
76
|
+
spam_lists = client.breaches(is_spam_list: true)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Get a single breach
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
breach = client.breach('Adobe')
|
|
83
|
+
# => {"Name"=>"Adobe", "Title"=>"Adobe", "Domain"=>"adobe.com", ...}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Get the latest breach
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
latest = client.latest_breach
|
|
90
|
+
# => {"Name"=>"...", "AddedDate"=>"2025-01-15T10:30:00Z", ...}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Get all data classes
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
classes = client.data_classes
|
|
97
|
+
# => ["Account balances", "Email addresses", "Passwords", ...]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Get subscribed domains
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
domains = client.subscribed_domains
|
|
104
|
+
# => [{"DomainName"=>"example.com", "PwnCount"=>150, ...}]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Stealer Logs
|
|
108
|
+
|
|
109
|
+
Requires Pwned 5+ subscription and verified domain ownership.
|
|
110
|
+
|
|
111
|
+
#### Get stealer log domains for an email
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
domains = client.stealer_logs_by_email('user@example.com')
|
|
115
|
+
# => ["netflix.com", "spotify.com"]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Get email addresses for a website domain
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
emails = client.stealer_logs_by_website_domain('netflix.com')
|
|
122
|
+
# => ["user1@gmail.com", "user2@yahoo.com"]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Get email aliases for an email domain
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
aliases = client.stealer_logs_by_email_domain('example.com')
|
|
129
|
+
# => {"user1"=>["netflix.com"], "user2"=>["spotify.com", "netflix.com"]}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Pastes
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
pastes = client.pastes('test@example.com')
|
|
136
|
+
# => [{"Source"=>"Pastebin", "Id"=>"8Q0BvKD8", "Title"=>"syslog", ...}]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Subscription Status
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
status = client.subscription_status
|
|
143
|
+
# => {"SubscriptionName"=>"Pwned 3", "Rpm"=>100, ...}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Pwned Passwords
|
|
147
|
+
|
|
148
|
+
The Pwned Passwords API is completely separate and requires no authentication.
|
|
149
|
+
|
|
150
|
+
#### Check if a password has been pwned
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
count = HaveIBeenPwned::PwnedPasswords.check('password123')
|
|
154
|
+
# => 123456 (number of times this password appears in breaches)
|
|
155
|
+
|
|
156
|
+
# If password not found
|
|
157
|
+
count = HaveIBeenPwned::PwnedPasswords.check('very-unique-password-xyz')
|
|
158
|
+
# => 0
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Check with padding (enhanced privacy)
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
count = HaveIBeenPwned::PwnedPasswords.check('password123', padding: true)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Check a pre-computed hash
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
hash = '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' # SHA-1 of 'password'
|
|
171
|
+
count = HaveIBeenPwned::PwnedPasswords.check_hash(hash)
|
|
172
|
+
# => 123456
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Get raw range search results
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# Get all hash suffixes for a prefix
|
|
179
|
+
results = HaveIBeenPwned::PwnedPasswords.range_search('5BAA6')
|
|
180
|
+
# => "1E4C9B93F3F0682250B6CF8331B7EE68FD8:123456\n..."
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Error Handling
|
|
184
|
+
|
|
185
|
+
The gem raises specific exceptions for different HTTP errors:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
begin
|
|
189
|
+
breaches = client.breached_account('test@example.com')
|
|
190
|
+
rescue HaveIBeenPwned::NotFoundError
|
|
191
|
+
puts "Account not found in any breaches"
|
|
192
|
+
rescue HaveIBeenPwned::UnauthorizedError
|
|
193
|
+
puts "Invalid API key"
|
|
194
|
+
rescue HaveIBeenPwned::ForbiddenError
|
|
195
|
+
puts "Missing or invalid user agent"
|
|
196
|
+
rescue HaveIBeenPwned::RateLimitError => e
|
|
197
|
+
puts "Rate limit exceeded. Retry after #{e.retry_after} seconds"
|
|
198
|
+
rescue HaveIBeenPwned::BadRequestError => e
|
|
199
|
+
puts "Bad request: #{e.message}"
|
|
200
|
+
rescue HaveIBeenPwned::ServiceUnavailableError
|
|
201
|
+
puts "Service temporarily unavailable"
|
|
202
|
+
rescue HaveIBeenPwned::Error => e
|
|
203
|
+
puts "Unexpected error: #{e.message}"
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Test Accounts
|
|
208
|
+
|
|
209
|
+
Use test accounts on `hibp-integration-tests.com` domain with a test API key (any 32-char hex string like `00000000000000000000000000000000`):
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
client = HaveIBeenPwned::Client.new(
|
|
213
|
+
api_key: '00000000000000000000000000000000',
|
|
214
|
+
user_agent: 'Testing'
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Test account that exists in breaches
|
|
218
|
+
breaches = client.breached_account('account-exists@hibp-integration-tests.com')
|
|
219
|
+
|
|
220
|
+
# Test account with spam list only
|
|
221
|
+
spam = client.breached_account('spam-list-only@hibp-integration-tests.com')
|
|
222
|
+
|
|
223
|
+
# Test account with stealer logs
|
|
224
|
+
logs = client.breached_account('stealer-log@hibp-integration-tests.com')
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
See the [full list of test accounts](https://haveibeenpwned.com/API/v3#TestAccounts) in the API documentation.
|
|
228
|
+
|
|
229
|
+
## Rate Limiting
|
|
230
|
+
|
|
231
|
+
Rate limits vary by subscription level. When exceeded, a `RateLimitError` is raised with the `retry_after` attribute indicating seconds to wait:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
begin
|
|
235
|
+
breaches = client.breached_account('test@example.com')
|
|
236
|
+
rescue HaveIBeenPwned::RateLimitError => e
|
|
237
|
+
sleep e.retry_after
|
|
238
|
+
retry
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Design Philosophy
|
|
243
|
+
|
|
244
|
+
This gem follows KISS, DRY, YAGNI, and shibui principles:
|
|
245
|
+
|
|
246
|
+
- **Simple**: Flat API surface with instance-based configuration
|
|
247
|
+
- **Clean**: Returns raw hashes, no unnecessary abstractions
|
|
248
|
+
- **Minimal**: Only essential dependencies (Faraday)
|
|
249
|
+
- **Clear**: Explicit error handling with semantic exceptions
|
|
250
|
+
|
|
251
|
+
## Contributing
|
|
252
|
+
|
|
253
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
This gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
258
|
+
|
|
259
|
+
## Attribution
|
|
260
|
+
|
|
261
|
+
This gem uses the Have I Been Pwned API. Please ensure proper attribution when using this service in your application.
|
|
262
|
+
|
|
263
|
+
Data sourced from [haveibeenpwned.com](https://haveibeenpwned.com) - check if your email has been compromised in a data breach.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require "erb"
|
|
6
|
+
|
|
7
|
+
module HaveIBeenPwned
|
|
8
|
+
class Client
|
|
9
|
+
BASE_URL = "https://haveibeenpwned.com/api/v3"
|
|
10
|
+
|
|
11
|
+
attr_reader :api_key, :user_agent
|
|
12
|
+
|
|
13
|
+
def initialize(api_key:, user_agent:)
|
|
14
|
+
@api_key = api_key
|
|
15
|
+
@user_agent = user_agent
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Breaches
|
|
19
|
+
|
|
20
|
+
def breached_account(account, truncate_response: true, domain: nil, include_unverified: true)
|
|
21
|
+
params = {
|
|
22
|
+
truncateResponse: truncate_response,
|
|
23
|
+
includeUnverified: include_unverified
|
|
24
|
+
}
|
|
25
|
+
params[:domain] = domain if domain
|
|
26
|
+
|
|
27
|
+
get("/breachedaccount/#{url_encode(account)}", params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def breached_domain(domain)
|
|
31
|
+
get("/breacheddomain/#{domain}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def subscribed_domains
|
|
35
|
+
get("/subscribeddomains")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def breaches(domain: nil, is_spam_list: nil)
|
|
39
|
+
params = {}
|
|
40
|
+
params[:domain] = domain if domain
|
|
41
|
+
params[:isSpamList] = is_spam_list unless is_spam_list.nil?
|
|
42
|
+
|
|
43
|
+
get("/breaches", params)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def breach(name)
|
|
47
|
+
get("/breach/#{name}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def latest_breach
|
|
51
|
+
get("/latestbreach")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def data_classes
|
|
55
|
+
get("/dataclasses")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Stealer Logs
|
|
59
|
+
|
|
60
|
+
def stealer_logs_by_email(email)
|
|
61
|
+
get("/stealerlogsbyemail/#{url_encode(email)}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stealer_logs_by_website_domain(domain)
|
|
65
|
+
get("/stealerlogsbywebsitedomain/#{domain}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def stealer_logs_by_email_domain(domain)
|
|
69
|
+
get("/stealerlogsbyemaildomain/#{domain}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pastes
|
|
73
|
+
|
|
74
|
+
def pastes(account)
|
|
75
|
+
get("/pasteaccount/#{url_encode(account)}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Subscription
|
|
79
|
+
|
|
80
|
+
def subscription_status
|
|
81
|
+
get("/subscription/status")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def connection
|
|
87
|
+
@connection ||= Faraday.new(url: BASE_URL) do |conn|
|
|
88
|
+
conn.headers["hibp-api-key"] = api_key
|
|
89
|
+
conn.headers["user-agent"] = user_agent
|
|
90
|
+
conn.adapter Faraday.default_adapter
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def get(path, params = {})
|
|
95
|
+
response = connection.get(path, params)
|
|
96
|
+
handle_response(response)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_response(response)
|
|
100
|
+
case response.status
|
|
101
|
+
when 200
|
|
102
|
+
parse_json(response.body)
|
|
103
|
+
when 400
|
|
104
|
+
raise BadRequestError, parse_error_message(response)
|
|
105
|
+
when 401
|
|
106
|
+
raise UnauthorizedError, parse_error_message(response)
|
|
107
|
+
when 403
|
|
108
|
+
raise ForbiddenError, parse_error_message(response)
|
|
109
|
+
when 404
|
|
110
|
+
raise NotFoundError, parse_error_message(response)
|
|
111
|
+
when 429
|
|
112
|
+
retry_after = response.headers["retry-after"]&.to_i
|
|
113
|
+
raise RateLimitError.new(parse_error_message(response), retry_after: retry_after)
|
|
114
|
+
when 503
|
|
115
|
+
raise ServiceUnavailableError, parse_error_message(response)
|
|
116
|
+
else
|
|
117
|
+
raise Error, "Unexpected response: #{response.status} - #{response.body}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_json(body)
|
|
122
|
+
return nil if body.nil? || body.empty?
|
|
123
|
+
JSON.parse(body)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_error_message(response)
|
|
127
|
+
body = parse_json(response.body)
|
|
128
|
+
body.is_a?(Hash) ? body["message"] || body["statusCode"] : response.body
|
|
129
|
+
rescue JSON::ParserError
|
|
130
|
+
response.body
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def url_encode(string)
|
|
134
|
+
ERB::Util.url_encode(string.to_s.strip)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaveIBeenPwned
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class BadRequestError < Error; end
|
|
7
|
+
|
|
8
|
+
class UnauthorizedError < Error; end
|
|
9
|
+
|
|
10
|
+
class ForbiddenError < Error; end
|
|
11
|
+
|
|
12
|
+
class NotFoundError < Error; end
|
|
13
|
+
|
|
14
|
+
class RateLimitError < Error
|
|
15
|
+
attr_reader :retry_after
|
|
16
|
+
|
|
17
|
+
def initialize(message, retry_after: nil)
|
|
18
|
+
super(message)
|
|
19
|
+
@retry_after = retry_after
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class ServiceUnavailableError < Error; end
|
|
24
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module HaveIBeenPwned
|
|
7
|
+
class PwnedPasswords
|
|
8
|
+
BASE_URL = "https://api.pwnedpasswords.com"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def check(password, mode: :sha1, padding: false)
|
|
12
|
+
hash = hash_password(password, mode)
|
|
13
|
+
check_hash(hash, mode: mode, padding: padding)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check_hash(hash, mode: :sha1, padding: false)
|
|
17
|
+
hash = hash.upcase
|
|
18
|
+
prefix = hash[0..4]
|
|
19
|
+
suffix = hash[5..-1]
|
|
20
|
+
|
|
21
|
+
response = range_search(prefix, mode: mode, padding: padding)
|
|
22
|
+
|
|
23
|
+
# Parse response and find matching suffix
|
|
24
|
+
response.each_line do |line|
|
|
25
|
+
hash_suffix, count = line.strip.split(":")
|
|
26
|
+
return count.to_i if hash_suffix == suffix
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def range_search(prefix, mode: :sha1, padding: false)
|
|
33
|
+
params = {}
|
|
34
|
+
params[:mode] = mode.to_s if mode == :ntlm
|
|
35
|
+
|
|
36
|
+
headers = {}
|
|
37
|
+
headers["Add-Padding"] = "true" if padding
|
|
38
|
+
|
|
39
|
+
response = connection.get("/range/#{prefix}", params) do |req|
|
|
40
|
+
headers.each { |key, value| req.headers[key] = value }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise Error, "Unexpected response: #{response.status}" unless response.status == 200
|
|
44
|
+
|
|
45
|
+
response.body
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def connection
|
|
51
|
+
@connection ||= Faraday.new(url: BASE_URL) do |conn|
|
|
52
|
+
conn.adapter Faraday.default_adapter
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def hash_password(password, mode)
|
|
57
|
+
case mode
|
|
58
|
+
when :sha1
|
|
59
|
+
Digest::SHA1.hexdigest(password)
|
|
60
|
+
when :ntlm
|
|
61
|
+
# NTLM hashing would require additional gem, keeping simple for now
|
|
62
|
+
raise NotImplementedError, "NTLM hashing not yet implemented. Use check_hash with pre-computed NTLM hash."
|
|
63
|
+
else
|
|
64
|
+
raise ArgumentError, "Invalid mode: #{mode}. Use :sha1 or :ntlm"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sorbet-hibp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Angelos Kapsimanis
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webmock
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
description: A simple, clean Ruby client for the Have I Been Pwned API v3, supporting
|
|
69
|
+
breach lookups, pastes, stealer logs, and pwned passwords
|
|
70
|
+
email:
|
|
71
|
+
- angelos@sorbet.ee
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- lib/haveibeenpwned.rb
|
|
79
|
+
- lib/haveibeenpwned/client.rb
|
|
80
|
+
- lib/haveibeenpwned/errors.rb
|
|
81
|
+
- lib/haveibeenpwned/pwned_passwords.rb
|
|
82
|
+
- lib/haveibeenpwned/version.rb
|
|
83
|
+
homepage: https://github.com/sorbet-ee/haveibeenpwned.
|
|
84
|
+
licenses:
|
|
85
|
+
- MIT
|
|
86
|
+
metadata:
|
|
87
|
+
homepage_uri: https://github.com/sorbet-ee/haveibeenpwned.
|
|
88
|
+
source_code_uri: https://github.com/sorbet-ee/haveibeenpwned.
|
|
89
|
+
changelog_uri: https://github.com/sorbet-ee/haveibeenpwned./blob/master/CHANGELOG.md
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: 2.7.0
|
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
requirements: []
|
|
104
|
+
rubygems_version: 3.6.9
|
|
105
|
+
specification_version: 4
|
|
106
|
+
summary: Ruby wrapper for the Have I Been Pwned API
|
|
107
|
+
test_files: []
|