hibp-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|