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/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
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