surtur
247c95f753
All checks were successful
continuous-integration/drone/push Build is passing
* also switch addedDate column to string temporarily, until saving yy-mm-dd as time is solved...
281 lines
6.5 KiB
Go
281 lines
6.5 KiB
Go
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package hibp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"git.dotya.ml/mirre-mt/pcmt/ent"
|
|
"git.dotya.ml/mirre-mt/pcmt/ent/hibp"
|
|
"git.dotya.ml/mirre-mt/pcmt/ent/schema"
|
|
"git.dotya.ml/mirre-mt/pcmt/slogging"
|
|
"golang.org/x/exp/slog"
|
|
)
|
|
|
|
// Subscription models the HIBP subscription struct.
|
|
type Subscription struct {
|
|
// The name representing the subscription being either "Pwned 1", "Pwned 2", "Pwned 3" or "Pwned 4".
|
|
SubscriptionName string
|
|
// A human readable sentence explaining the scope of the subscription.
|
|
Description string
|
|
// The date and time the current subscription ends in ISO 8601 format.
|
|
SubscribedUntil time.Time
|
|
// The rate limit in requests per minute. This applies to the rate the breach search by email address API can be requested.
|
|
Rpm int
|
|
// The size of the largest domain the subscription can search. This is expressed in the total number of breached accounts on the domain, excluding those that appear solely in spam list.
|
|
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 (
|
|
api = "https://haveibeenpwned.com/api/v3"
|
|
appID = "pcmt (https://git.dotya.ml/mirre-mt/pcmt)"
|
|
// set default request timeout so as not to hang forever.
|
|
reqTmOut = 5 * time.Second
|
|
|
|
headerUA = "user-agent"
|
|
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 (
|
|
apiKey = os.Getenv("PCMT_HIBP_API_KEY")
|
|
client = &http.Client{Timeout: reqTmOut}
|
|
)
|
|
|
|
// SubscriptionStatus models https://haveibeenpwned.com/API/v3#SubscriptionStatus.
|
|
func SubscriptionStatus() (*Subscription, error) {
|
|
u := api + "/subscription"
|
|
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
setUA(req)
|
|
setAuthHeader(req)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// bodyString := string(body)
|
|
// fmt.Println("API Response as a string:\n" + bodyString)
|
|
|
|
var s Subscription
|
|
|
|
if err := json.Unmarshal(body, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
// fmt.Printf("Subscription struct %+v\n", s)
|
|
|
|
return &Subscription{}, nil
|
|
}
|
|
|
|
// GetAllBreaches retrieves all breaches available in HIBP, as per
|
|
// https://haveibeenpwned.com/API/v3#AllBreaches. This should be run at
|
|
// start-up to populate the cache.
|
|
func GetAllBreaches() (*[]schema.HIBPSchema, error) {
|
|
u := api + "/breaches"
|
|
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
respCh, errCh := rChans()
|
|
|
|
setUA(req)
|
|
slog.Info("scheduling all breaches")
|
|
scheduleReq(req, &respCh, &errCh)
|
|
slog.Info("scheduled all breaches")
|
|
|
|
resp := <-respCh
|
|
err = <-errCh
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// bodyString := string(body)
|
|
// fmt.Println("API Response as a string:\n" + bodyString)
|
|
|
|
ab := make([]schema.HIBPSchema, 0)
|
|
|
|
if err = json.Unmarshal(body, &ab); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// GetBreachesForBreachNames retrieves HIBP breaches from the database for a
|
|
// list of names.
|
|
func GetBreachesForBreachNames(ctx context.Context, client *ent.Client, names []string) ([]*ent.HIBP, error) {
|
|
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
|
log := *slogger
|
|
|
|
log.Logger = log.Logger.With(
|
|
slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
|
|
)
|
|
|
|
hs := make([]*ent.HIBP, 0)
|
|
|
|
for _, name := range names {
|
|
b, err := client.HIBP.
|
|
Query().
|
|
Where(hibp.NameEQ(name)).
|
|
Only(ctx)
|
|
if err != nil {
|
|
switch {
|
|
case ent.IsNotFound(err):
|
|
log.Warnf("breach not found by name %q: %q", name, err)
|
|
return nil, ErrBreachNotFound
|
|
|
|
case ent.IsNotSingular(err):
|
|
log.Warnf("multiple breaches returned for name %q: %q", name, err)
|
|
return nil, ErrBreachNotSingular
|
|
|
|
case err != nil:
|
|
log.Warn("failed to query breach by name", "error", err, "name requested", name)
|
|
return nil, ErrFailedToQueryBreaches
|
|
|
|
default:
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
hs = append(hs, b)
|
|
}
|
|
|
|
return hs, nil
|
|
}
|
|
|
|
// SaveAllBreaches saves all breaches to DB as a cache.
|
|
func SaveAllBreaches(ctx context.Context, client *ent.Client, breaches *[]schema.HIBPSchema) error {
|
|
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
|
log := *slogger
|
|
|
|
log.Logger = log.Logger.With(
|
|
slog.Group("pcmt extra", slog.String("module", "modules/hibp")),
|
|
)
|
|
|
|
if breaches == nil {
|
|
return ErrNoBreachesToSave
|
|
}
|
|
|
|
for _, b := range *breaches {
|
|
_, err := client.HIBP.
|
|
Create().
|
|
SetName(b.Name).
|
|
SetTitle(b.Title).
|
|
SetDomain(b.Domain).
|
|
SetBreachDate(b.BreachDate).
|
|
SetAddedDate(b.AddedDate).
|
|
SetModifiedDate(b.ModifiedDate).
|
|
SetPwnCount(b.PwnCount).
|
|
SetDescription(b.Description).
|
|
SetDataclasses(b.DataClasses).
|
|
SetIsVerified(b.IsVerified).
|
|
SetIsFabricated(b.IsFabricated).
|
|
SetIsSensitive(b.IsSensitive).
|
|
SetIsRetired(b.IsRetired).
|
|
SetIsSpamList(b.IsSpamList).
|
|
SetIsMalware(b.IsMalware).
|
|
SetLogoPath(b.LogoPath).
|
|
Save(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func setUA(r *http.Request) {
|
|
r.Header.Set(headerUA, appID)
|
|
}
|
|
|
|
func setAuthHeader(r *http.Request) {
|
|
r.Header.Set(headerHIBP, apiKey)
|
|
}
|
|
|
|
func rChans() (chan *http.Response, chan error) {
|
|
return make(chan *http.Response), make(chan error)
|
|
}
|