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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +406 -0
  7. data/Rakefile +8 -0
  8. data/lib/have_i_been_pwned_api/client.rb +71 -0
  9. data/lib/have_i_been_pwned_api/configuration.rb +37 -0
  10. data/lib/have_i_been_pwned_api/endpoints/breaches/breach.rb +25 -0
  11. data/lib/have_i_been_pwned_api/endpoints/breaches/breached_account.rb +33 -0
  12. data/lib/have_i_been_pwned_api/endpoints/breaches/breached_domain.rb +26 -0
  13. data/lib/have_i_been_pwned_api/endpoints/breaches/breaches.rb +30 -0
  14. data/lib/have_i_been_pwned_api/endpoints/breaches/data_classes.rb +23 -0
  15. data/lib/have_i_been_pwned_api/endpoints/breaches/latest_breach.rb +24 -0
  16. data/lib/have_i_been_pwned_api/endpoints/breaches/subscribed_domains.rb +22 -0
  17. data/lib/have_i_been_pwned_api/endpoints/breaches.rb +18 -0
  18. data/lib/have_i_been_pwned_api/endpoints/endpoint.rb +33 -0
  19. data/lib/have_i_been_pwned_api/endpoints/pastes/paste_account.rb +25 -0
  20. data/lib/have_i_been_pwned_api/endpoints/pastes.rb +12 -0
  21. data/lib/have_i_been_pwned_api/endpoints/pwned_passwords/check_pwd.rb +41 -0
  22. data/lib/have_i_been_pwned_api/endpoints/pwned_passwords.rb +12 -0
  23. data/lib/have_i_been_pwned_api/endpoints/stealer_logs/by_email.rb +24 -0
  24. data/lib/have_i_been_pwned_api/endpoints/stealer_logs/by_email_domain.rb +24 -0
  25. data/lib/have_i_been_pwned_api/endpoints/stealer_logs/by_website_domain.rb +24 -0
  26. data/lib/have_i_been_pwned_api/endpoints/stealer_logs.rb +14 -0
  27. data/lib/have_i_been_pwned_api/endpoints/subscription/status.rb +22 -0
  28. data/lib/have_i_been_pwned_api/endpoints/subscription.rb +12 -0
  29. data/lib/have_i_been_pwned_api/error.rb +48 -0
  30. data/lib/have_i_been_pwned_api/models/breaches/breach.rb +43 -0
  31. data/lib/have_i_been_pwned_api/models/breaches/breach_collection.rb +21 -0
  32. data/lib/have_i_been_pwned_api/models/breaches/breached_domain.rb +16 -0
  33. data/lib/have_i_been_pwned_api/models/breaches/domain.rb +39 -0
  34. data/lib/have_i_been_pwned_api/models/breaches/truncated_breach.rb +13 -0
  35. data/lib/have_i_been_pwned_api/models/pastes/paste.rb +37 -0
  36. data/lib/have_i_been_pwned_api/models/pastes/paste_collection.rb +17 -0
  37. data/lib/have_i_been_pwned_api/models/subscription/subscription_status.rb +38 -0
  38. data/lib/have_i_been_pwned_api/models.rb +14 -0
  39. data/lib/have_i_been_pwned_api/version.rb +5 -0
  40. data/lib/have_i_been_pwned_api.rb +24 -0
  41. data/lib/utils/autoloader.rb +20 -0
  42. data/lib/utils/strings.rb +13 -0
  43. data/sig/have_i_been_pwned_api.rbs +4 -0
  44. 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-04-18
4
+
5
+ - Initial release
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
+ [![CI](https://github.com/hugo0706/have-i-been-pwned-api/actions/workflows/main.yml/badge.svg)](https://github.com/hugo0706/have-i-been-pwned-api/actions/workflows/main.yml)
3
+ [![codecov](https://codecov.io/gh/hugo0706/have-i-been-pwned-api/branch/main/graph/badge.svg?token=LX044H2DY5)](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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: %i[spec]
@@ -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