Skip to content

Commit f305149

Browse files
committed
Created repo
0 parents  commit f305149

File tree

12 files changed

+903
-0
lines changed

12 files changed

+903
-0
lines changed

.github/workflows/release.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: goreleaser
2+
3+
on:
4+
pull_request:
5+
push:
6+
tags:
7+
- "*"
8+
9+
permissions:
10+
contents: write
11+
packages: write
12+
13+
jobs:
14+
goreleaser:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
- name: Set up Go
22+
uses: actions/setup-go@v5
23+
with:
24+
go-version: stable
25+
- name: Run GoReleaser
26+
uses: goreleaser/goreleaser-action@v6
27+
with:
28+
distribution: goreleaser
29+
version: "latest"
30+
args: release --clean
31+
env:
32+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config.json
2+
.env
3+
dist/

.goreleaser.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# This is an example .goreleaser.yml file with some sensible defaults.
2+
# Make sure to check the documentation at https://goreleaser.com
3+
4+
# The lines below are called `modelines`. See `:help modeline`
5+
# Feel free to remove those if you don't want/need to use them.
6+
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7+
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
8+
9+
version: 2
10+
11+
builds:
12+
- env:
13+
- CGO_ENABLED=0
14+
goos:
15+
- linux
16+
- windows
17+
- darwin
18+
19+
archives:
20+
- format: tar.gz
21+
# this name template makes the OS and Arch compatible with the results of `uname`.
22+
name_template: >-
23+
{{ .ProjectName }}_
24+
{{- title .Os }}_
25+
{{- if eq .Arch "amd64" }}x86_64
26+
{{- else if eq .Arch "386" }}i386
27+
{{- else }}{{ .Arch }}{{ end }}
28+
{{- if .Arm }}v{{ .Arm }}{{ end }}
29+
30+
# use zip for windows archives
31+
format_overrides:
32+
- goos: windows
33+
format: zip
34+
35+
release:
36+
extra_files:
37+
- glob: email_template.html
38+
- glob: config.example.json
39+
- glob: README.md
40+
41+
changelog:
42+
sort: asc
43+
filters:
44+
exclude:
45+
- "^docs:"
46+
- "^test:"

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Have I Been Pwned Notifier
2+
Notifies users when their details have been in a data breach based on the HIBP API.
3+
4+
## How it works
5+
1. Checks the HIBP API to see what the latest breach is
6+
2. If the breach is the one mentioned in the configuration file, exit
7+
3. If it is a new breach, get all breached users from the domains endpoint for all domains in the config
8+
4. Check if a user has been in a breach that is not in the "notifiedBreaches" list
9+
5. If they're in that list, send them an email with the new breach and a list of old breaches their account has been in as well
10+
11+
## Configuration
12+
```json
13+
{
14+
# Have I been Pwned API Key
15+
"hibpApiKey": "",
16+
17+
# Domains to check for breaches
18+
# All domains here have to be verified on the HIBP dashboard before usage
19+
"domains": [
20+
"domain.com"
21+
],
22+
23+
# The latest breach that the script has notified users about
24+
"latestBreach": "VTech",
25+
26+
# Breaches that the script has already notified users about
27+
"notifiedBreaches": [
28+
"Spytech",
29+
"Adobe",
30+
"Badoo",
31+
"Kickstarter",
32+
"MyFitnessPal"
33+
],
34+
35+
# SMTP Settings
36+
"smtp": {
37+
"sender": "mailer@domain.com",
38+
"host": "smtp.domain.com",
39+
"port": 25,
40+
"secure": false,
41+
"starttls": true,
42+
"user": "username",
43+
"pass": "password"
44+
},
45+
46+
# Ignore breaches of certain types
47+
# To read more, visit https://haveibeenpwned.com/API/v3#BreachModel
48+
"ignore": {
49+
"unverified": false,
50+
"fabricated": false,
51+
"sensitive": false,
52+
"retired": false,
53+
"spamList": false,
54+
"malware": false
55+
},
56+
57+
# Settings for colors and texts in the email sent to the users
58+
# You can also provide your own custom email template. For substitutions, check below
59+
"email": {
60+
"colors": {
61+
"background": "#ff8000",
62+
"text": "#ffffff"
63+
},
64+
"subject": "Your account might have been compromised",
65+
"body": {
66+
"header": "Your account might have been compromised",
67+
"texts": [
68+
"In a recent scan, we found that your email address was mentioned in a recent breach of user information.",
69+
"This does not necessarily mean that your password has been compromised, but for security reasons we do recommend that you change it.",
70+
"For more information about the breach, click one of the links next to the relevant breaches below."
71+
],
72+
"previous_breach_texts": [
73+
"Your account has also been in previous, older breaches. You can find a complete list below."
74+
]
75+
}
76+
}
77+
}
78+
```
79+
80+
81+
## Email Template Substitutions
82+
| Placeholder | Description | Example | Value From |
83+
|-------------|-------------|---------|------------|
84+
| `{{text_color}}` | The color of the text in the email | `#ffffff` | config.json |
85+
| `{{bg_color}}` | The color of the background in the email | `#ff8000` | config.json |
86+
| `{{body_text}}` | The body text section from the configuration file | `In a recent scan, we found that your email address was mentioned in a recent breach of user information.` | config.json |
87+
| `{{previous_breaches_text}}` | The text that is shown before the list of previous breaches | `Your account has also been in previous, older breaches. You can find a complete list below.` | config.json |
88+
| `{{username}}` | The email of the user that was in the breach | `user@domain.com` | HIBP API |
89+
| `{{new_breach_rows}}` | The list of new breaches that the user was in | `<tr><td>BreachA</td><td>Link to BreachA</td></tr>` | HIBP API |
90+
| `{{previous_breaches_rows}}` | The list of previous breaches that the user was in | `<tr><td>BreachA</td><td>Link to BreachA</td></tr>` | HIBP API |

config.example.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"hibpApiKey": "",
3+
"domains": [
4+
"domain.com"
5+
],
6+
7+
"latestBreach": "VTech",
8+
"notifiedBreaches": [
9+
"Spytech",
10+
"Adobe",
11+
"Badoo",
12+
"Kickstarter",
13+
"MyFitnessPal"
14+
],
15+
16+
"smtp": {
17+
"sender": "mailer@domain.com",
18+
"host": "smtp.domain.com",
19+
"port": 25,
20+
"secure": false,
21+
"starttls": true,
22+
"user": "username",
23+
"pass": "password"
24+
},
25+
26+
"ignore": {
27+
"unverified": false,
28+
"fabricated": false,
29+
"sensitive": false,
30+
"retired": false,
31+
"spamList": false,
32+
"malware": false
33+
},
34+
35+
"email": {
36+
"colors": {
37+
"background": "#ff8000",
38+
"text": "#ffffff"
39+
},
40+
"subject": "Your account might have been compromised",
41+
"body": {
42+
"header": "Your account might have been compromised",
43+
"texts": [
44+
"In a recent scan, we found that your email address was mentioned in a recent breach of user information.",
45+
"This does not necessarily mean that your password has been compromised, but for security reasons we do recommend that you change it.",
46+
"For more information about the breach, click one of the links next to the relevant breaches below."
47+
],
48+
"previous_breach_texts": [
49+
"Your account has also been in previous, older breaches. You can find a complete list below."
50+
]
51+
}
52+
}
53+
}

config.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
)
7+
8+
type SmtpConfig struct {
9+
Sender string `json:"sender"`
10+
Host string `json:"host"`
11+
Port int `json:"port"`
12+
User string `json:"user"`
13+
Pass string `json:"pass"`
14+
Secure bool `json:"secure"`
15+
StartTLS bool `json:"starttls"`
16+
}
17+
18+
type IgnoreConfig struct {
19+
Unverified bool `json:"unverified"`
20+
Fabricated bool `json:"fabricated"`
21+
Sensitive bool `json:"sensitive"`
22+
SpamList bool `json:"spamList"`
23+
Malware bool `json:"malware"`
24+
Retired bool `json:"retired"`
25+
}
26+
27+
type EmailConfig struct {
28+
Colors struct {
29+
Background string `json:"background"`
30+
Text string `json:"text"`
31+
} `json:"colors"`
32+
33+
Subject string `json:"subject"`
34+
Body struct {
35+
Header string `json:"header"`
36+
Texts []string `json:"texts"`
37+
PreviousBreachTexts []string `json:"previous_breach_texts"`
38+
} `json:"body"`
39+
}
40+
41+
type Config struct {
42+
ApiKey string `json:"hibpApiKey"`
43+
Domains []string `json:"domains"`
44+
LatestBreach string `json:"latestBreach"`
45+
NotifiedBreaches []string `json:"notifiedBreaches"`
46+
Smtp SmtpConfig `json:"smtp"`
47+
Ignore IgnoreConfig `json:"ignore"`
48+
Email EmailConfig `json:"email"`
49+
}
50+
51+
func LoadConfig() (Config, error) {
52+
config, err := os.ReadFile("config.json")
53+
if err != nil {
54+
return Config{}, err
55+
}
56+
57+
var cfg Config
58+
err = json.Unmarshal(config, &cfg)
59+
return cfg, err
60+
}
61+
62+
func SaveConfig(cfg Config) error {
63+
config, err := json.MarshalIndent(cfg, "", " ")
64+
if err != nil {
65+
return err
66+
}
67+
68+
err = os.WriteFile("config.json", config, 0644)
69+
return err
70+
}

domain_breaches.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"strconv"
9+
"time"
10+
)
11+
12+
type DomainBreachesResponse map[string][]string
13+
14+
func QueryDomainBreaches(domain string, config Config) (DomainBreachesResponse, error) {
15+
req, err := http.NewRequest("GET", fmt.Sprintf("https://haveibeenpwned.com/api/v3/breacheddomain/%s", domain), nil)
16+
if err != nil {
17+
log.Fatal(err)
18+
return DomainBreachesResponse{}, err
19+
}
20+
21+
req.Header.Set("User-Agent", "HIBP Breach Alert")
22+
req.Header.Set("hibp-api-key", config.ApiKey)
23+
24+
client := &http.Client{}
25+
resp, err := client.Do(req)
26+
if err != nil {
27+
log.Fatal(err)
28+
return DomainBreachesResponse{}, err
29+
}
30+
31+
defer resp.Body.Close()
32+
33+
if resp.StatusCode != 200 {
34+
if resp.StatusCode != 429 {
35+
log.Fatalf("Received status code %d", resp.StatusCode)
36+
return DomainBreachesResponse{}, fmt.Errorf("Received status code %d", resp.StatusCode)
37+
}
38+
39+
waitFor := resp.Header.Get("Retry-After")
40+
log.Printf("Rate limited. Waiting for %s seconds", waitFor)
41+
ival, err := strconv.ParseInt(waitFor, 10, 64)
42+
if err != nil {
43+
log.Fatal(err)
44+
return DomainBreachesResponse{}, err
45+
}
46+
time.Sleep(time.Second * time.Duration(ival+10))
47+
48+
return QueryDomainBreaches(domain, config)
49+
}
50+
51+
var domainBreachResponse DomainBreachesResponse
52+
53+
if err := json.NewDecoder(resp.Body).Decode(&domainBreachResponse); err != nil {
54+
log.Fatal(err)
55+
return DomainBreachesResponse{}, err
56+
}
57+
58+
return domainBreachResponse, nil
59+
}

0 commit comments

Comments
 (0)