go(hibp): add AllBreachesForAccount + amend tests
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
* also automatically use hibp api key with direnv and in CI * check for rate-limit * don't interpret rate-limit in tests as a failure * report errors properly
This commit is contained in:
parent
ec7a8ca61a
commit
9fb9cc2735
|
@ -59,6 +59,9 @@ steps:
|
||||||
volumes:
|
volumes:
|
||||||
- name: gopath
|
- name: gopath
|
||||||
path: /go
|
path: /go
|
||||||
|
environment:
|
||||||
|
PCMT_HIBP_API_KEY:
|
||||||
|
from_secret: hibp_api_key
|
||||||
commands:
|
commands:
|
||||||
- go test -cover ./...
|
- go test -cover ./...
|
||||||
|
|
||||||
|
@ -123,6 +126,9 @@ steps:
|
||||||
volumes:
|
volumes:
|
||||||
- name: gopath
|
- name: gopath
|
||||||
path: /go
|
path: /go
|
||||||
|
environment:
|
||||||
|
PCMT_HIBP_API_KEY:
|
||||||
|
from_secret: hibp_api_key
|
||||||
commands:
|
commands:
|
||||||
- go test -cover ./...
|
- go test -cover ./...
|
||||||
|
|
||||||
|
|
1
.envrc
1
.envrc
|
@ -3,3 +3,4 @@ source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cf
|
||||||
use devenv
|
use devenv
|
||||||
|
|
||||||
t=${XDG_RUNTIME_DIR}/secrets/pcmt_gitea_token; test -f ${t} && source_env ${t} && unset ${t}
|
t=${XDG_RUNTIME_DIR}/secrets/pcmt_gitea_token; test -f ${t} && source_env ${t} && unset ${t}
|
||||||
|
t=${XDG_RUNTIME_DIR}/secrets/hibp_api_key; test -f ${t} && source_env ${t} && unset ${t}
|
||||||
|
|
|
@ -5,6 +5,7 @@ package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -29,6 +30,15 @@ type Subscription struct {
|
||||||
DomainSearchMaxBreachedAccounts int
|
DomainSearchMaxBreachedAccounts int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BreachName is used to represent a HIBP breach name object.
|
||||||
|
type BreachName struct {
|
||||||
|
// Name holds the actual breach name, which in HIBP is permanently unique.
|
||||||
|
Name string `json:"Name" validate:"required,Name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BreachNames is a slice of BreachName objects.
|
||||||
|
type BreachNames []BreachName
|
||||||
|
|
||||||
const (
|
const (
|
||||||
api = "https://haveibeenpwned.com/api/v3"
|
api = "https://haveibeenpwned.com/api/v3"
|
||||||
appID = "pcmt (https://git.dotya.ml/mirre-mt/pcmt)"
|
appID = "pcmt (https://git.dotya.ml/mirre-mt/pcmt)"
|
||||||
|
@ -37,11 +47,16 @@ const (
|
||||||
|
|
||||||
headerUA = "user-agent"
|
headerUA = "user-agent"
|
||||||
headerHIBP = "hibp-api-key"
|
headerHIBP = "hibp-api-key"
|
||||||
|
|
||||||
|
authKeyCheckValue = `Your request to the API couldn't be authorised. Check you have the right value in the "hibp-api-key" header, refer to the documentation for more: https://haveibeenpwned.com/API/v3#Authorisation`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
|
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
|
||||||
client = &http.Client{Timeout: reqTmOut}
|
client = &http.Client{Timeout: reqTmOut}
|
||||||
|
|
||||||
|
ErrAuthKeyCheckValue = errors.New(authKeyCheckValue)
|
||||||
|
ErrRateLimited = errors.New("We have been rate limited")
|
||||||
)
|
)
|
||||||
|
|
||||||
// SubscriptionStatus models https://haveibeenpwned.com/API/v3#SubscriptionStatus.
|
// SubscriptionStatus models https://haveibeenpwned.com/API/v3#SubscriptionStatus.
|
||||||
|
@ -78,7 +93,8 @@ func SubscriptionStatus() (*Subscription, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllBreaches retrieves all breaches available in HIBP, as per
|
// GetAllBreaches retrieves all breaches available in HIBP, as per
|
||||||
// https://haveibeenpwned.com/API/v3#AllBreaches.
|
// https://haveibeenpwned.com/API/v3#AllBreaches. This should be run at
|
||||||
|
// start-up to populate the cache.
|
||||||
func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
||||||
u := api + "/breaches"
|
u := api + "/breaches"
|
||||||
|
|
||||||
|
@ -117,6 +133,57 @@ func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
||||||
return &ab, nil
|
return &ab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllBreachesForAccount retrieves a list of breach names for a given
|
||||||
|
// account.
|
||||||
|
func GetAllBreachesForAccount(account string) ([]BreachName, error) {
|
||||||
|
u := api + "/breachedaccount/" + account
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respCh, errCh := rChans()
|
||||||
|
|
||||||
|
setUA(req)
|
||||||
|
setAuthHeader(req)
|
||||||
|
scheduleReq(req, &respCh, &errCh)
|
||||||
|
|
||||||
|
resp := <-respCh
|
||||||
|
err = <-errCh
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if sc := resp.StatusCode; sc != 200 {
|
||||||
|
// this is brittle...
|
||||||
|
if string(body) == authKeyCheckValue {
|
||||||
|
return nil, ErrAuthKeyCheckValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc == 429 {
|
||||||
|
return nil, ErrRateLimited
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bn := make([]BreachName, 0)
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(body, &bn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bn, nil
|
||||||
|
}
|
||||||
|
|
||||||
func setUA(r *http.Request) {
|
func setUA(r *http.Request) {
|
||||||
r.Header.Set(headerUA, appID)
|
r.Header.Set(headerUA, appID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package hibp
|
package hibp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -52,6 +53,7 @@ func TestMultipleBreaches(t *testing.T) {
|
||||||
respCh, errCh := rChans()
|
respCh, errCh := rChans()
|
||||||
|
|
||||||
setUA(req)
|
setUA(req)
|
||||||
|
setAuthHeader(req)
|
||||||
scheduleReq(req, &respCh, &errCh)
|
scheduleReq(req, &respCh, &errCh)
|
||||||
|
|
||||||
resp := <-respCh
|
resp := <-respCh
|
||||||
|
@ -67,17 +69,63 @@ func TestMultipleBreaches(t *testing.T) {
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
b := string(body)
|
b := string(body)
|
||||||
|
|
||||||
t.Log(b)
|
t.Log(b)
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if apiKey == "" {
|
||||||
if apiKey == "" {
|
switch {
|
||||||
if resp.StatusCode == 401 {
|
case resp.StatusCode != 200:
|
||||||
t.Logf("apiKey is empty, expected a 401")
|
switch {
|
||||||
|
case resp.StatusCode == 401:
|
||||||
|
t.Logf("apiKey is empty, a 401 is expected")
|
||||||
|
case resp.StatusCode == 429:
|
||||||
|
t.Log("skipping due to rate-limit")
|
||||||
|
t.SkipNow()
|
||||||
|
default:
|
||||||
|
t.Errorf("apiKey is empty, expected 401, got: %q, body: %q", resp.Status, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected 200 without apiKey, got: %q, body: %q", resp.Status, b)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
if resp.StatusCode == 429 {
|
||||||
|
t.Log("skipping due to rate-limit")
|
||||||
|
t.SkipNow()
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("apiKey is empty, expected 401, got: %q", resp.Status)
|
t.Errorf("expected 200, got: %q body: %q", resp.Status, b)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("wanted 200, got: %q", resp.Status)
|
want := `[{"Name":"Adobe"},{"Name":"Gawker"},{"Name":"Stratfor"}]`
|
||||||
|
if want != b {
|
||||||
|
t.Errorf("unexpected body, want: %q, got: %q", want, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account := a + hibpTestDomain
|
||||||
|
if b, err := GetAllBreachesForAccount(account); err != nil {
|
||||||
|
if errors.Is(err, ErrRateLimited) {
|
||||||
|
t.Log("skipping due to rate-limit")
|
||||||
|
t.SkipNow()
|
||||||
|
} else {
|
||||||
|
t.Errorf("error getting all breaches for account: %q", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if b != nil {
|
||||||
|
t.Logf("breach names for account %q: %#v", account, b)
|
||||||
|
|
||||||
|
if len(b) != 1 {
|
||||||
|
t.Errorf("expected 1 breach name for account %q, got: %d: %#v", account, len(b), b)
|
||||||
|
} else {
|
||||||
|
want := "Dubsmash"
|
||||||
|
if b[0].Name != want {
|
||||||
|
t.Errorf("expected breach name %q for account %q, got %q", want, account, b[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("expected breach names for account %q, got nil", account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,3 +136,31 @@ func TestGetAllBreaches(t *testing.T) {
|
||||||
t.Errorf("error: %q", err)
|
t.Errorf("error: %q", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAllBreachesForAccount(t *testing.T) {
|
||||||
|
account := "dumb@test.com"
|
||||||
|
|
||||||
|
if b, err := GetAllBreachesForAccount(account); err != nil {
|
||||||
|
if errors.Is(err, ErrRateLimited) {
|
||||||
|
t.Log("skipping due to rate-limit")
|
||||||
|
t.SkipNow()
|
||||||
|
} else {
|
||||||
|
t.Errorf("error getting all breaches for account: %q", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if b != nil {
|
||||||
|
t.Logf("breach names for account %q: %#v", account, b)
|
||||||
|
|
||||||
|
if len(b) != 1 {
|
||||||
|
t.Errorf("expected 1 breach name for account %q, got: %d: %#v", account, len(b), b)
|
||||||
|
} else {
|
||||||
|
want := "Dubsmash"
|
||||||
|
if b[0].Name != want {
|
||||||
|
t.Errorf("expected breach name %q for account %q, got %q", want, account, b[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("expected breach names for account %q, got nil", account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue