hibp-client 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/.gitignore +66 -0
- data/.rspec +3 -0
- data/.travis.yml +13 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +189 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/hibp.gemspec +37 -0
- data/lib/hibp.rb +30 -0
- data/lib/hibp/client.rb +162 -0
- data/lib/hibp/helpers/attribute_assignment.rb +40 -0
- data/lib/hibp/helpers/json_conversion.rb +47 -0
- data/lib/hibp/models/breach.rb +115 -0
- data/lib/hibp/models/password.rb +28 -0
- data/lib/hibp/models/paste.rb +62 -0
- data/lib/hibp/parsers/breach.rb +42 -0
- data/lib/hibp/parsers/json.rb +62 -0
- data/lib/hibp/parsers/password.rb +36 -0
- data/lib/hibp/parsers/paste.rb +24 -0
- data/lib/hibp/query.rb +85 -0
- data/lib/hibp/request.rb +106 -0
- data/lib/hibp/service_error.rb +53 -0
- data/lib/hibp/version.rb +5 -0
- metadata +143 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Helpers
|
5
|
+
# Hibp::Helpers::AttributeAssignment
|
6
|
+
#
|
7
|
+
# Used to assign attributes in models
|
8
|
+
#
|
9
|
+
module AttributeAssignment
|
10
|
+
private
|
11
|
+
|
12
|
+
def assign_attributes(new_attributes)
|
13
|
+
unless new_attributes.is_a?(Hash)
|
14
|
+
raise ArgumentError, 'Attributes must be a Hash'
|
15
|
+
end
|
16
|
+
|
17
|
+
return if new_attributes.nil? || new_attributes.empty?
|
18
|
+
|
19
|
+
attributes = stringify_keys(new_attributes)
|
20
|
+
attributes.each { |k, v| _assign_attribute(k, v) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def stringify_keys(hash)
|
24
|
+
transform_keys(hash, &:to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
def transform_keys(hash)
|
28
|
+
hash.each_with_object({}) do |(key, value), result|
|
29
|
+
result[yield(key)] = value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def _assign_attribute(attr_name, attr_value)
|
34
|
+
return unless respond_to?("#{attr_name}=")
|
35
|
+
|
36
|
+
public_send("#{attr_name}=", attr_value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Helpers
|
5
|
+
# Hibp::Helpers::JsonConversion
|
6
|
+
#
|
7
|
+
# Used to convert raw API response data to the entity models
|
8
|
+
#
|
9
|
+
module JsonConversion
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Convert raw data to the entity model
|
13
|
+
#
|
14
|
+
# @param data [Array<Hash>, Hash] - Raw data from response
|
15
|
+
#
|
16
|
+
def convert(data, &block)
|
17
|
+
data.is_a?(Array) ? convert_to_list(data, &block) : convert_to_entity(data, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def convert_to_list(data, &block)
|
23
|
+
data.map { |d| convert_to_entity(d, &block) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def convert_to_entity(data)
|
27
|
+
attributes = data.each_with_object({}) do |(key, value), hash|
|
28
|
+
hash[transform_key(key)] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
yield(attributes)
|
32
|
+
end
|
33
|
+
|
34
|
+
def transform_key(key)
|
35
|
+
underscore(key.to_s).to_sym
|
36
|
+
end
|
37
|
+
|
38
|
+
def underscore(camel_cased_word)
|
39
|
+
camel_cased_word.gsub(/::/, '/')
|
40
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
41
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
42
|
+
.tr('-', '_')
|
43
|
+
.downcase
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Models
|
5
|
+
# Hibp::Models::Breach
|
6
|
+
#
|
7
|
+
# Used to construct a "breach" model
|
8
|
+
#
|
9
|
+
# A "breach" is an instance of a system having been compromised by an
|
10
|
+
# attacker and the data disclosed.
|
11
|
+
#
|
12
|
+
# For example, Adobe was a breach, Gawker was a breach etc.
|
13
|
+
#
|
14
|
+
# A "breach" is an incident where data is inadvertently exposed in a vulnerable system,
|
15
|
+
# usually due to insufficient access controls or security weaknesses in the software.
|
16
|
+
#
|
17
|
+
# @see https://haveibeenpwned.com/FAQs
|
18
|
+
#
|
19
|
+
class Breach
|
20
|
+
include Helpers::AttributeAssignment
|
21
|
+
|
22
|
+
attr_accessor :name, :title, :domain, :description, :logo_path,
|
23
|
+
:data_classes, :pwn_count,
|
24
|
+
:breach_date, :added_date, :modified_date,
|
25
|
+
:is_verified, :is_fabricated, :is_sensitive, :is_retired, :is_spam_list
|
26
|
+
|
27
|
+
# @param attributes [Hash] - Attributes in a hash
|
28
|
+
#
|
29
|
+
# @option attributes [String] :name -
|
30
|
+
# A name representing the breach which is unique across all other breaches.
|
31
|
+
# This value never changes and may be used to name dependent assets
|
32
|
+
# (such as images) but should not be shown directly to end users
|
33
|
+
# (see the "title" attribute instead).
|
34
|
+
#
|
35
|
+
# @option attributes [String] :title -
|
36
|
+
# A descriptive title for the breach suitable for displaying to end users.
|
37
|
+
# It's unique across all breaches but individual values may change in the future
|
38
|
+
# (i.e. if another breach occurs against an organisation already in the system).
|
39
|
+
# If a stable value is required to reference the breach, refer to the "Name" attribute instead.
|
40
|
+
#
|
41
|
+
# @option attributes [String] :domain -
|
42
|
+
# The domain of the primary website the breach occurred on.
|
43
|
+
# This may be used for identifying other assets external systems may have for the site.
|
44
|
+
#
|
45
|
+
# @option attributes [Date] :breach_date -
|
46
|
+
# The date (with no time) the breach originally occurred on in ISO 8601 format.
|
47
|
+
# This is not always accurate — frequently breaches are discovered and reported long after the original incident.
|
48
|
+
# Use this attribute as a guide only.
|
49
|
+
#
|
50
|
+
# @option attributes [DateTime] :added_date -
|
51
|
+
# The date and time (precision to the minute) the breach was added to the system in ISO 8601 format.
|
52
|
+
#
|
53
|
+
# @option attributes [DateTime] :modified_date -
|
54
|
+
# The date and time (precision to the minute) the breach was modified in ISO 8601 format.
|
55
|
+
# This will only differ from the AddedDate attribute if other attributes
|
56
|
+
# represented here are changed or data in the breach itself is changed
|
57
|
+
# (i.e. additional data is identified and loaded).
|
58
|
+
# It is always either equal to or greater then the AddedDate attribute, never less than.
|
59
|
+
#
|
60
|
+
# @option attributes [Integer] :pwn_count -
|
61
|
+
# The total number of accounts loaded into the system.
|
62
|
+
# This is usually less than the total number reported by the media due to
|
63
|
+
# duplication or other data integrity issues in the source data.
|
64
|
+
#
|
65
|
+
# @option attributes [String] :description -
|
66
|
+
# Contains an overview of the breach represented in HTML markup.
|
67
|
+
# The description may include markup such as emphasis and strong tags as well as hyperlinks.
|
68
|
+
#
|
69
|
+
# @option attributes [Array<String>] :data_classes -
|
70
|
+
# This attribute describes the nature of the data compromised in the breach and
|
71
|
+
# contains an alphabetically ordered string array of impacted data classes.
|
72
|
+
#
|
73
|
+
# @option attributes [Boolean] :is_verified -
|
74
|
+
# Indicates that the breach is considered unverified.
|
75
|
+
# An unverified breach may not have been hacked from the indicated website.
|
76
|
+
# An unverified breach is still loaded into HIBP when there's
|
77
|
+
# sufficient confidence that a significant portion of the data is legitimate.
|
78
|
+
#
|
79
|
+
# @option attributes [Boolean] :is_fabricated -
|
80
|
+
# Indicates that the breach is considered fabricated.
|
81
|
+
# A fabricated breach is unlikely to have been hacked from the
|
82
|
+
# indicated website and usually contains a large amount of manufactured data.
|
83
|
+
# However, it still contains legitimate email addresses and asserts that
|
84
|
+
# the account owners were compromised in the alleged breach.
|
85
|
+
#
|
86
|
+
# @option attributes [Boolean] :is_sensitive -
|
87
|
+
# Indicates if the breach is considered sensitive.
|
88
|
+
# The public API will not return any accounts for a breach flagged as sensitive.
|
89
|
+
#
|
90
|
+
# @option attributes [Boolean] :is_retired -
|
91
|
+
# Indicates if the breach has been retired.
|
92
|
+
# This data has been permanently removed and will not be returned by the API.
|
93
|
+
#
|
94
|
+
# @option attributes [Boolean] :is_spam_list -
|
95
|
+
# Indicates if the breach is considered a spam list.
|
96
|
+
# This flag has no impact on any other attributes but
|
97
|
+
# it means that the data has not come as a result of a security compromise.
|
98
|
+
#
|
99
|
+
# @option attributes [String] :logo_path -
|
100
|
+
# A URI that specifies where a logo for the breached service can be found.
|
101
|
+
# Logos are always in PNG format.
|
102
|
+
#
|
103
|
+
# @raise [ArgumentError]
|
104
|
+
# @raise [UnknownAttributeError]
|
105
|
+
#
|
106
|
+
def initialize(attributes)
|
107
|
+
assign_attributes(attributes)
|
108
|
+
end
|
109
|
+
|
110
|
+
%i[verified fabricated sensitive retired spam_list].each do |method|
|
111
|
+
define_method("#{method}?") { public_send("is_#{method}") }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Models
|
5
|
+
# Hibp::Models::Password
|
6
|
+
#
|
7
|
+
# Represents password by the suffix of and
|
8
|
+
# a count of how many times it appears in the data set
|
9
|
+
#
|
10
|
+
class Password
|
11
|
+
include Helpers::AttributeAssignment
|
12
|
+
|
13
|
+
attr_accessor :suffix, :occurrences
|
14
|
+
|
15
|
+
# @param attributes [Hash]
|
16
|
+
#
|
17
|
+
# @option attributes [String] :suffix -
|
18
|
+
# Password suffix(password hash without first five symbols)
|
19
|
+
#
|
20
|
+
# @option attributes [Integer] :occurrences -
|
21
|
+
# Count of how many times suffix appears in the data set
|
22
|
+
#
|
23
|
+
def initialize(attributes)
|
24
|
+
assign_attributes(attributes)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Models
|
5
|
+
# Hibp::Models::Paste
|
6
|
+
#
|
7
|
+
# Used to construct a "paste" model
|
8
|
+
#
|
9
|
+
# A "paste" is information that has been "pasted" to a publicly facing
|
10
|
+
# website designed to share content such as Pastebin.
|
11
|
+
#
|
12
|
+
# These services are favoured by hackers due to the ease of anonymously
|
13
|
+
# sharing information and they're frequently the first place a breach appears.
|
14
|
+
#
|
15
|
+
# @note In the future, these attributes may expand without the API being versioned.
|
16
|
+
#
|
17
|
+
# @see https://haveibeenpwned.com/FAQs
|
18
|
+
#
|
19
|
+
class Paste
|
20
|
+
include Helpers::AttributeAssignment
|
21
|
+
|
22
|
+
attr_accessor :source, :id, :title, :date, :email_count
|
23
|
+
|
24
|
+
# @param attributes [Hash]
|
25
|
+
#
|
26
|
+
# @option attributes [String] :source -
|
27
|
+
# The paste service the record was retrieved from.
|
28
|
+
# Current values are:
|
29
|
+
# - Pastebin
|
30
|
+
# - Pastie
|
31
|
+
# - Slexy
|
32
|
+
# - Ghostbin
|
33
|
+
# - QuickLeak
|
34
|
+
# - JustPaste
|
35
|
+
# - AdHocUrl
|
36
|
+
# - PermanentOptOut
|
37
|
+
# - OptOut
|
38
|
+
#
|
39
|
+
# @option attributes [String] :id -
|
40
|
+
# The ID of the paste as it was given at the source service.
|
41
|
+
# Combined with the "Source" attribute, this can be used to resolve the URL of the paste.
|
42
|
+
#
|
43
|
+
# @option attributes [String] :title -
|
44
|
+
# The title of the paste as observed on the source site.
|
45
|
+
# This may be null.
|
46
|
+
#
|
47
|
+
# @option attributes [String] :date -
|
48
|
+
# The date and time (precision to the second) that the paste was posted.
|
49
|
+
# This is taken directly from the paste site when this information is
|
50
|
+
# available but may be null if no date is published.
|
51
|
+
#
|
52
|
+
# @option attributes [Integer] :email_count -
|
53
|
+
# The number of emails that were found when processing the paste.
|
54
|
+
# Emails are extracted by using the regular expression:
|
55
|
+
# \b+(?!^.{256})[a-zA-Z0-9\.\-_\+]+@[a-zA-Z0-9\.\-_]+\.[a-zA-Z]+\b
|
56
|
+
#
|
57
|
+
def initialize(attributes)
|
58
|
+
assign_attributes(attributes)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Parsers
|
5
|
+
# Hibp::Parsers::Breach
|
6
|
+
#
|
7
|
+
# Used to convert raw API response data to the breach entity or
|
8
|
+
# array of the entities in case if response data contains multiple breaches
|
9
|
+
#
|
10
|
+
class Breach < Json
|
11
|
+
|
12
|
+
# Convert raw data to the breach entity
|
13
|
+
#
|
14
|
+
# @param response [Faraday::Response] -
|
15
|
+
# Response that contains raw data for conversion
|
16
|
+
#
|
17
|
+
# @see https://haveibeenpwned.com/API/v3 (The breach model, Sample breach response)
|
18
|
+
#
|
19
|
+
# @return [Array<Hibp::Breach>, Hibp::Breach]
|
20
|
+
#
|
21
|
+
def parse_response(response)
|
22
|
+
super(response) do |attributes|
|
23
|
+
Models::Breach.new(convert_dates!(attributes))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def convert_dates!(attributes)
|
30
|
+
%i[modified_date breach_date added_date].each do |attr_key|
|
31
|
+
next if attributes[attr_key].nil?
|
32
|
+
|
33
|
+
type = attr_key == :breach_date ? Date : Time
|
34
|
+
|
35
|
+
attributes[attr_key] = type.parse(attributes[attr_key])
|
36
|
+
end
|
37
|
+
|
38
|
+
attributes
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Parsers
|
5
|
+
# Hibp::Parsers::Json
|
6
|
+
#
|
7
|
+
# Used to parse API JSON response and transform(convert) this
|
8
|
+
# raw data to the model specified by converter
|
9
|
+
#
|
10
|
+
class Json
|
11
|
+
include Helpers::JsonConversion
|
12
|
+
|
13
|
+
# Parse API response
|
14
|
+
#
|
15
|
+
# @param response [Faraday::Response] -
|
16
|
+
# Response that contains raw data for conversion
|
17
|
+
#
|
18
|
+
# @yield [attributes] - (optional, default: nil)
|
19
|
+
# Converter that used to convert complex
|
20
|
+
# raw response data to the particular model representation.
|
21
|
+
#
|
22
|
+
# @note If block with conversion not set than parser returns raw data
|
23
|
+
# (useful in cases when response returns array of strings)
|
24
|
+
#
|
25
|
+
# @raise [Hibp::ServiceError]
|
26
|
+
#
|
27
|
+
def parse_response(response, &block)
|
28
|
+
return nil if empty_response?(response)
|
29
|
+
|
30
|
+
begin
|
31
|
+
_, body = prepare_response(response)
|
32
|
+
|
33
|
+
block_given? ? convert(body, &block) : body
|
34
|
+
rescue Oj::ParseError
|
35
|
+
raise_error(response.body)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def empty_response?(response)
|
42
|
+
response.body.nil? || response.body.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def prepare_response(response)
|
46
|
+
headers = response.headers
|
47
|
+
body = Oj.load(response.body, symbolize_keys: true)
|
48
|
+
|
49
|
+
[headers, body]
|
50
|
+
end
|
51
|
+
|
52
|
+
def raise_error(payload)
|
53
|
+
error = ServiceError.new(
|
54
|
+
"Unparseable response: #{payload}",
|
55
|
+
title: 'UNPARSEABLE_RESPONSE', status_code: 500
|
56
|
+
)
|
57
|
+
|
58
|
+
raise error
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Parsers
|
5
|
+
# Parsers::Password
|
6
|
+
#
|
7
|
+
# Used to parse raw data and convert it to the password models
|
8
|
+
#
|
9
|
+
class Password
|
10
|
+
ROWS_SPLITTER = "\r\n"
|
11
|
+
ATTRIBUTES_SPLITTER = ':'
|
12
|
+
|
13
|
+
# Convert API response raw data to the passwords models.
|
14
|
+
#
|
15
|
+
# @param response [] -
|
16
|
+
# Contains the suffix of every hash beginning with the specified prefix,
|
17
|
+
# followed by a count of how many times it appears in the data set
|
18
|
+
#
|
19
|
+
# @return [Array<Hibp::Models::Password>]
|
20
|
+
#
|
21
|
+
def parse_response(response)
|
22
|
+
data = response.body
|
23
|
+
|
24
|
+
data.split(ROWS_SPLITTER).map(&method(:convert_to_password))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def convert_to_password(row)
|
30
|
+
suffix, occurrences = row.split(ATTRIBUTES_SPLITTER)
|
31
|
+
|
32
|
+
Models::Password.new(suffix: suffix, occurrences: occurrences.to_i)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hibp
|
4
|
+
module Parsers
|
5
|
+
# Hibp::Parsers::Paste
|
6
|
+
#
|
7
|
+
# Used to convert raw API response data to the array of the paste entities
|
8
|
+
#
|
9
|
+
class Paste < Json
|
10
|
+
# Convert raw data to the pastes entities
|
11
|
+
#
|
12
|
+
# @param response [Faraday::Response] -
|
13
|
+
# Response that contains raw data for conversion
|
14
|
+
#
|
15
|
+
# @see https://haveibeenpwned.com/API/v3 (The paste model, Sample paste response)
|
16
|
+
#
|
17
|
+
# @return [Array<Hibp::Paste>]
|
18
|
+
#
|
19
|
+
def parse_response(response)
|
20
|
+
super(response) { |attributes| Models::Paste.new(attributes) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|