Skip to content

Commit

Permalink
Merge pull request #3 from wneessen/breaches
Browse files Browse the repository at this point in the history
Added BreachByName() to breaches API
  • Loading branch information
wneessen authored Sep 22, 2021
2 parents a98a5c9 + 0b3734d commit b130145
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 16 deletions.
84 changes: 68 additions & 16 deletions breach.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,48 @@ type ApiDate time.Time

// Breaches returns a list of all breaches in the HIBP system
func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
queryParms := map[string]string{
"truncateResponse": "true",
"includeUnverified": "true",
}
queryParams := b.setBreachOpts(options...)
apiUrl := fmt.Sprintf("%s/breaches", BaseUrl)

for _, opt := range options {
opt(b)
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()
}()

if b.domain != "" {
queryParms["domain"] = b.domain
hb, err := io.ReadAll(hr.Body)
if err != nil {
return nil, hr, err
}

if b.disableTrunc {
queryParms["truncateResponse"] = "false"
var breachList []*Breach
if err := json.Unmarshal(hb, &breachList); err != nil {
return nil, hr, err
}

if b.noUnverified {
queryParms["includeUnverified"] = "false"
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")
}

hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParms)
apiUrl := fmt.Sprintf("%s/breach/%s", BaseUrl, n)

hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl, queryParams)
if err != nil {
return nil, nil, err
}
Expand All @@ -139,12 +158,12 @@ func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response
return nil, hr, err
}

var breachList []*Breach
if err := json.Unmarshal(hb, &breachList); err != nil {
var breachDetails *Breach
if err := json.Unmarshal(hb, &breachDetails); err != nil {
return nil, hr, err
}

return breachList, hr, nil
return breachDetails, hr, nil
}

// WithDomain sets the domain filter for the breaches API
Expand All @@ -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
func (d *ApiDate) UnmarshalJSON(s []byte) error {
ds := string(s)
Expand All @@ -182,3 +208,29 @@ func (d *ApiDate) UnmarshalJSON(s []byte) error {
func (d ApiDate) Time() time.Time {
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
}
71 changes: 71 additions & 0 deletions breach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
29 changes: 29 additions & 0 deletions examples/breaches/all-breaches-nounverified.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
24 changes: 24 additions & 0 deletions examples/breaches/all-breaches.go
Original file line number Diff line number Diff line change
@@ -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"))
}
}
}
25 changes: 25 additions & 0 deletions examples/breaches/breach-by-name.go
Original file line number Diff line number Diff line change
@@ -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())

}
}

0 comments on commit b130145

Please sign in to comment.