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:
parent
1fa2176412
commit
6216076b74
@ -20,6 +20,7 @@ import (
|
||||
"github.com/wtfutil/wtf/modules/gitter"
|
||||
"github.com/wtfutil/wtf/modules/gspreadsheets"
|
||||
"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/ipinfo"
|
||||
"github.com/wtfutil/wtf/modules/jenkins"
|
||||
@ -114,6 +115,9 @@ func MakeWidget(
|
||||
case "hackernews":
|
||||
settings := hackernews.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig)
|
||||
widget = hackernews.NewWidget(app, pages, settings)
|
||||
case "hibp":
|
||||
settings := hibp.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig)
|
||||
widget = hibp.NewWidget(app, settings)
|
||||
case "ipapi":
|
||||
settings := ipapi.NewSettingsFromYAML(widgetName, moduleConfig, globalConfig)
|
||||
widget = ipapi.NewWidget(app, settings)
|
||||
|
117
modules/hibp/client.go
Normal file
117
modules/hibp/client.go
Normal 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
|
||||
}
|
21
modules/hibp/hibp_breach.go
Normal file
21
modules/hibp/hibp_breach.go
Normal 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
|
||||
}
|
28
modules/hibp/hibp_status.go
Normal file
28
modules/hibp/hibp_status.go
Normal 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
68
modules/hibp/settings.go
Normal 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
76
modules/hibp/widget.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user