Added BreachByName() to breaches API

This commit is contained in:
Winni Neessen 2021-09-22 09:46:18 +02:00
parent e670c59a8d
commit 0b3734da33
Signed by: wneessen
GPG key ID: 385AC9889632126E
5 changed files with 222 additions and 21 deletions

View file

@ -97,29 +97,10 @@ type ApiDate time.Time
// Breaches returns a list of all breaches in the HIBP system // Breaches returns a list of all breaches in the HIBP system
func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) { func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
queryParms := map[string]string{ queryParams := b.setBreachOpts(options...)
"truncateResponse": "true",
"includeUnverified": "true",
}
apiUrl := fmt.Sprintf("%s/breaches", BaseUrl) apiUrl := fmt.Sprintf("%s/breaches", BaseUrl)
for _, opt := range options { hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
opt(b)
}
if b.domain != "" {
queryParms["domain"] = b.domain
}
if b.disableTrunc {
queryParms["truncateResponse"] = "false"
}
if b.noUnverified {
queryParms["includeUnverified"] = "false"
}
hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParms)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -147,6 +128,44 @@ func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response
return breachList, hr, nil return breachList, hr, nil
} }
// BreachByName returns a single breached site based on its name
func (b *BreachApi) BreachByName(n string, options ...BreachOption) (*Breach, *http.Response, error) {
queryParams := b.setBreachOpts(options...)
if n == "" {
return nil, nil, fmt.Errorf("no breach name given")
}
apiUrl := fmt.Sprintf("%s/breach/%s", BaseUrl, n)
hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
if err != nil {
return nil, nil, err
}
hr, err := b.hibp.hc.Do(hreq)
if err != nil {
return nil, hr, err
}
if hr.StatusCode != 200 {
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
}
defer func() {
_ = hr.Body.Close()
}()
hb, err := io.ReadAll(hr.Body)
if err != nil {
return nil, hr, err
}
var breachDetails *Breach
if err := json.Unmarshal(hb, &breachDetails); err != nil {
return nil, hr, err
}
return breachDetails, hr, nil
}
// WithDomain sets the domain filter for the breaches API // WithDomain sets the domain filter for the breaches API
func WithDomain(d string) BreachOption { func WithDomain(d string) BreachOption {
return func(b *BreachApi) { return func(b *BreachApi) {
@ -161,6 +180,13 @@ func WithoutTruncate() BreachOption {
} }
} }
// WithoutUnverified suppress unverified breaches from the query
func WithoutUnverified() BreachOption {
return func(b *BreachApi) {
b.noUnverified = true
}
}
// UnmarshalJSON for the ApiDate type converts a give date string into a time.Time type // UnmarshalJSON for the ApiDate type converts a give date string into a time.Time type
func (d *ApiDate) UnmarshalJSON(s []byte) error { func (d *ApiDate) UnmarshalJSON(s []byte) error {
ds := string(s) ds := string(s)
@ -182,3 +208,29 @@ func (d *ApiDate) UnmarshalJSON(s []byte) error {
func (d ApiDate) Time() time.Time { func (d ApiDate) Time() time.Time {
return time.Time(d) return time.Time(d)
} }
// setBreachOpts returns a map of default settings and overridden values from different BreachOption
func (b *BreachApi) setBreachOpts(options ...BreachOption) map[string]string {
queryParams := map[string]string{
"truncateResponse": "true",
"includeUnverified": "true",
}
for _, opt := range options {
opt(b)
}
if b.domain != "" {
queryParams["domain"] = b.domain
}
if b.disableTrunc {
queryParams["truncateResponse"] = "false"
}
if b.noUnverified {
queryParams["includeUnverified"] = "false"
}
return queryParams
}

View file

@ -62,3 +62,74 @@ func TestBreachesWithDomain(t *testing.T) {
}) })
} }
} }
// TestBreachesWithoutUnverified tests the Breaches() method of the breaches API with the unverified parameter
func TestBreachesWithoutUnverified(t *testing.T) {
testTable := []struct {
testName string
domain string
isBreached bool
isVerified bool
}{
{"adobe.com is breached and verified", "adobe.com", true, true},
{"parapa.mail.ru is breached and verified", "parapa.mail.ru", true, true},
{"xiaomi.cn is breached but not verified", "xiaomi.cn", true, false},
}
hc := New()
if hc == nil {
t.Error("failed to create HIBP client")
return
}
for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) {
breachList, _, err := hc.BreachApi.Breaches(WithDomain(tc.domain), WithoutUnverified())
if err != nil {
t.Error(err)
}
if breachList == nil && tc.isVerified && tc.isBreached {
t.Errorf("domain %s is expected to be breached, but returned 0 results.",
tc.domain)
}
})
}
}
// TestBreachByName tests the BreachByName() method of the breaches API for a specific domain
func TestBreachByName(t *testing.T) {
testTable := []struct {
testName string
breachName string
isBreached bool
shouldFail bool
}{
{"Adobe is a known breach", "Adobe", true, false},
{"Example is not a known breach", "Example", false, true},
}
hc := New()
if hc == nil {
t.Error("failed to create HIBP client")
return
}
for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) {
breachDetails, _, err := hc.BreachApi.BreachByName(tc.breachName)
if err != nil && !tc.shouldFail {
t.Error(err)
}
if breachDetails == nil && tc.isBreached {
t.Errorf("breach with the name %q is expected to be breached, but returned 0 results.",
tc.breachName)
}
if breachDetails != nil && !tc.isBreached {
t.Errorf("breach with the name %q is expected to be not breached, but returned breach details.",
tc.breachName)
}
})
}
}

View file

@ -0,0 +1,29 @@
package main
import (
"fmt"
hibp "github.com/wneessen/go-hibp"
)
func main() {
hc := hibp.New()
if hc == nil {
panic("failed to create HIBP client")
}
bl, _, err := hc.BreachApi.Breaches()
if err != nil {
panic(err)
}
if bl != nil && len(bl) != 0 {
fmt.Printf("Found %d breaches total.\n", len(bl))
}
bl, _, err = hc.BreachApi.Breaches(hibp.WithoutUnverified())
if err != nil {
panic(err)
}
if bl != nil && len(bl) != 0 {
fmt.Printf("Found %d verified breaches total.\n", len(bl))
}
}

View file

@ -0,0 +1,24 @@
package main
import (
"fmt"
hibp "github.com/wneessen/go-hibp"
)
func main() {
hc := hibp.New()
if hc == nil {
panic("failed to create HIBP client")
}
bl, _, err := hc.BreachApi.Breaches()
if err != nil {
panic(err)
}
if bl != nil && len(bl) != 0 {
for _, b := range bl {
fmt.Printf("Found breach:\n\tName: %s\n\tDomain: %s\n\tBreach date: %s\n\n",
b.Name, b.Domain, b.BreachDate.Time().Format("Mon, 2. January 2006"))
}
}
}

View file

@ -0,0 +1,25 @@
package main
import (
"fmt"
hibp "github.com/wneessen/go-hibp"
)
func main() {
hc := hibp.New()
if hc == nil {
panic("failed to create HIBP client")
}
bd, _, err := hc.BreachApi.BreachByName("Adobe")
if err != nil {
panic(err)
}
if bd != nil {
fmt.Println("Details of the 'Adobe' breach:")
fmt.Printf("\tDomain: %s\n", bd.Domain)
fmt.Printf("\tBreach date: %s\n", bd.BreachDate.Time().Format("2006-01-02"))
fmt.Printf("\tAdded to HIBP: %s\n", bd.AddedDate.String())
}
}