have_i_been_pwned_api 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/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +406 -0
- data/Rakefile +8 -0
- data/lib/have_i_been_pwned_api/client.rb +71 -0
- data/lib/have_i_been_pwned_api/configuration.rb +37 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/breach.rb +25 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/breached_account.rb +33 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/breached_domain.rb +26 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/breaches.rb +30 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/data_classes.rb +23 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/latest_breach.rb +24 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches/subscribed_domains.rb +22 -0
- data/lib/have_i_been_pwned_api/endpoints/breaches.rb +18 -0
- data/lib/have_i_been_pwned_api/endpoints/endpoint.rb +33 -0
- data/lib/have_i_been_pwned_api/endpoints/pastes/paste_account.rb +25 -0
- data/lib/have_i_been_pwned_api/endpoints/pastes.rb +12 -0
- data/lib/have_i_been_pwned_api/endpoints/pwned_passwords/check_pwd.rb +41 -0
- data/lib/have_i_been_pwned_api/endpoints/pwned_passwords.rb +12 -0
- data/lib/have_i_been_pwned_api/endpoints/stealer_logs/by_email.rb +24 -0
- data/lib/have_i_been_pwned_api/endpoints/stealer_logs/by_email_domain.rb +24 -0
- data/lib/have_i_been_pwned_api/endpoints/stealer_logs/by_website_domain.rb +24 -0
- data/lib/have_i_been_pwned_api/endpoints/stealer_logs.rb +14 -0
- data/lib/have_i_been_pwned_api/endpoints/subscription/status.rb +22 -0
- data/lib/have_i_been_pwned_api/endpoints/subscription.rb +12 -0
- data/lib/have_i_been_pwned_api/error.rb +48 -0
- data/lib/have_i_been_pwned_api/models/breaches/breach.rb +43 -0
- data/lib/have_i_been_pwned_api/models/breaches/breach_collection.rb +21 -0
- data/lib/have_i_been_pwned_api/models/breaches/breached_domain.rb +16 -0
- data/lib/have_i_been_pwned_api/models/breaches/domain.rb +39 -0
- data/lib/have_i_been_pwned_api/models/breaches/truncated_breach.rb +13 -0
- data/lib/have_i_been_pwned_api/models/pastes/paste.rb +37 -0
- data/lib/have_i_been_pwned_api/models/pastes/paste_collection.rb +17 -0
- data/lib/have_i_been_pwned_api/models/subscription/subscription_status.rb +38 -0
- data/lib/have_i_been_pwned_api/models.rb +14 -0
- data/lib/have_i_been_pwned_api/version.rb +5 -0
- data/lib/have_i_been_pwned_api.rb +24 -0
- data/lib/utils/autoloader.rb +20 -0
- data/lib/utils/strings.rb +13 -0
- data/sig/have_i_been_pwned_api.rbs +4 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9ca03b6ad1b8ad711fbb008e6aaead59c3ae466e75bef27d2fd90f403d748723
|
4
|
+
data.tar.gz: b694ef3a451d590890ff1a7aae3757ce5fb978ae0dcc7ec6735e99b7ba71515b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4aba1e9a33cdea5c548a73c89eea13e08fd352177790814fe5722aaea5cf23907322e9b6a16aca9500f706ff9c1fcec94a990d19ec99da476a7df06fe27f5d55
|
7
|
+
data.tar.gz: d4cdf13a80098f09bbb240ec1aab47278abeae43b6e26ec8f8afcb0514d057cb713c033213224e3e2f1b60db9b0df5ad5397a8f6746eba71d7706a74497fce6d
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.0
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
EnforcedStyle: double_quotes
|
6
|
+
|
7
|
+
Style/StringLiteralsInInterpolation:
|
8
|
+
EnforcedStyle: double_quotes
|
9
|
+
|
10
|
+
Style/Documentation:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/RegexpLiteral:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Style/PerlBackrefs:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Metrics/BlockLength:
|
20
|
+
AllowedMethods: ['describe', 'context']
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 hugo0706
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,406 @@
|
|
1
|
+
# HaveIBeenPwnedApi
|
2
|
+
[](https://github.com/hugo0706/have-i-been-pwned-api/actions/workflows/main.yml)
|
3
|
+
[](https://codecov.io/gh/hugo0706/have-i-been-pwned-api)
|
4
|
+
|
5
|
+
A simple ruby wrapper for [haveibeenpwned's V3 API](https://haveibeenpwned.com/API/v3).
|
6
|
+
|
7
|
+
# Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
15
|
+
```
|
16
|
+
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
21
|
+
```
|
22
|
+
|
23
|
+
# Setup
|
24
|
+
|
25
|
+
Configure the API client on an initializer
|
26
|
+
You can configure the follwing parameters of the gem:
|
27
|
+
| Parameter | Required | Type | Description |
|
28
|
+
|----------------|-----------|-----------|-------------|
|
29
|
+
| `api_key` | `False` | `String` | Required only if using other than [PwnedPassword endpoints](#pwned-passwords) |
|
30
|
+
| `user_agent` | `False` | `String` | User agent used on requests to API. Required by HIBP. Defaults to `"have_i_been_pwned_api gem [current gem version]"` |
|
31
|
+
```ruby
|
32
|
+
HaveIBeenPwnedApi.configure do |config|
|
33
|
+
config.api_key = ENV['HIBP_API_KEY']
|
34
|
+
config.user_agent = "your_custom_user_agent"
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
# Free endpoints
|
39
|
+
|
40
|
+
## Pwned Passwords
|
41
|
+
The usage of this endpoint does not require a HIBP key configured, but it can be used also if you have one.
|
42
|
+
|
43
|
+
This API endpoint returns the total times a given password has been pwned. It returns `0` whenever it has not been pwned.
|
44
|
+
|
45
|
+
It protects the value of queried passwords by using k-Anonymity model, which allows a password to be searched for by using only a partial hash. You can read more about k-anonimity [here](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/)
|
46
|
+
|
47
|
+
| Parameter | Required | Type | Description |
|
48
|
+
|----------------|-----------|-----------|-------------|
|
49
|
+
| `password` | `True` | `String` | (e.g. `"password"`) Represents the password searched for |
|
50
|
+
```ruby
|
51
|
+
HaveIBeenPwnedApi::PwnedPasswords.check_pwd(password: "your-password")
|
52
|
+
# => 1724
|
53
|
+
# Returns 0 if the password has not been compromised
|
54
|
+
# otherwise returns the total amount of times it has been compromised
|
55
|
+
```
|
56
|
+
|
57
|
+
# Premium endpoints
|
58
|
+
The usage of the following endpoints requires a HIBP api key configured.
|
59
|
+
## > Breaches Endpoints
|
60
|
+
### Latest breach
|
61
|
+
This API returns the most recently added breach based on the `"AddedDate"` attribute of the breach model. This may not be the most recent breach to occur as there may be significant lead time between a service being breached and the data later appearing on HIBP.
|
62
|
+
```ruby
|
63
|
+
latest_breach = HaveIBeenPwnedApi::Breaches.latest_breach
|
64
|
+
# Returns an instance of HaveIBeenPwnedApi::Models::Breach
|
65
|
+
latest_breach.class
|
66
|
+
=> HaveIBeenPwnedApi::Models::Breach
|
67
|
+
latest_breach.name
|
68
|
+
=> "SamsungGermany"
|
69
|
+
```
|
70
|
+
See more about the [Breach model](#breach) and how to access its values in Models section
|
71
|
+
|
72
|
+
### Breaches
|
73
|
+
This API endpoint returns the details of each of the breaches in the system stored in a `BreachCollection`
|
74
|
+
```ruby
|
75
|
+
# Returns an instance of HaveIBeenPwnedApi::Models::BreachCollection
|
76
|
+
collection = HaveIBeenPwnedApi::Breaches.breaches
|
77
|
+
collection.breaches.class
|
78
|
+
# => HaveIBeenPwnedApi::Models::BreachCollection
|
79
|
+
collection.breaches.count
|
80
|
+
# => 882
|
81
|
+
```
|
82
|
+
See more about [BreachCollection model](#breachcollection) and how to access its values in Models section
|
83
|
+
|
84
|
+
You can pass optional keyword arguments to narrow down the returned set:
|
85
|
+
|
86
|
+
| Parameter | Required | Type | Description |
|
87
|
+
|----------------|-----------|-----------|-------------|
|
88
|
+
| `domain` | `False` | `String` | (e.g. `"adobe.com"`) Filters the result set to only breaches against the domain specified. It is possible that one site (and consequently domain), is compromised on multiple occasions. |
|
89
|
+
| `is_spam_list` | `False` | `Boolean` | Filters the result set to only breaches that either are or are not flagged as a spam list. |
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Fetch only breaches against adobe.com that are flagged as spam list
|
93
|
+
collection = HaveIBeenPwnedApi::Breaches.breaches(domain: "adobe.com", is_spam_list: true)
|
94
|
+
```
|
95
|
+
|
96
|
+
### Breached Account
|
97
|
+
This API endpoint returns a list of all breaches a particular account has been involved in. The API requires a single parameter which is the account to be searched for.
|
98
|
+
| Parameter | Required | Type | Description |
|
99
|
+
|----------------|-----------|-----------|-------------|
|
100
|
+
| `account` | `True` | `String` | (e.g. `"email@mail.com"`) Not case-sensitive. Represents the account searched for |
|
101
|
+
| `truncate_response` | `False` | `Boolean` | Default `true`. By default reduces the size of responses by 98% returning a collection of TruncatedBreach. If set to `false` it will return a collection of full Breach objects |
|
102
|
+
`domain` | `False` | `string` | e.g. `"adobe.com"`) Filters the result set to only breaches against the domain specified. It is possible that one site (and consequently domain), is compromised on multiple occasions. |
|
103
|
+
`include_unverified` | `False` | `Boolean` | Returns breaches that have been flagged as "unverified". By default, both verified and unverified breaches are returned when performing a search |
|
104
|
+
```ruby
|
105
|
+
# By default returns a BreachCollection of TruncatedBreaches
|
106
|
+
collection = HaveIBeenPwnedApi::Breaches.breached_account(account: "mail@gmail.com")
|
107
|
+
# => #<HaveIBeenPwnedApi::Models::BreachCollection
|
108
|
+
collection.breaches.first.class
|
109
|
+
# => HaveIBeenPwnedApi::Models::TruncatedBreach
|
110
|
+
```
|
111
|
+
If no breach is found an empty BreachCollection is returned
|
112
|
+
```ruby
|
113
|
+
<HaveIBeenPwnedApi::Models::BreachCollection @breaches=[]>
|
114
|
+
```
|
115
|
+
If you set `truncate_response` to `false`, you will get a collection of full breach models.
|
116
|
+
```ruby
|
117
|
+
collection = HaveIBeenPwnedApi::Breaches.breached_account(account: "mail@gmail.com",
|
118
|
+
truncate_response: false,
|
119
|
+
domain: "example.com", include_unverified: false))
|
120
|
+
# => #<HaveIBeenPwnedApi::Models::BreachCollection
|
121
|
+
collection.breaches.first.class
|
122
|
+
# => HaveIBeenPwnedApi::Models::Breach
|
123
|
+
```
|
124
|
+
|
125
|
+
### Breached Domain
|
126
|
+
This API returns email addresses on a given domain and the breaches they've appeared in. Only domains that have been successfully added to [your domain search dashboard](https://haveibeenpwned.com/DomainSearch) will be returned.
|
127
|
+
```ruby
|
128
|
+
# Returns a HaveIBeenPwnedApi::Models::BreachedDomain object
|
129
|
+
breached_domain = HaveIBeenPwnedApi::Breaches.breached_domain(domain: "mydomain.com")
|
130
|
+
# => HaveIBeenPwnedApi::Models::BreachedDomain
|
131
|
+
breached_domain.entries.count
|
132
|
+
# => 2
|
133
|
+
```
|
134
|
+
If it does not find a breach for the given domain, an empty BreachedDomain object is returned
|
135
|
+
```ruby
|
136
|
+
<HaveIBeenPwnedApi::Models::BreachedDomain @entries={}>
|
137
|
+
```
|
138
|
+
See more about the [BreachedDomain model](#breacheddomain) and how to access its values in Models section
|
139
|
+
|
140
|
+
### Data Classes
|
141
|
+
This API returns the full list of breached data classes as an ordered array of strings. Each string represents an attribute of records compromised on a breach. For example `"Email addresses"` and `"Passwords"`
|
142
|
+
```ruby
|
143
|
+
HaveIBeenPwnedApi::Breaches.data_classes
|
144
|
+
# =>
|
145
|
+
# ["Account balances",
|
146
|
+
# "Address book contacts",
|
147
|
+
# "Age groups",
|
148
|
+
# ...]
|
149
|
+
```
|
150
|
+
|
151
|
+
### Subscribed Domains
|
152
|
+
Domains that have been successfully added to [your domain search dashboard](https://haveibeenpwned.com/DomainSearch) are returned via this API.
|
153
|
+
```ruby
|
154
|
+
# Returns an array of Domain objects
|
155
|
+
domains = HaveIBeenPwnedApi::Breaches.subscribed_domains
|
156
|
+
# =>
|
157
|
+
# [#<HaveIBeenPwnedApi::Models::Domain,
|
158
|
+
# ...]
|
159
|
+
```
|
160
|
+
See more about the [Domain model](#domain) and how to access its values in Models section
|
161
|
+
|
162
|
+
---
|
163
|
+
## > Pastes Endpoints
|
164
|
+
### Paste Account
|
165
|
+
Returns all the `Pastes` where a given account is present, stored on a `PasteCollection` object. Takes a single parameter which is the email address to be searched for.
|
166
|
+
| Parameter | Required | Type | Description |
|
167
|
+
|----------------|-----------|-----------|-------------|
|
168
|
+
| `account` | `True` | `String` | (e.g. `"email@mail.com"`) Not case-sensitive. Represents the account searched for |
|
169
|
+
```ruby
|
170
|
+
paste_collection = HaveIBeenPwnedApi::Pastes.paste_account(account: "test@gmail.com")
|
171
|
+
=>
|
172
|
+
#<HaveIBeenPwnedApi::Models::PasteCollection
|
173
|
+
```
|
174
|
+
See more about the [PasteCollection model](#pastecollection) and how to access its values in Models section
|
175
|
+
|
176
|
+
---
|
177
|
+
## > Stealer Logs Endpoints
|
178
|
+
All stealer log APIs [require a Pwned 5 subscription or higher](https://haveibeenpwned.com/API/Key), regardless of domain size. Each search can only be performed against domains that have been successfully added to [your domain search dashboard](https://haveibeenpwned.com/DomainSearch).
|
179
|
+
|
180
|
+
### By email
|
181
|
+
This API returns an array of domains where the given email and password where captured by an infostealer.
|
182
|
+
| Parameter | Required | Type | Description |
|
183
|
+
|----------------|-----------|-----------|-------------|
|
184
|
+
| `email` | `True` | `String` | (e.g. `"email@mail.com"`) Not case-sensitive. Represents the account searched for |
|
185
|
+
```ruby
|
186
|
+
domains = HaveIBeenPwnedApi::StealerLogs.by_email(email: 'mail@mydomain.com')
|
187
|
+
# => ["netflix.com", "spotify.com"]
|
188
|
+
```
|
189
|
+
|
190
|
+
### By domain
|
191
|
+
This API returns an array of emails that have been captured by an infostealer on the given domain.
|
192
|
+
| Parameter | Required | Type | Description |
|
193
|
+
|----------------|-----------|-----------|-------------|
|
194
|
+
| `domain` | `True` | `String` | (e.g. `"mydomain.com"`) Represents the domain searched for |
|
195
|
+
```ruby
|
196
|
+
emails = HaveIBeenPwnedApi::StealerLogs.by_website_domain(domain: 'fiestup.com')
|
197
|
+
# => ["andy@mydomain.com", "jane@mydomain.com"]
|
198
|
+
```
|
199
|
+
|
200
|
+
### By Email Domain
|
201
|
+
This API returns stealer log data by the domain of the email address.
|
202
|
+
| Parameter | Required | Type | Description |
|
203
|
+
|----------------|-----------|-----------|-------------|
|
204
|
+
| `domain` | `True` | `String` | (e.g. `"mydomain.com"`) Represents the domain searched for |
|
205
|
+
```ruby
|
206
|
+
emails = HaveIBeenPwnedApi::StealerLogs.by_email_domain(domain: 'fiestup.com')
|
207
|
+
# => {"andy"=>["netflix.com"], "jane"=>["netflix.com", "spotify.com"]}
|
208
|
+
```
|
209
|
+
|
210
|
+
|
211
|
+
---
|
212
|
+
## > Subscription Endpoints
|
213
|
+
### Status
|
214
|
+
This API returns details of the current subscription
|
215
|
+
```ruby
|
216
|
+
HaveIBeenPwnedApi::Subscription.status
|
217
|
+
# =>
|
218
|
+
# #<HaveIBeenPwnedApi::Models::SubscriptionStatus
|
219
|
+
# @description="Domains with up to 25 breached addresses each, and a rate limited API key allowing 10 email address searches per minute",
|
220
|
+
# @domain_search_max_breached_accounts=25,
|
221
|
+
# @rpm=10,
|
222
|
+
# @subscribed_until=#<DateTime: 2025-05-18T11:52:59+00:00 ((2460814j,42779s,0n),+0s,2299161j)>,
|
223
|
+
# @subscription_name="Pwned 1">
|
224
|
+
```
|
225
|
+
See more about [SubsctiptionStatus model](#subscriptionstatus) and how to access its values in Models section
|
226
|
+
|
227
|
+
---
|
228
|
+
# Models
|
229
|
+
### BreachCollection
|
230
|
+
A wrapper around an array of `Breach` or `TruncatedBreach` objects. Includes `Enumerable` so you can iterate, filter, and query like a standard Ruby collection.
|
231
|
+
|
232
|
+
#### Attribute readers
|
233
|
+
|
234
|
+
- **`breaches`** (`Array<HaveIBeenPwnedApi::Models::Breach` or `TruncatedBreach>`): the list of breach models in this collection
|
235
|
+
|
236
|
+
---
|
237
|
+
### Breach
|
238
|
+
Represents the full details of a single breach returned by the Have I Been Pwned API.
|
239
|
+
|
240
|
+
#### Example
|
241
|
+
```ruby
|
242
|
+
<HaveIBeenPwnedApi::Models::Breach
|
243
|
+
@added_date=#<DateTime: 2025-04-13T00:24:36+00:00 ((2460779j,1476s,0n),+0s,2299161j)>,
|
244
|
+
@breach_date=#<Date: 2025-03-30 ((2460765j,0s,0n),+0s,2299161j)>,
|
245
|
+
@data_classes=["Email addresses", "Names", "Physical addresses", "Purchases", "Salutations", "Shipment tracking numbers", "Support tickets"],
|
246
|
+
@description=
|
247
|
+
"In March 2025, <a href=\"https://www.infostealers.com/article/samsung-tickets-data-leak-infostealers-strike-again-in-massive-free-dump/\" target=\"_blank\" rel=\"noopener\">data from Samsung Germany was compromised in a data breach of their logistics provider, Spectos</a>. Allegedly due to credentials being obtained by malware running on a Spectos employee's machine, the breach included 216k unique email addresses along with names, physical addresses, items purchased from Samsung Germany and related support tickets and shipping tracking numbers.",
|
248
|
+
@domain="samsung.de",
|
249
|
+
@is_fabricated=false,
|
250
|
+
@is_malware=false,
|
251
|
+
@is_retired=false,
|
252
|
+
@is_sensitive=false,
|
253
|
+
@is_spam_list=false,
|
254
|
+
@is_stealer_log=false,
|
255
|
+
@is_subscription_free=false,
|
256
|
+
@is_verified=true,
|
257
|
+
@logo_path="https://haveibeenpwned.com/Content/Images/PwnedLogos/Samsung.png",
|
258
|
+
@modified_date=#<DateTime: 2025-04-13T12:42:28+00:00 ((2460779j,45748s,0n),+0s,2299161j)>,
|
259
|
+
@name="SamsungGermany",
|
260
|
+
@pwn_count=216333,
|
261
|
+
@title="Samsung Germany Customer Tickets">
|
262
|
+
```
|
263
|
+
|
264
|
+
#### Attribute readers
|
265
|
+
|
266
|
+
- **`name`** (`String`): Internal, Pascal-cased unique breach identifier.
|
267
|
+
- **`title`** (`String`): User-friendly breach title .
|
268
|
+
- **`domain`** (`String`): Primary website domain where the breach occurred.
|
269
|
+
- **`breach_date`** (`Date`): Date the breach happened.
|
270
|
+
- **`added_date`** (`DateTime`): When the breach was added to HIBP.
|
271
|
+
- **`modified_date`** (`DateTime`): Last time the breach record was updated.
|
272
|
+
- **`pwn_count`** (`Integer`): Number of accounts loaded into the system for this breach.
|
273
|
+
- **`description`** (`String`): HTML overview of the incident (may include links, formatting).
|
274
|
+
- **`data_classes`** (`Array<String>`): List of data types compromised (e.g. `"Email addresses"`, `"Passwords"`, etc.).
|
275
|
+
- **Boolean flags** (`true`/`false`):
|
276
|
+
- `is_verified`
|
277
|
+
- `is_fabricated`
|
278
|
+
- `is_sensitive`
|
279
|
+
- `is_retired`
|
280
|
+
- `is_spam_list`
|
281
|
+
- `is_malware`
|
282
|
+
- `is_subscription_free`
|
283
|
+
- `is_stealer_log`
|
284
|
+
Indicate special breach characteristics (verified, fabricated, sensitive, etc.).
|
285
|
+
- **`logo_path`** (`String`): URL to the breach’s PNG logo.
|
286
|
+
|
287
|
+
---
|
288
|
+
### TruncatedBreach
|
289
|
+
A simplified Breach model used when only breach names are returned (e.g. truncated responses).
|
290
|
+
|
291
|
+
#### Attribute readers
|
292
|
+
|
293
|
+
- **`name`** (`String`): Pascal-cased breach identifier (same as the `Name` field in the API).
|
294
|
+
|
295
|
+
---
|
296
|
+
### BreachedDomain
|
297
|
+
Represents the result of a domain-scoped breach lookup, where each email alias is mapped to the list of breach names in which it appears.
|
298
|
+
|
299
|
+
#### Example
|
300
|
+
```ruby
|
301
|
+
<HaveIBeenPwnedApi::Models::BreachedDomain @entries={"alias1"=>["Adobe"], "alias2"=>["Adobe", "Gawker", "Stratfor"], "alias3"=>["AshleyMadison"]}>
|
302
|
+
```
|
303
|
+
|
304
|
+
#### Attribute readers
|
305
|
+
|
306
|
+
- **`entries`** (`Hash<String, Array<String>>`):
|
307
|
+
A hash mapping each email local-part (e.g. `"alias1"` for `alias1@example.com`) to an array of Pascal-cased breach names:
|
308
|
+
```ruby
|
309
|
+
{
|
310
|
+
"alias1" => ["Adobe"],
|
311
|
+
"alias2" => ["Adobe", "Gawker", "Stratfor"],
|
312
|
+
"alias3" => ["AshleyMadison"]
|
313
|
+
}
|
314
|
+
```
|
315
|
+
|
316
|
+
---
|
317
|
+
### Domain
|
318
|
+
|
319
|
+
#### Attribute readers
|
320
|
+
|
321
|
+
- **`domain_name`** (`String`): The full domain name that has been successfully verified.
|
322
|
+
- **`pwn_count`** (`Integer` or `nil`): Total breached email addresses found on the domain at last search (null if no searches yet).
|
323
|
+
- **`pwn_count_excluding_spam_lists`** (`Integer` or `nil`): Same as `pwn_count`, excluding breaches flagged as spam lists (null if no searches yet).
|
324
|
+
- **`pwn_count_excluding_spam_lists_at_last_subscription_renewal`** (`Integer` or `nil`): `pwn_count_excluding_spam_lists` value locked in when the current subscription began (null if never subscribed).
|
325
|
+
- **`next_subscription_renewal`** (`DateTime` or `nil`): ISO 8601 timestamp when the current subscription ends (null if never subscribed).
|
326
|
+
|
327
|
+
---
|
328
|
+
### PasteCollection
|
329
|
+
A wrapper around an array of `Paste` objects returned by the paste-related endpoints. Includes `Enumerable` so you can iterate, filter, and query just like a standard Ruby collection.
|
330
|
+
|
331
|
+
#### Example
|
332
|
+
```ruby
|
333
|
+
<HaveIBeenPwnedApi::Models::PasteCollection
|
334
|
+
@pastes=
|
335
|
+
[#<HaveIBeenPwnedApi::Models::Paste @date=#<DateTime: 2016-06-22T11:06:26+00:00 ((2457562j,39986s,0n),+0s,2299161j)>, @domain=nil, @email_count=323, @id="Y8k3SJjg", @source="Pastebin", @title=nil>,
|
336
|
+
#<HaveIBeenPwnedApi::Models::Paste @date=#<DateTime: 2016-02-18T15:51:55+00:00 ((2457437j,57115s,0n),+0s,2299161j)>, @domain=nil, @email_count=2225, @id="X1tzUFdD", @source="Pastebin", @title=nil>, ...
|
337
|
+
```
|
338
|
+
|
339
|
+
#### Attribute readers
|
340
|
+
|
341
|
+
- **`pastes`** (`Array<HaveIBeenPwnedApi::Models::Paste>`):
|
342
|
+
The underlying list of `Paste` instances.
|
343
|
+
|
344
|
+
---
|
345
|
+
### Paste
|
346
|
+
Represents a single paste record.
|
347
|
+
|
348
|
+
#### Example
|
349
|
+
```ruby
|
350
|
+
<HaveIBeenPwnedApi::Models::Paste @date=#<DateTime: 2014-03-04T19:14:54+00:00 ((2456721j,69294s,0n),+0s,2299161j)>, @domain=nil, @email_count=139, @id="8Q0BvKD8", @source="Pastebin", @title="syslog"
|
351
|
+
```
|
352
|
+
|
353
|
+
#### Attribute readers
|
354
|
+
|
355
|
+
- **`source`** (`String`): The service the paste came from (e.g. `"Pastebin"`, `"Ghostbin"`, `"JustPaste"`, etc.).
|
356
|
+
- **`id`** (`String`): The identifier assigned by the source service (used, together with `source`, to build the paste URL).
|
357
|
+
- **`title`** (`String` or `nil`): The paste’s title as shown on the source site (may be `nil` if none was provided).
|
358
|
+
- **`date`** (`DateTime` or `nil`): Timestamp when the paste was posted (precision to the second; may be `nil` if unavailable).
|
359
|
+
- **`email_count`** (`Integer`): Number of email addresses extracted from the paste.
|
360
|
+
|
361
|
+
---
|
362
|
+
### SubscriptionStatus
|
363
|
+
Encapsulates your current subscription details and rate limits.
|
364
|
+
|
365
|
+
#### Example
|
366
|
+
```ruby
|
367
|
+
<HaveIBeenPwnedApi::Models::SubscriptionStatus
|
368
|
+
@description="Domains with up to 25 breached addresses each, and a rate limited API key allowing 10 email address searches per minute",
|
369
|
+
@domain_search_max_breached_accounts=25,
|
370
|
+
@rpm=10,
|
371
|
+
@subscribed_until=#<DateTime: 2025-05-18T11:52:59+00:00 ((2460814j,42779s,0n),+0s,2299161j)>,
|
372
|
+
@subscription_name="Pwned 1">
|
373
|
+
```
|
374
|
+
|
375
|
+
#### Attribute readers
|
376
|
+
|
377
|
+
- **`description`** (`String`):
|
378
|
+
Human-readable summary of your subscription tier (e.g. `"Domains with up to 25 breached addresses each, and a rate limited API key allowing 10 email address searches per minute"`).
|
379
|
+
|
380
|
+
- **`domain_search_max_breached_accounts`** (`Integer`):
|
381
|
+
Maximum number of breached accounts returned per domain search (e.g. `25`).
|
382
|
+
|
383
|
+
- **`rpm`** (`Integer`):
|
384
|
+
Allowed email-search requests per minute (rate limit) (e.g. `10`).
|
385
|
+
|
386
|
+
- **`subscribed_until`** (`DateTime`):
|
387
|
+
ISO 8601 timestamp when your current subscription expires (e.g. `2025-05-18T11:52:59+00:00`).
|
388
|
+
|
389
|
+
- **`subscription_name`** (`String`):
|
390
|
+
The name of your subscription plan (e.g. `"Pwned 1"`).
|
391
|
+
|
392
|
+
## Errors
|
393
|
+
|
394
|
+
## Development
|
395
|
+
|
396
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
397
|
+
|
398
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
399
|
+
|
400
|
+
## Contributing
|
401
|
+
|
402
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hugo0706/have_i_been_pwned_api
|
403
|
+
|
404
|
+
## License
|
405
|
+
|
406
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module HaveIBeenPwnedApi
|
8
|
+
class Client
|
9
|
+
ERROR_CLASSES = {
|
10
|
+
"400" => BadRequest,
|
11
|
+
"401" => Unauthorized,
|
12
|
+
"403" => Forbidden,
|
13
|
+
"404" => NotFound,
|
14
|
+
"429" => RateLimitExceeded,
|
15
|
+
"503" => ServiceUnavailable
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def get(uri, headers: {})
|
20
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
21
|
+
http.use_ssl = true
|
22
|
+
|
23
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
24
|
+
set_headers(request, headers)
|
25
|
+
|
26
|
+
response = http.request(request)
|
27
|
+
|
28
|
+
handle_errors!(response)
|
29
|
+
|
30
|
+
parse_body(response)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def handle_errors!(resp)
|
36
|
+
return if resp.code == "200"
|
37
|
+
|
38
|
+
error_class = ERROR_CLASSES.fetch(resp.code, Error)
|
39
|
+
|
40
|
+
raise error_class.new(detail: resp.body)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_body(resp)
|
44
|
+
case resp.header["content-type"].downcase
|
45
|
+
when /text\/plain/
|
46
|
+
resp.body
|
47
|
+
when /application\/json/
|
48
|
+
JSON.parse(resp.body)
|
49
|
+
else
|
50
|
+
resp.body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_headers(request, headers)
|
55
|
+
request["hibp-api-key"] = config.api_key
|
56
|
+
request["user-agent"] = config.user_agent
|
57
|
+
|
58
|
+
return if headers.empty?
|
59
|
+
|
60
|
+
headers.each do |header, value|
|
61
|
+
header = header.to_s.gsub("_", "-")
|
62
|
+
request[header] = value.to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def config
|
67
|
+
HaveIBeenPwnedApi.config
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HaveIBeenPwnedApi
|
4
|
+
class Configuration
|
5
|
+
PREMIUM_URL = "https://haveibeenpwned.com/api/v3/"
|
6
|
+
PWNED_PWD_URL = "https://api.pwnedpasswords.com/"
|
7
|
+
|
8
|
+
attr_accessor :api_key, :user_agent
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@api_key = nil
|
12
|
+
@user_agent = "have_i_been_pwned_api gem v#{VERSION}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def base_url_for_endpoint_type(type)
|
16
|
+
check_access_allowed!(type)
|
17
|
+
type == :free ? PWNED_PWD_URL : PREMIUM_URL
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
other.is_a?(Configuration) &&
|
22
|
+
attributes == other.attributes
|
23
|
+
end
|
24
|
+
|
25
|
+
def attributes
|
26
|
+
instance_variables.map { |iv| [iv, instance_variable_get(iv)] }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_access_allowed!(type)
|
32
|
+
return unless api_key.nil? && type == :premium
|
33
|
+
|
34
|
+
raise Error, "An HIBP API key is required for premium endpoints"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../endpoint"
|
4
|
+
|
5
|
+
module HaveIBeenPwnedApi
|
6
|
+
module Breaches
|
7
|
+
class Breach < Endpoint
|
8
|
+
class << self
|
9
|
+
def call(name:)
|
10
|
+
data = Client.get(uri(name))
|
11
|
+
Models::Breach.new(data)
|
12
|
+
rescue NotFound
|
13
|
+
Models::Breach.new({})
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def uri(name)
|
19
|
+
encoded_name = URI.encode_www_form_component(name)
|
20
|
+
URI("#{endpoint_url}breach/#{encoded_name}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../endpoint"
|
4
|
+
|
5
|
+
module HaveIBeenPwnedApi
|
6
|
+
module Breaches
|
7
|
+
class BreachedAccount < Endpoint
|
8
|
+
ALLOWED_PARAMS = %i[domain include_unverified truncate_response].freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def call(account:, **kwargs)
|
12
|
+
truncate = kwargs[:truncate_response] != false
|
13
|
+
params = parse_optional_params(kwargs, ALLOWED_PARAMS)
|
14
|
+
|
15
|
+
data = Client.get(uri(account, params))
|
16
|
+
|
17
|
+
Models::BreachCollection.new(data, truncated: truncate)
|
18
|
+
rescue NotFound
|
19
|
+
Models::BreachCollection.new({})
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def uri(account, params)
|
25
|
+
encoded_account = URI.encode_www_form_component(account)
|
26
|
+
uri = URI("#{endpoint_url}breachedaccount/#{encoded_account}")
|
27
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
28
|
+
uri
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../endpoint"
|
4
|
+
|
5
|
+
module HaveIBeenPwnedApi
|
6
|
+
module Breaches
|
7
|
+
class BreachedDomain < Endpoint
|
8
|
+
class << self
|
9
|
+
def call(domain:)
|
10
|
+
data = Client.get(uri(domain))
|
11
|
+
|
12
|
+
Models::BreachedDomain.new(data)
|
13
|
+
rescue NotFound
|
14
|
+
Models::BreachedDomain.new({})
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def uri(domain)
|
20
|
+
encoded_domain = URI.encode_www_form_component(domain)
|
21
|
+
URI("#{endpoint_url}breacheddomain/#{encoded_domain}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|