1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Add a working Have I Been Pwned module

This commit is contained in:
Chris Cummer 2019-06-20 09:28:48 -04:00
parent 1fa2176412
commit 6216076b74
6 changed files with 314 additions and 0 deletions

View File

@ -20,6 +20,7 @@ import (
"github.com/wtfutil/wtf/modules/gitter" "github.com/wtfutil/wtf/modules/gitter"
"github.com/wtfutil/wtf/modules/gspreadsheets" "github.com/wtfutil/wtf/modules/gspreadsheets"
"github.com/wtfutil/wtf/modules/hackernews" "github.com/wtfutil/wtf/modules/hackernews"
"github.com/wtfutil/wtf/modules/hibp"
"github.com/wtfutil/wtf/modules/ipaddresses/ipapi" "github.com/wtfutil/wtf/modules/ipaddresses/ipapi"
"github.com/wtfutil/wtf/modules/ipaddresses/ipinfo" "github.com/wtfutil/wtf/modules/ipaddresses/ipinfo"
"github.com/wtfutil/wtf/modules/jenkins" "github.com/wtfutil/wtf/modules/jenkins"
@ -114,6 +115,9 @@ func MakeWidget(
case "hackernews": case "hackernews":
settings := hackernews.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig) settings := hackernews.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig)
widget = hackernews.NewWidget(app, pages, settings) widget = hackernews.NewWidget(app, pages, settings)
case "hibp":
settings := hibp.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig)
widget = hibp.NewWidget(app, settings)
case "ipapi": case "ipapi":
settings := ipapi.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig) settings := ipapi.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig)
widget = ipapi.NewWidget(app, settings) widget = ipapi.NewWidget(app, settings)

117
modules/hibp/client.go Normal file
View File

@ -0,0 +1,117 @@
package hibp
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
const (
apiURL = "https://haveibeenpwned.com/api/breachedaccount/"
apiVersion = "application/vnd.haveibeenpwned.v2+json"
clientTimeoutSecs = 2
userAgent = "WTFUtil"
)
func (widget *Widget) fullURL(account string, truncated bool) string {
truncStr := "false"
if truncated == true {
truncStr = "true"
}
return apiURL + account + fmt.Sprintf("?truncateResponse=%s", truncStr)
}
func (widget *Widget) fetchForAccount(account string, since string) (*Status, error) {
if account == "" {
return nil, nil
}
hibpClient := http.Client{
Timeout: time.Second * clientTimeoutSecs,
}
asTruncated := true
if since != "" {
asTruncated = false
}
request, err := http.NewRequest(http.MethodGet, widget.fullURL(account, asTruncated), nil)
if err != nil {
return nil, err
}
request.Header.Set("Accept", apiVersion)
request.Header.Set("User-Agent", userAgent)
response, getErr := hibpClient.Do(request)
if getErr != nil {
return nil, err
}
body, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, err
}
stat, err := widget.parseResponseBody(account, body)
if err != nil {
return nil, err
}
return stat, nil
}
func (widget *Widget) parseResponseBody(account string, body []byte) (*Status, error) {
// If the body is empty then there's no breaches
if len(body) == 0 {
stat := NewStatus(account, []Breach{})
return stat, nil
}
// Else we have breaches for this account
breaches := make([]Breach, 0)
jsonErr := json.Unmarshal(body, &breaches)
if jsonErr != nil {
return nil, jsonErr
}
breaches = widget.filterBreaches(breaches)
return NewStatus(account, breaches), nil
}
func (widget *Widget) filterBreaches(breaches []Breach) []Breach {
// If there's no valid since value in the settings, there's no point in trying to filter
// the breaches on that value, they'll all pass
if !widget.settings.HasSince() {
return breaches
}
sinceDate, err := widget.settings.SinceDate()
if err != nil {
return breaches
}
latestBreaches := []Breach{}
for _, breach := range breaches {
breachDate, err := breach.BreachDate()
if err != nil {
// Append the erring breach here because a failing breach date doesn't mean that
// the breach itself isn't applicable. The date could be missing or malformed,
// in which case we err on the side of caution and assume that the breach is valid
latestBreaches = append(latestBreaches, breach)
continue
}
if breachDate.After(sinceDate) {
latestBreaches = append(latestBreaches, breach)
}
}
return latestBreaches
}

View File

@ -0,0 +1,21 @@
package hibp
import "time"
// Breach represents a breach in the HIBP system
type Breach struct {
Date string `json:"BreachDate"`
Name string `json:"Name"`
}
// BreachDate returns the date of the breach
func (br *Breach) BreachDate() (time.Time, error) {
dt, err := time.Parse("2006-01-02", br.Date)
if err != nil {
// I would much rather return (nil, err) err but that doesn't seem possible
// Not sure what a better value would be
return time.Now(), err
}
return dt, nil
}

View File

@ -0,0 +1,28 @@
package hibp
// Status represents the status of an account in the HIBP system
type Status struct {
Account string
Breaches []Breach
}
// NewStatus creates and returns an instance of Status
func NewStatus(acct string, breaches []Breach) *Status {
stat := Status{
Account: acct,
Breaches: breaches,
}
return &stat
}
// HasBeenCompromised returns TRUE if the specified account has any breaches associated
// with it, FALSE if no breaches are associated with it
func (stat *Status) HasBeenCompromised() bool {
return stat.Len() > 0
}
// Len returns the number of breaches found for the specified account
func (stat *Status) Len() int {
return len(stat.Breaches)
}

68
modules/hibp/settings.go Normal file
View File

@ -0,0 +1,68 @@
package hibp
import (
"time"
"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/wtf"
)
const (
defaultTitle = "HIBP"
minRefreshSecs = 21600 // TODO: Finish implementing this
)
type colors struct {
ok string
pwned string
}
// Settings defines the configuration properties for this module
type Settings struct {
colors
common *cfg.Common
accounts []string
since string
}
// NewSettingsFromYAML creates a new settings instance from a YAML config block
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
settings := Settings{
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, ymlConfig, globalConfig),
accounts: wtf.ToStrs(ymlConfig.UList("accounts")),
since: ymlConfig.UString("since", ""),
}
settings.colors.ok = ymlConfig.UString("colors.ok", "green")
settings.colors.pwned = ymlConfig.UString("colors.pwned", "red")
return &settings
}
// HasSince returns TRUE if there's a valid "since" value setting, FALSE if there is not
func (sett *Settings) HasSince() bool {
if sett.since == "" {
return false
}
_, err := sett.SinceDate()
if err != nil {
return false
}
return true
}
// SinceDate returns the "since" settings as a proper Time instance
func (sett *Settings) SinceDate() (time.Time, error) {
dt, err := time.Parse("2006-01-02", sett.since)
if err != nil {
return time.Now(), err
}
return dt, nil
}

76
modules/hibp/widget.go Normal file
View File

@ -0,0 +1,76 @@
package hibp
import (
"fmt"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/wtf"
)
// Widget is the container for hibp data
type Widget struct {
wtf.TextWidget
accounts []string
settings *Settings
}
// NewWidget creates a new instance of a widget
func NewWidget(app *tview.Application, settings *Settings) *Widget {
widget := Widget{
TextWidget: wtf.NewTextWidget(app, settings.common, false),
settings: settings,
}
return &widget
}
/* -------------------- Exported Functions -------------------- */
// Fetch rettrieves HIBP data from the HIBP API
func (widget *Widget) Fetch(accounts []string) ([]*Status, error) {
data := []*Status{}
for _, account := range accounts {
stat, err := widget.fetchForAccount(account, widget.settings.since)
if err != nil {
return nil, err
}
data = append(data, stat)
}
return data, nil
}
// Refresh updates the data for this widget and displays it onscreen
func (widget *Widget) Refresh() {
data, err := widget.Fetch(widget.settings.accounts)
var content string
if err != nil {
content = err.Error()
} else {
content = widget.contentFrom(data)
}
widget.Redraw(widget.CommonSettings.Title, content, false)
}
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) contentFrom(data []*Status) string {
str := ""
for _, stat := range data {
color := widget.settings.colors.ok
if stat.HasBeenCompromised() {
color = widget.settings.colors.pwned
}
str = str + fmt.Sprintf(" [%s]%s[white]\n", color, stat.Account)
}
return str
}