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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Breaches
7
+ class Breaches < Endpoint
8
+ ALLOWED_PARAMS = %i[domain is_spam_list].freeze
9
+
10
+ class << self
11
+ def call(**kwargs)
12
+ params = parse_optional_params(kwargs, ALLOWED_PARAMS)
13
+
14
+ data = Client.get(uri(params))
15
+ Models::BreachCollection.new(data, truncated: false)
16
+ rescue NotFound
17
+ Models::BreachCollection.new({})
18
+ end
19
+
20
+ private
21
+
22
+ def uri(params)
23
+ uri = URI("#{endpoint_url}breaches")
24
+ uri.query = URI.encode_www_form(params) unless params.empty?
25
+ uri
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Breaches
7
+ class DataClasses < Endpoint
8
+ class << self
9
+ def call
10
+ Client.get(uri)
11
+ rescue NotFound
12
+ []
13
+ end
14
+
15
+ private
16
+
17
+ def uri
18
+ URI("#{endpoint_url}dataclasses")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Breaches
7
+ class LatestBreach < Endpoint
8
+ class << self
9
+ def call
10
+ data = Client.get(uri)
11
+ Models::Breach.new(data)
12
+ rescue NotFound
13
+ Models::Breach.new({})
14
+ end
15
+
16
+ private
17
+
18
+ def uri
19
+ URI("#{endpoint_url}latestbreach")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Breaches
7
+ class SubscribedDomains < Endpoint
8
+ class << self
9
+ def call
10
+ data = Client.get(uri)
11
+ Array(data).map { |d| Models::Domain.new(d) }
12
+ end
13
+
14
+ private
15
+
16
+ def uri
17
+ URI("#{endpoint_url}subscribeddomains")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "breaches/latest_breach"
4
+ require_relative "breaches/breached_account"
5
+ require_relative "breaches/breach"
6
+ require_relative "breaches/breached_domain"
7
+ require_relative "breaches/breaches"
8
+ require_relative "breaches/data_classes"
9
+ require_relative "breaches/subscribed_domains"
10
+
11
+ require_relative "../../utils/strings"
12
+ require_relative "../../utils/autoloader"
13
+
14
+ module HaveIBeenPwnedApi
15
+ module Breaches
16
+ Utils::Autoloader.define_endpoint_methods(self)
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ class Endpoint
5
+ class << self
6
+ def type
7
+ :premium
8
+ end
9
+
10
+ def endpoint_url
11
+ config.base_url_for_endpoint_type(type)
12
+ end
13
+
14
+ def config
15
+ HaveIBeenPwnedApi.config
16
+ end
17
+
18
+ private
19
+
20
+ def parse_optional_params(kwargs, allowed_params)
21
+ params = {}
22
+ kwargs.map do |key, value|
23
+ raise Error, "Invalid argument #{key}" unless allowed_params.include?(key)
24
+
25
+ key = key.to_s.gsub("_", "")
26
+ params[key] = value unless value.nil?
27
+ end
28
+
29
+ params
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Pastes
7
+ class PasteAccount < Endpoint
8
+ class << self
9
+ def call(account:)
10
+ data = Client.get(uri(account))
11
+ Models::PasteCollection.new(data)
12
+ rescue NotFound
13
+ Models::PasteCollection.new({})
14
+ end
15
+
16
+ private
17
+
18
+ def uri(account)
19
+ encoded_account = URI.encode_www_form_component(account)
20
+ URI("#{endpoint_url}pasteaccount/#{encoded_account}")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pastes/paste_account"
4
+
5
+ require_relative "../../utils/strings"
6
+ require_relative "../../utils/autoloader"
7
+
8
+ module HaveIBeenPwnedApi
9
+ module Pastes
10
+ Utils::Autoloader.define_endpoint_methods(self)
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+ require "digest"
5
+
6
+ module HaveIBeenPwnedApi
7
+ module PwnedPasswords
8
+ class CheckPwd < Endpoint
9
+ class << self
10
+ def type
11
+ :free
12
+ end
13
+
14
+ def call(password:, add_padding: false)
15
+ digest = hash_password(password)
16
+
17
+ data = Client.get(uri(digest[..4]),
18
+ headers: { add_padding: add_padding })
19
+
20
+ count_password_appereances(digest, data)
21
+ end
22
+
23
+ private
24
+
25
+ def count_password_appereances(digest, data)
26
+ partial_hash = Regexp.escape(digest[5..])
27
+ count = data.match(/#{partial_hash}:(\d+)/) { $1.to_i }
28
+ count.nil? ? 0 : count
29
+ end
30
+
31
+ def hash_password(password)
32
+ Digest::SHA1.hexdigest(password).upcase
33
+ end
34
+
35
+ def uri(digest_chars)
36
+ URI("#{endpoint_url}range/#{digest_chars}")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pwned_passwords/check_pwd"
4
+
5
+ require_relative "../../utils/strings"
6
+ require_relative "../../utils/autoloader"
7
+
8
+ module HaveIBeenPwnedApi
9
+ module PwnedPasswords
10
+ Utils::Autoloader.define_endpoint_methods(self)
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module StealerLogs
7
+ class ByEmail < Endpoint
8
+ class << self
9
+ def call(email:)
10
+ Client.get(uri(email))
11
+ rescue NotFound
12
+ []
13
+ end
14
+
15
+ private
16
+
17
+ def uri(email)
18
+ encoded_email = URI.encode_www_form_component(email)
19
+ URI("#{endpoint_url}stealerlogsbyemail/#{encoded_email}")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module StealerLogs
7
+ class ByEmailDomain < Endpoint
8
+ class << self
9
+ def call(domain:)
10
+ Client.get(uri(domain))
11
+ rescue NotFound
12
+ {}
13
+ end
14
+
15
+ private
16
+
17
+ def uri(domain)
18
+ encoded_domain = URI.encode_www_form_component(domain)
19
+ URI("#{endpoint_url}stealerlogsbyemaildomain/#{encoded_domain}")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module StealerLogs
7
+ class ByWebsiteDomain < Endpoint
8
+ class << self
9
+ def call(domain:)
10
+ Client.get(uri(domain))
11
+ rescue NotFound
12
+ []
13
+ end
14
+
15
+ private
16
+
17
+ def uri(domain)
18
+ encoded_domain = URI.encode_www_form_component(domain)
19
+ URI("#{endpoint_url}stealerlogsbywebsitedomain/#{encoded_domain}")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stealer_logs/by_email"
4
+ require_relative "stealer_logs/by_email_domain"
5
+ require_relative "stealer_logs/by_website_domain"
6
+
7
+ require_relative "../../utils/strings"
8
+ require_relative "../../utils/autoloader"
9
+
10
+ module HaveIBeenPwnedApi
11
+ module StealerLogs
12
+ Utils::Autoloader.define_endpoint_methods(self)
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../endpoint"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Subscription
7
+ class Status < Endpoint
8
+ class << self
9
+ def call
10
+ data = Client.get(uri)
11
+ Models::SubscriptionStatus.new(data)
12
+ end
13
+
14
+ private
15
+
16
+ def uri
17
+ URI("#{endpoint_url}subscription/status")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subscription/status"
4
+
5
+ require_relative "../../utils/strings"
6
+ require_relative "../../utils/autoloader"
7
+
8
+ module HaveIBeenPwnedApi
9
+ module Subscription
10
+ Utils::Autoloader.define_endpoint_methods(self)
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ class Error < StandardError
5
+ attr_reader :detail
6
+
7
+ DEFAULT_MESSAGE = "An unexpected error occurred"
8
+
9
+ # @param message [String] custom error message
10
+ # @param detail [String, nil] raw response body or other debug info
11
+ def initialize(message = nil, detail: nil)
12
+ super(message || self.class::DEFAULT_MESSAGE)
13
+ @detail = detail
14
+ end
15
+
16
+ def to_s
17
+ if detail && !detail.empty?
18
+ "#{super} - Error detail: #{detail}"
19
+ else
20
+ super
21
+ end
22
+ end
23
+ end
24
+
25
+ class BadRequest < Error
26
+ DEFAULT_MESSAGE = "Bad request — invalid resource format"
27
+ end
28
+
29
+ class Unauthorized < Error
30
+ DEFAULT_MESSAGE = "Missing or invalid API key"
31
+ end
32
+
33
+ class Forbidden < Error
34
+ DEFAULT_MESSAGE = "Forbidden — Check error details"
35
+ end
36
+
37
+ class NotFound < Error
38
+ DEFAULT_MESSAGE = "Resource Not found"
39
+ end
40
+
41
+ class RateLimitExceeded < Error
42
+ DEFAULT_MESSAGE = "Rate limit exceeded"
43
+ end
44
+
45
+ class ServiceUnavailable < Error
46
+ DEFAULT_MESSAGE = "Service unavailable"
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Models
7
+ class Breach
8
+ ATTRS = %w[ Name Title Domain BreachDate AddedDate ModifiedDate
9
+ PwnCount Description DataClasses IsVerified
10
+ IsFabricated IsSensitive IsRetired IsSpamList
11
+ IsMalware IsSubscriptionFree IsStealerLog LogoPath].freeze
12
+
13
+ DATE_FIELDS = %w[BreachDate].freeze
14
+ DATETIME_FIELDS = %w[AddedDate ModifiedDate].freeze
15
+
16
+ ATTRS.each do |k|
17
+ attr_reader Utils::Strings.underscore(k).to_sym
18
+ end
19
+
20
+ def initialize(attrs)
21
+ ATTRS.each do |k|
22
+ key = Utils::Strings.underscore(k)
23
+ raw = attrs[k]
24
+ value = parse_date_field(k, raw)
25
+ instance_variable_set("@#{key}", value)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def parse_date_field(field, raw)
32
+ case field
33
+ when *DATE_FIELDS
34
+ raw && Date.iso8601(raw)
35
+ when *DATETIME_FIELDS
36
+ raw && DateTime.parse(raw)
37
+ else
38
+ raw
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ module Models
5
+ class BreachCollection
6
+ include Enumerable
7
+
8
+ attr_reader :breaches
9
+
10
+ def initialize(data, truncated: true)
11
+ @breaches = data.map do |h|
12
+ if truncated
13
+ TruncatedBreach.new(h["Name"])
14
+ else
15
+ Breach.new(h)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ module Models
5
+ class BreachedDomain
6
+ attr_reader :entries
7
+
8
+ def initialize(attrs)
9
+ @entries = {}
10
+ attrs.each do |account_alias, breaches|
11
+ @entries[account_alias] = breaches
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Models
7
+ class Domain
8
+ ATTRS = %w[ DomainName PwnCount PwnCountExcludingSpamLists
9
+ PwnCountExcludingSpamListsAtLastSubscriptionRenewal
10
+ NextSubscriptionRenewal ].freeze
11
+
12
+ DATETIME_FIELDS = %w[NextSubscriptionRenewal].freeze
13
+
14
+ ATTRS.each do |k|
15
+ attr_reader Utils::Strings.underscore(k).to_sym
16
+ end
17
+
18
+ def initialize(attrs)
19
+ ATTRS.each do |k|
20
+ key = Utils::Strings.underscore(k)
21
+ raw = attrs[k]
22
+ value = parse_date_field(k, raw)
23
+ instance_variable_set("@#{key}", value)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def parse_date_field(field, raw)
30
+ case field
31
+ when *DATETIME_FIELDS
32
+ raw && DateTime.parse(raw)
33
+ else
34
+ raw
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ module Models
5
+ class TruncatedBreach
6
+ attr_reader :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Models
7
+ class Paste
8
+ ATTRS = %w[Source Id Title Domain Date EmailCount].freeze
9
+
10
+ DATETIME_FIELDS = %w[Date].freeze
11
+
12
+ ATTRS.each do |k|
13
+ attr_reader Utils::Strings.underscore(k).to_sym
14
+ end
15
+
16
+ def initialize(attrs)
17
+ ATTRS.each do |k|
18
+ key = Utils::Strings.underscore(k)
19
+ raw = attrs[k]
20
+ value = parse_date_field(k, raw)
21
+ instance_variable_set("@#{key}", value)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def parse_date_field(field, raw)
28
+ case field
29
+ when *DATETIME_FIELDS
30
+ raw && DateTime.parse(raw)
31
+ else
32
+ raw
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ module Models
5
+ class PasteCollection
6
+ include Enumerable
7
+
8
+ attr_reader :pastes
9
+
10
+ def initialize(data)
11
+ @pastes = data.map do |h|
12
+ Paste.new(h)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module HaveIBeenPwnedApi
6
+ module Models
7
+ class SubscriptionStatus
8
+ ATTRS = %w[SubscriptionName Description SubscribedUntil
9
+ Rpm DomainSearchMaxBreachedAccounts].freeze
10
+
11
+ DATETIME_FIELDS = %w[SubscribedUntil].freeze
12
+
13
+ ATTRS.each do |k|
14
+ attr_reader Utils::Strings.underscore(k).to_sym
15
+ end
16
+
17
+ def initialize(attrs)
18
+ ATTRS.each do |k|
19
+ key = Utils::Strings.underscore(k)
20
+ raw = attrs[k]
21
+ value = parse_date_field(k, raw)
22
+ instance_variable_set("@#{key}", value)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def parse_date_field(field, raw)
29
+ case field
30
+ when *DATETIME_FIELDS
31
+ raw && DateTime.parse(raw)
32
+ else
33
+ raw
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ module Models
5
+ autoload :Breach, "have_i_been_pwned_api/models/breaches/breach"
6
+ autoload :TruncatedBreach, "have_i_been_pwned_api/models/breaches/truncated_breach"
7
+ autoload :BreachedDomain, "have_i_been_pwned_api/models/breaches/breached_domain"
8
+ autoload :BreachCollection, "have_i_been_pwned_api/models/breaches/breach_collection"
9
+ autoload :Domain, "have_i_been_pwned_api/models/breaches/domain"
10
+ autoload :Paste, "have_i_been_pwned_api/models/pastes/paste"
11
+ autoload :PasteCollection, "have_i_been_pwned_api/models/pastes/paste_collection"
12
+ autoload :SubscriptionStatus, "have_i_been_pwned_api/models/subscription/subscription_status"
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaveIBeenPwnedApi
4
+ VERSION = "0.1.0"
5
+ end