Added breaches

This commit is contained in:
Winni Neessen 2021-09-21 18:21:23 +02:00
parent 15e1aed098
commit 277b45ec8d
Signed by: wneessen
GPG key ID: 385AC9889632126E
6 changed files with 214 additions and 14 deletions

137
breach.go Normal file
View file

@ -0,0 +1,137 @@
package hibp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// BreachApi is a HIBP breaches API client
type BreachApi struct {
hibp *Client
// Options
domain string // Filter for a specific breach domain
}
// 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
BreachDate *ApiDate `json:"BreachDate,omitempty"`
// 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"`
}
// 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
// Breaches returns a list of all breaches in the HIBP system
func (b *BreachApi) Breaches(options ...BreachOption) ([]*Breach, *http.Response, error) {
for _, opt := range options {
opt(b)
}
apiUrl := fmt.Sprintf("%s/breaches", BaseUrl)
if b.domain != "" {
apiUrl = fmt.Sprintf("%s?domain=%s", apiUrl, b.domain)
}
hreq, err := b.hibp.HttpReq(http.MethodGet, apiUrl)
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 breachList []*Breach
if err := json.Unmarshal(hb, &breachList); err != nil {
return nil, hr, err
}
return breachList, hr, nil
}
// WithDomain sets the domain filter for the breaches API
func WithDomain(d string) BreachOption {
return func(b *BreachApi) {
b.domain = d
}
}
// 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" {
return nil
}
pd, err := time.Parse("2006-01-02", ds)
if err != nil {
return fmt.Errorf("failed to convert API date string to time.Time type: %s", err)
}
*(*time.Time)(d) = pd
return nil
}
// Time adds a Time() method to the ApiDate converted time.Time type
func (d ApiDate) Time() time.Time {
return time.Time(d)
}

26
breach_test.go Normal file
View file

@ -0,0 +1,26 @@
package hibp
import (
"fmt"
"os"
"testing"
)
// TestNew tests the New() function
func TestBreach(t *testing.T) {
hc := New(WithApiKey(os.Getenv("HIBP_API_KE")))
if hc == nil {
t.Errorf("hibp client creation failed")
}
foo, _, err := hc.BreachApi.Breaches(WithDomain("adobe.com"))
if err != nil {
t.Error(err)
}
for _, b := range foo {
fmt.Printf("%+v", *b)
if b.BreachDate != nil {
fmt.Printf("%+v", b.BreachDate.Time().String())
}
break
}
}

25
hibp.go
View file

@ -11,13 +11,19 @@ import (
// Version represents the version of this package // Version represents the version of this package
const Version = "0.1.1" const Version = "0.1.1"
// BaseUrl is the base URL for the majority of API calls
const BaseUrl = "https://haveibeenpwned.com/api/v3"
// Client is the HIBP client object // Client is the HIBP client object
type Client struct { type Client struct {
hc *http.Client // HTTP client to perform the API requests hc *http.Client // HTTP client to perform the API requests
to time.Duration // HTTP client timeout to time.Duration // HTTP client timeout
ak string // HIBP API key ak string // HIBP API key
PwnedPassword *PwnedPassword // Reference to the PwnedPassword API PwnedPassApi *PwnedPassApi // Reference to the PwnedPassApi API
PwnedPassApiOpts *PwnedPasswordOptions // Additional options for the PwnedPassApi API
BreachApi *BreachApi // Reference to the BreachApi API
} }
// Option is a function that is used for grouping of Client options. // Option is a function that is used for grouping of Client options.
@ -29,6 +35,7 @@ func New(options ...Option) *Client {
// Set defaults // Set defaults
c.to = time.Second * 5 c.to = time.Second * 5
c.PwnedPassApiOpts = &PwnedPasswordOptions{}
// Set additional options // Set additional options
for _, opt := range options { for _, opt := range options {
@ -39,7 +46,8 @@ func New(options ...Option) *Client {
c.hc = httpClient(c.to) c.hc = httpClient(c.to)
// Associate the different HIBP service APIs with the Client // Associate the different HIBP service APIs with the Client
c.PwnedPassword = &PwnedPassword{hc: c} c.PwnedPassApi = &PwnedPassApi{hibp: c}
c.BreachApi = &BreachApi{hibp: c}
return c return c
} }
@ -58,6 +66,13 @@ func WithApiKey(k string) Option {
} }
} }
// WithPwnedPadding enables padding-mode for the PwnedPasswords API client
func WithPwnedPadding() Option {
return func(c *Client) {
c.PwnedPassApiOpts.WithPadding = true
}
}
// HttpReq performs an HTTP request to the corresponding API // HttpReq performs an HTTP request to the corresponding API
func (c *Client) HttpReq(m, p string) (*http.Request, error) { func (c *Client) HttpReq(m, p string) (*http.Request, error) {
u, err := url.Parse(p) u, err := url.Parse(p)
@ -73,7 +88,11 @@ func (c *Client) HttpReq(m, p string) (*http.Request, error) {
hr.Header.Set("User-Agent", fmt.Sprintf("go-hibp v%s - https://github.com/wneessen/go-hibp", Version)) hr.Header.Set("User-Agent", fmt.Sprintf("go-hibp v%s - https://github.com/wneessen/go-hibp", Version))
if c.ak != "" { if c.ak != "" {
hr.Header["hibp-api-key"] = []string{c.ak} hr.Header.Set("hibp-api-key", c.ak)
}
if c.PwnedPassApiOpts.WithPadding {
hr.Header.Set("Add-Padding", "true")
} }
return hr, nil return hr, nil

View file

@ -13,7 +13,7 @@ func TestNew(t *testing.T) {
} }
} }
// TestNewWithHttpTimeout tests the New() function // TestNewWithHttpTimeout tests the New() function with the http timeout option
func TestNewWithHttpTimeout(t *testing.T) { func TestNewWithHttpTimeout(t *testing.T) {
hc := New(WithHttpTimeout(time.Second * 10)) hc := New(WithHttpTimeout(time.Second * 10))
if hc == nil { if hc == nil {
@ -25,3 +25,16 @@ func TestNewWithHttpTimeout(t *testing.T) {
time.Second*10, hc.to) time.Second*10, hc.to)
} }
} }
// TestNewWithPwnedPadding tests the New() function with the PwnedPadding option
func TestNewWithPwnedPadding(t *testing.T) {
hc := New(WithPwnedPadding())
if hc == nil {
t.Errorf("hibp client creation failed")
return
}
if !hc.PwnedPassApiOpts.WithPadding {
t.Errorf("hibp client pwned padding option was not set properly. Expected %v, got: %v",
true, hc.PwnedPassApiOpts.WithPadding)
}
}

View file

@ -9,9 +9,9 @@ import (
"strings" "strings"
) )
// PwnedPassword is a HIBP Pwned Passwords API client // PwnedPassApi is a HIBP Pwned Passwords API client
type PwnedPassword struct { type PwnedPassApi struct {
hc *Client hibp *Client
} }
// Match represents a match in the Pwned Passwords API // Match represents a match in the Pwned Passwords API
@ -20,14 +20,19 @@ type Match struct {
Count int64 Count int64
} }
// PwnedPasswordOptions is a struct of additional options for the PP API
type PwnedPasswordOptions struct {
WithPadding bool
}
// CheckPassword checks the Pwned Passwords database against a given password string // CheckPassword checks the Pwned Passwords database against a given password string
func (p *PwnedPassword) CheckPassword(pw string) (*Match, *http.Response, error) { func (p *PwnedPassApi) CheckPassword(pw string) (*Match, *http.Response, error) {
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw))) shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
return p.CheckSHA1(shaSum) return p.CheckSHA1(shaSum)
} }
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password // CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password
func (p *PwnedPassword) CheckSHA1(h string) (*Match, *http.Response, error) { func (p *PwnedPassApi) CheckSHA1(h string) (*Match, *http.Response, error) {
pwMatches, hr, err := p.apiCall(h) pwMatches, hr, err := p.apiCall(h)
if err != nil { if err != nil {
return &Match{}, hr, err return &Match{}, hr, err
@ -44,13 +49,13 @@ func (p *PwnedPassword) CheckSHA1(h string) (*Match, *http.Response, error) {
// apiCall performs the API call to the Pwned Password API endpoint and returns // apiCall performs the API call to the Pwned Password API endpoint and returns
// the http.Response // the http.Response
func (p *PwnedPassword) apiCall(h string) ([]Match, *http.Response, error) { func (p *PwnedPassApi) apiCall(h string) ([]Match, *http.Response, error) {
sh := h[:5] sh := h[:5]
hreq, err := p.hc.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", sh)) hreq, err := p.hibp.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", sh))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
hr, err := p.hc.hc.Do(hreq) hr, err := p.hibp.hc.Do(hreq)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View file

@ -19,7 +19,7 @@ func TestPwnedPasswordString(t *testing.T) {
for _, tc := range testTable { for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
m, _, err := hc.PwnedPassword.CheckPassword(tc.pwString) m, _, err := hc.PwnedPassApi.CheckPassword(tc.pwString)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -50,7 +50,7 @@ func TestPwnedPasswordHash(t *testing.T) {
for _, tc := range testTable { for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
m, _, err := hc.PwnedPassword.CheckSHA1(tc.pwHash) m, _, err := hc.PwnedPassApi.CheckSHA1(tc.pwHash)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }