Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing API calls for subscribeddomains and breacheddomain #33

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 88 additions & 4 deletions breach.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)

Expand Down Expand Up @@ -88,12 +87,40 @@ type Breach struct {
LogoPath string `json:"LogoPath"`
}

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"`
}

// BreachOption is an additional option the can be set for the BreachApiClient
type BreachOption func(*BreachAPI)

// APIDate is a date string without time returned by the API represented as time.Time type
type APIDate time.Time

// RenewalTime is a timestamp returned by the API that doesn't have timezone information
type RenewalTime time.Time

// Breaches returns a list of all breaches in the HIBP system
func (b *BreachAPI) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
qp := b.setBreachOpts(options...)
Expand Down Expand Up @@ -173,6 +200,42 @@ func (b *BreachAPI) BreachedAccount(a string, options ...BreachOption) ([]*Breac
return bd, hr, nil
}

// 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
}

// WithDomain sets the domain filter for the breaches API
func WithDomain(d string) BreachOption {
return func(b *BreachAPI) {
Expand All @@ -197,9 +260,8 @@ func WithoutUnverified() BreachOption {

// UnmarshalJSON for the APIDate type converts a give date string into a time.Time type
func (d *APIDate) UnmarshalJSON(s []byte) error {
ds := string(s)
ds = strings.ReplaceAll(ds, `"`, ``)
if ds == "null" {
ds := string(s[1 : len(s)-1])
if ds == "null" || ds == "" {
return nil
}

Expand All @@ -218,6 +280,28 @@ func (d *APIDate) Time() time.Time {
return time.Time(dp)
}

// 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)
}

// setBreachOpts returns a map of default settings and overridden values from different BreachOption
func (b *BreachAPI) setBreachOpts(options ...BreachOption) map[string]string {
qp := map[string]string{
Expand Down
70 changes: 70 additions & 0 deletions breach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,76 @@ func TestBreachAPI_BreachedAccount_WithoutTruncate(t *testing.T) {
}
}

// TestBreachAPI_SubscribedDomains tests the SubscribedDomains() method of the breaches API
func TestBreachAPI_SubscribedDomains(t *testing.T) {
apiKey := os.Getenv("HIBP_API_KEY")
if apiKey == "" {
t.SkipNow()
}
hc := New(WithAPIKey(apiKey), WithRateLimitSleep())

domains, _, err := hc.BreachAPI.SubscribedDomains()
if err != nil {
t.Error(err)
}

if len(domains) < 1 {
t.Log("no subscribed domains found with provided api key")
t.SkipNow()
}

for i, domain := range domains {
t.Run(fmt.Sprintf("checking domain %d", i), func(t *testing.T) {
if domain.DomainName == "" {
t.Error("domain name is missing")
}

if domain.NextSubscriptionRenewal.Time().IsZero() {
t.Error("next subscription renewal is missing")
}
})
}
}

// TestBreachAPI_BreachedDomain tests the BreachedDomain() method of the breaches API
func TestBreachAPI_BreachedDomain(t *testing.T) {
apiKey := os.Getenv("HIBP_API_KEY")
if apiKey == "" {
t.SkipNow()
}
hc := New(WithAPIKey(apiKey), WithRateLimitSleep())

domains, _, err := hc.BreachAPI.SubscribedDomains()
if err != nil {
t.Error(err)
}

if len(domains) < 1 {
t.Log("no subscribed domains found with provided api key")
t.SkipNow()
}

for i, domain := range domains {
t.Run(fmt.Sprintf("checking domain %d", i), func(t *testing.T) {
breaches, _, err := hc.BreachAPI.BreachedDomain(domain.DomainName)
if err != nil {
t.Error(err)
}

if len(breaches) < 1 {
t.Logf("domain %s contains no breaches", domain.DomainName)
t.SkipNow()
}

for alias, list := range breaches {
if l := len(list); l == 0 {
t.Errorf("alias %s contains %d breaches, there should be at least 1", alias, l)
}
}
})
}
}

// TestAPIDate_UnmarshalJSON_Time tests the APIDate type JSON unmarshalling
func TestAPIDate_UnmarshalJSON_Time(t *testing.T) {
type testData struct {
Expand Down
Loading