2021-09-21 18:21:23 +02:00
|
|
|
package hibp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2022-10-29 15:32:12 +02:00
|
|
|
// BreachAPI is a HIBP breaches API client
|
|
|
|
type BreachAPI struct {
|
2021-09-21 19:46:48 +02:00
|
|
|
hibp *Client // References back to the parent HIBP client
|
2021-09-21 18:21:23 +02:00
|
|
|
|
2021-09-21 19:46:48 +02:00
|
|
|
domain string // Filter for a specific breach domain
|
|
|
|
disableTrunc bool // Controls the truncateResponse parameter for the breaches API (defaults to false)
|
|
|
|
noUnverified bool // Controls the includeUnverified parameter for the breaches API (defaults to false)
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Breach represents a JSON response structure of the breaches API
|
|
|
|
type Breach struct {
|
|
|
|
// Name is a pascal-cased name representing the breach which is unique across all other breaches.
|
|
|
|
// This value never changes and may be used to name dependent assets (such as images) but should not
|
|
|
|
// be shown directly to end users (see the "Title" attribute instead)
|
|
|
|
Name string `json:"Name"`
|
|
|
|
|
|
|
|
// Title is a descriptive title for the breach suitable for displaying to end users. It's unique across
|
|
|
|
// all breaches but individual values may change in the future (i.e. if another breach occurs against
|
|
|
|
// an organisation already in the system). If a stable value is required to reference the breach,
|
|
|
|
// refer to the "Name" attribute instead
|
|
|
|
Title string `json:"Title"`
|
|
|
|
|
|
|
|
// Domain of the primary website the breach occurred on. This may be used for identifying other
|
|
|
|
// assets external systems may have for the site
|
|
|
|
Domain string `json:"Domain"`
|
|
|
|
|
|
|
|
// BreachDate is the date (with no time) the breach originally occurred on in ISO 8601 format. This is not
|
|
|
|
// always accurate — frequently breaches are discovered and reported long after the original incident. Use
|
|
|
|
// this attribute as a guide only
|
2022-10-29 15:32:12 +02:00
|
|
|
BreachDate *APIDate `json:"BreachDate,omitempty"`
|
2021-09-21 18:21:23 +02:00
|
|
|
|
|
|
|
// AddedDate represents the date and time (precision to the minute) the breach was added to the system
|
|
|
|
// in ISO 8601 format
|
|
|
|
AddedDate time.Time `json:"AddedDate"`
|
|
|
|
|
|
|
|
// ModifiedDate is the date and time (precision to the minute) the breach was modified in ISO 8601 format.
|
|
|
|
// This will only differ from the AddedDate attribute if other attributes represented here are changed or
|
|
|
|
// data in the breach itself is changed (i.e. additional data is identified and loaded). It is always
|
|
|
|
// either equal to or greater then the AddedDate attribute, never less than
|
|
|
|
ModifiedDate time.Time `json:"ModifiedDate"`
|
|
|
|
|
|
|
|
// PwnCount is the total number of accounts loaded into the system. This is usually less than the total
|
|
|
|
// number reported by the media due to duplication or other data integrity issues in the source data
|
|
|
|
PwnCount int `json:"PwnCount"`
|
|
|
|
|
|
|
|
// Description contains an overview of the breach represented in HTML markup. The description may include
|
|
|
|
// markup such as emphasis and strong tags as well as hyperlinks
|
|
|
|
Description string `json:"Description"`
|
|
|
|
|
|
|
|
// DataClasses describes the nature of the data compromised in the breach and contains an alphabetically ordered
|
|
|
|
// string array of impacted data classes
|
|
|
|
DataClasses []string `json:"DataClasses"`
|
2021-09-21 19:46:48 +02:00
|
|
|
|
|
|
|
// IsVerified indicates that the breach is considered unverified. An unverified breach may not have
|
|
|
|
// been hacked from the indicated website. An unverified breach is still loaded into HIBP when there's
|
|
|
|
// sufficient confidence that a significant portion of the data is legitimate
|
|
|
|
IsVerified bool `json:"IsVerified"`
|
|
|
|
|
|
|
|
// IsFabricated indicates that the breach is considered fabricated. A fabricated breach is unlikely
|
|
|
|
// to have been hacked from the indicated website and usually contains a large amount of manufactured
|
|
|
|
// data. However, it still contains legitimate email addresses and asserts that the account owners
|
|
|
|
// were compromised in the alleged breach
|
|
|
|
IsFabricated bool `json:"IsFabricated"`
|
|
|
|
|
|
|
|
// IsSensitive indicates if the breach is considered sensitive. The public API will not return any
|
|
|
|
// accounts for a breach flagged as sensitive
|
|
|
|
IsSensitive bool `json:"IsSensitive"`
|
|
|
|
|
|
|
|
// IsRetired indicates if the breach has been retired. This data has been permanently removed and
|
|
|
|
// will not be returned by the API
|
|
|
|
IsRetired bool `json:"IsRetired"`
|
|
|
|
|
|
|
|
// IsSpamList indicates
|
|
|
|
IsSpamList bool `json:"IsSpamList"`
|
|
|
|
|
|
|
|
// LogoPath represents a URI that specifies where a logo for the breached service can be found.
|
|
|
|
// Logos are always in PNG format
|
|
|
|
LogoPath string `json:"LogoPath"`
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
2024-04-03 07:31:21 +02:00
|
|
|
type SubscribedDomains struct {
|
|
|
|
// DomainName is the full domain name that has been successfully verified.
|
|
|
|
DomainName string `json:"DomainName"`
|
|
|
|
|
|
|
|
// PwnCount is the total number of breached email addresses found on the domain at last search
|
|
|
|
// (will be null if no searches yet performed).
|
|
|
|
PwnCount *int `json:"PwnCount"`
|
|
|
|
|
|
|
|
// PwnCountExcludingSpamLists is the number of breached email addresses found on the domain
|
|
|
|
// at last search, excluding any breaches flagged as a spam list (will be null if no
|
|
|
|
// searches yet performed).
|
|
|
|
PwnCountExcludingSpamLists *int `json:"PwnCountExcludingSpamLists"`
|
|
|
|
|
|
|
|
// The total number of breached email addresses found on the domain when the current
|
|
|
|
// subscription was taken out (will be null if no searches yet performed). This number
|
|
|
|
// ensures the domain remains searchable throughout the subscription period even if the
|
|
|
|
// volume of breached accounts grows beyond the subscription's scope.
|
|
|
|
PwnCountExcludingSpamListsAtLastSubscriptionRenewal *int `json:"PwnCountExcludingSpamListsAtLastSubscriptionRenewal"`
|
|
|
|
|
|
|
|
// The date and time the current subscription ends in ISO 8601 format. The
|
|
|
|
// PwnCountExcludingSpamListsAtLastSubscriptionRenewal value is locked in until this time (will
|
|
|
|
// be null if there have been no subscriptions).
|
|
|
|
NextSubscriptionRenewal RenewalTime `json:"NextSubscriptionRenewal"`
|
|
|
|
}
|
|
|
|
|
2021-09-21 18:21:23 +02:00
|
|
|
// BreachOption is an additional option the can be set for the BreachApiClient
|
2022-10-29 15:32:12 +02:00
|
|
|
type BreachOption func(*BreachAPI)
|
2021-09-21 18:21:23 +02:00
|
|
|
|
2022-10-29 15:32:12 +02:00
|
|
|
// APIDate is a date string without time returned by the API represented as time.Time type
|
|
|
|
type APIDate time.Time
|
2021-09-21 18:21:23 +02:00
|
|
|
|
2024-04-03 07:31:21 +02:00
|
|
|
// RenewalTime is a timestamp returned by the API that doesn't have timezone information
|
|
|
|
type RenewalTime time.Time
|
|
|
|
|
2021-09-21 18:21:23 +02:00
|
|
|
// Breaches returns a list of all breaches in the HIBP system
|
2022-10-29 15:32:12 +02:00
|
|
|
func (b *BreachAPI) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp := b.setBreachOpts(options...)
|
|
|
|
au := fmt.Sprintf("%s/breaches", BaseURL)
|
2021-09-21 19:46:48 +02:00
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, qp)
|
2021-09-22 09:46:18 +02:00
|
|
|
if err != nil {
|
2022-12-22 11:55:06 +01:00
|
|
|
return nil, hr, err
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
2021-09-21 19:46:48 +02:00
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
var bl []*Breach
|
|
|
|
if err := json.Unmarshal(hb, &bl); err != nil {
|
2021-09-22 09:46:18 +02:00
|
|
|
return nil, hr, err
|
2021-09-21 19:46:48 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
return bl, hr, nil
|
2021-09-22 09:46:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// BreachByName returns a single breached site based on its name
|
2022-10-29 15:32:12 +02:00
|
|
|
func (b *BreachAPI) BreachByName(n string, options ...BreachOption) (*Breach, *http.Response, error) {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp := b.setBreachOpts(options...)
|
2021-09-22 09:46:18 +02:00
|
|
|
|
|
|
|
if n == "" {
|
2022-12-22 11:55:06 +01:00
|
|
|
return nil, nil, ErrNoName
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
au := fmt.Sprintf("%s/breach/%s", BaseURL, n)
|
|
|
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, qp)
|
2021-09-21 18:21:23 +02:00
|
|
|
if err != nil {
|
2022-12-22 11:55:06 +01:00
|
|
|
return nil, hr, err
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
2021-09-22 13:59:22 +02:00
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
var bd *Breach
|
|
|
|
if err := json.Unmarshal(hb, &bd); err != nil {
|
2021-09-22 13:59:22 +02:00
|
|
|
return nil, hr, err
|
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
return bd, hr, nil
|
2021-09-22 13:59:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// DataClasses are attribute of a record compromised in a breach. This method returns a list of strings
|
|
|
|
// with all registered data classes known to HIBP
|
2022-10-29 15:32:12 +02:00
|
|
|
func (b *BreachAPI) DataClasses() ([]string, *http.Response, error) {
|
2022-12-22 11:55:06 +01:00
|
|
|
au := fmt.Sprintf("%s/dataclasses", BaseURL)
|
|
|
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, nil)
|
2021-09-21 18:21:23 +02:00
|
|
|
if err != nil {
|
2022-12-22 11:55:06 +01:00
|
|
|
return nil, hr, err
|
2021-09-22 13:59:22 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
var dc []string
|
|
|
|
if err := json.Unmarshal(hb, &dc); err != nil {
|
2021-09-21 18:21:23 +02:00
|
|
|
return nil, hr, err
|
|
|
|
}
|
2021-09-22 13:59:22 +02:00
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
return dc, hr, nil
|
2021-09-22 13:59:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// BreachedAccount returns a single breached site based on its name
|
2022-10-29 15:32:12 +02:00
|
|
|
func (b *BreachAPI) BreachedAccount(a string, options ...BreachOption) ([]*Breach, *http.Response, error) {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp := b.setBreachOpts(options...)
|
2021-09-22 13:59:22 +02:00
|
|
|
|
|
|
|
if a == "" {
|
2022-12-22 11:55:06 +01:00
|
|
|
return nil, nil, ErrNoAccountID
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
au := fmt.Sprintf("%s/breachedaccount/%s", BaseURL, a)
|
|
|
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, qp)
|
2021-09-21 18:21:23 +02:00
|
|
|
if err != nil {
|
2022-12-22 11:55:06 +01:00
|
|
|
return nil, hr, err
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
var bd []*Breach
|
|
|
|
if err := json.Unmarshal(hb, &bd); err != nil {
|
2021-09-21 18:21:23 +02:00
|
|
|
return nil, hr, err
|
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
return bd, hr, nil
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
2024-04-03 07:31:21 +02:00
|
|
|
// SubscribedDomains returns domains that have been successfully added to the domain
|
|
|
|
// search dashboard after verifying control are returned via this API. This is an
|
|
|
|
// authenticated API requiring an HIBP API key which will then return all domains associated with that key.
|
|
|
|
func (b *BreachAPI) SubscribedDomains() ([]SubscribedDomains, *http.Response, error) {
|
|
|
|
au := fmt.Sprintf("%s/subscribeddomains", BaseURL)
|
|
|
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, hr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var bd []SubscribedDomains
|
|
|
|
if err := json.Unmarshal(hb, &bd); err != nil {
|
|
|
|
return nil, hr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return bd, hr, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// BreachedDomain returns all email addresses on a given domain and the breaches they've appeared
|
|
|
|
// in can be returned via the domain search API. Only domains that have been successfully added
|
|
|
|
// to the domain search dashboard after verifying control can be searched.
|
|
|
|
func (b *BreachAPI) BreachedDomain(domain string) (map[string][]string, *http.Response, error) {
|
|
|
|
au := fmt.Sprintf("%s/breacheddomain/%s", BaseURL, domain)
|
|
|
|
hb, hr, err := b.hibp.HTTPResBody(http.MethodGet, au, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, hr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var bd map[string][]string
|
|
|
|
if err := json.Unmarshal(hb, &bd); err != nil {
|
|
|
|
return nil, hr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return bd, hr, nil
|
|
|
|
}
|
|
|
|
|
2021-09-21 18:21:23 +02:00
|
|
|
// WithDomain sets the domain filter for the breaches API
|
|
|
|
func WithDomain(d string) BreachOption {
|
2022-10-29 15:32:12 +02:00
|
|
|
return func(b *BreachAPI) {
|
2021-09-21 18:21:23 +02:00
|
|
|
b.domain = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-21 19:46:48 +02:00
|
|
|
// WithoutTruncate disables the truncateResponse parameter in the breaches API
|
2021-09-22 13:59:22 +02:00
|
|
|
// This option only influences the BreachedAccount method
|
2021-09-21 19:46:48 +02:00
|
|
|
func WithoutTruncate() BreachOption {
|
2022-10-29 15:32:12 +02:00
|
|
|
return func(b *BreachAPI) {
|
2021-09-21 19:46:48 +02:00
|
|
|
b.disableTrunc = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-22 09:46:18 +02:00
|
|
|
// WithoutUnverified suppress unverified breaches from the query
|
|
|
|
func WithoutUnverified() BreachOption {
|
2022-10-29 15:32:12 +02:00
|
|
|
return func(b *BreachAPI) {
|
2021-09-22 09:46:18 +02:00
|
|
|
b.noUnverified = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-29 15:32:12 +02:00
|
|
|
// UnmarshalJSON for the APIDate type converts a give date string into a time.Time type
|
|
|
|
func (d *APIDate) UnmarshalJSON(s []byte) error {
|
2024-04-03 07:31:21 +02:00
|
|
|
ds := string(s[1 : len(s)-1])
|
|
|
|
if ds == "null" || ds == "" {
|
2021-09-21 18:21:23 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
pd, err := time.Parse("2006-01-02", ds)
|
|
|
|
if err != nil {
|
2022-12-22 11:55:06 +01:00
|
|
|
return fmt.Errorf("convert API date string to time.Time type: %w", err)
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
*(*time.Time)(d) = pd
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-10-29 15:32:12 +02:00
|
|
|
// Time adds a Time() method to the APIDate converted time.Time type
|
|
|
|
func (d *APIDate) Time() time.Time {
|
2022-10-01 16:05:50 +02:00
|
|
|
dp := *d
|
|
|
|
return time.Time(dp)
|
2021-09-21 18:21:23 +02:00
|
|
|
}
|
2021-09-22 09:46:18 +02:00
|
|
|
|
2024-04-03 07:31:21 +02:00
|
|
|
// UnmarshalJSON for the RenewalTime type converts a give date string into a time.Time type
|
|
|
|
func (d *RenewalTime) UnmarshalJSON(s []byte) error {
|
|
|
|
ds := string(s[1 : len(s)-1])
|
|
|
|
if ds == "null" || ds == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
pd, err := time.Parse("2006-01-02T15:04:05", ds)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("convert API date string to time.Time type: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
*(*time.Time)(d) = pd
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Time adds a Time() method to the RenewalTime converted time.Time type
|
|
|
|
func (d *RenewalTime) Time() time.Time {
|
|
|
|
dp := *d
|
|
|
|
return time.Time(dp)
|
|
|
|
}
|
|
|
|
|
2021-09-22 09:46:18 +02:00
|
|
|
// setBreachOpts returns a map of default settings and overridden values from different BreachOption
|
2022-10-29 15:32:12 +02:00
|
|
|
func (b *BreachAPI) setBreachOpts(options ...BreachOption) map[string]string {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp := map[string]string{
|
2021-09-22 09:46:18 +02:00
|
|
|
"truncateResponse": "true",
|
|
|
|
"includeUnverified": "true",
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, opt := range options {
|
2021-09-22 13:59:22 +02:00
|
|
|
if opt == nil {
|
|
|
|
continue
|
|
|
|
}
|
2021-09-22 09:46:18 +02:00
|
|
|
opt(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
if b.domain != "" {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp["domain"] = b.domain
|
2021-09-22 09:46:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if b.disableTrunc {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp["truncateResponse"] = "false"
|
2021-09-22 09:46:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if b.noUnverified {
|
2022-12-22 11:55:06 +01:00
|
|
|
qp["includeUnverified"] = "false"
|
2021-09-22 09:46:18 +02:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:55:06 +01:00
|
|
|
return qp
|
2021-09-22 09:46:18 +02:00
|
|
|
}
|