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