Compare commits

..

No commits in common. "main" and "1.1.0" have entirely different histories.
main ... 1.1.0

16 changed files with 313 additions and 395 deletions

3
.gitignore vendored
View file

@ -24,6 +24,3 @@ dist/
data
config
*.bak
# Config/data files
*.json

View file

@ -1,4 +1,4 @@
# WidNotifier
# WID Notifier
The German [BSI](https://www.bsi.bund.de/) and [LSI Bavaria](https://lsi.bayern.de/) each have a page listing current security notices.
This software queries the APIs of these services for new security notices and sends configurable email notifications.
@ -17,7 +17,7 @@ This Software only supports Linux.
# Build
To cross-compile the software for `i386`, `amd64`, `arm` and `arm64`, run `build.sh`.
You need Go 1.22.x and git.
You need a go version >= 1.21 and git.
# Usage
@ -34,28 +34,27 @@ Example:
```json
{
"api_fetch_interval": 600,
"datafile": "data",
"enabled_api_endpoints": [
"bay",
"bund"
],
"datafile": "data.json",
"loglevel": 2,
"lists": [
"recipients": [
{
"name": "Example List",
"recipients": ["someone@example.org"],
"filter": [
{"classification": "hoch", "title_contains": "Microsoft"},
{"classification": "kritisch"}
"address": "guenther@example.org",
"include": [
{"classification": "kritisch"},
{"title_contains": "jQuery"}
]
}
],
"smtp": {
"from": "user@localhost",
"host": "127.0.0.1",
"from": "from@example.org",
"host": "example.org",
"port": 587,
"user": "user@localhost",
"password": "change me :)"
"user": "from@example.org",
"password": "SiEhAbEnMiChInSgEsIcHtGeFiLmTdAsDĂĽRfEnSiEnIcHt"
},
"template": {
"subject": "",
@ -68,9 +67,9 @@ To show debug messages, set the `loglevel` to `3`.
## Filters
You define filters for notices to be sent (per recipient). Multiple filters can be set per recipient and multiple criteria can be used per filter. The configuration field for those filters is `include`. See [Configuration](#configuration) for an example.
You define filters for notices to be sent per recipient. Multiple filters can be set per recipient and multiple criteria can be used per filter. The configuration field for those filters is `include`. See [Configuration](#configuration) for an example.
If a notice is included is determined by the following logic:
It is determined by the following logic, if a notice is included:
```
{criteria, criteria, ... ALL APPLY}
@ -78,7 +77,7 @@ OR {criteria, criteria, ... ALL APPLY}
OR ...
```
The following criteria are available. Criteria marked with * are optional fields that are not supported by every API endpoint (e.g. https://wid.lsi.bayern.de) - notices from those endpoints will therefore not be included when using those criteria in filters.
The following criteria are available. Criteria marked with `*` are for optional fields that are not supported by every API endpoint (e.g. https://wid.lsi.bayern.de) - notices from those endpoints will therefore not be included when using those criteria in filters.
```json
"include": [
@ -123,7 +122,7 @@ Classification can be `"kritisch"`, `"hoch"`, `"mittel"` or `"niedrig"`.
```
If set to `""`, this criteria will be ignored.
### min_basescore *
### min_basescore `*`
Include notices whose basescore (`0` - `100`) is >= `min_basescore`.
@ -132,7 +131,7 @@ Include notices whose basescore (`0` - `100`) is >= `min_basescore`.
```
This criteria will be ignored if set to `0`.
### status *
### status `*`
Include notices with this status. This is usually either `NEU` or `UPDATE`.
@ -141,7 +140,7 @@ Include notices with this status. This is usually either `NEU` or `UPDATE`.
```
If set to `""`, this criteria will be ignored.
### products_contain *
### products_contain `*`
Include notices whose product list contains this text.
@ -150,7 +149,7 @@ Include notices whose product list contains this text.
```
If set to `""`, this criteria will be ignored.
### no_patch *
### no_patch `*`
If set to `"true"`, notices where no patch is available will be included.
@ -203,6 +202,4 @@ type WidNotice struct {
}
```
Additionally, the field `WidNotifierVersion` holds the version of the software.
For an example, take a look at `DEFAULT_SUBJECT_TEMPLATE` and `DEFAULT_BODY_TEMPLATE` in [template.go](./template.go).

View file

@ -2,13 +2,14 @@
VERSION=$(git describe --tags)
function build() {
printf "Compiling version ${VERSION} for ${1}/${2}\t"
GOOS=${1} GOARCH=${2} go build -ldflags "-X 'main.Version=${VERSION}'" -o dist/wid-notifier_${VERSION}_${1}_${3}
echo "âś…"
}
# i386
GOOS=linux GOARCH=386 go build -o dist/wid-notifier_${VERSION}_linux_i386
build linux "386" i386
build linux amd64 amd64
build linux arm arm
build linux arm64 arm64
# amd64
GOOS=linux GOARCH=amd64 go build -o dist/wid-notifier_${VERSION}_linux_amd64
# arm
GOOS=linux GOARCH=arm go build -o dist/wid-notifier_${VERSION}_linux_arm
# arm64
GOOS=linux GOARCH=arm64 go build -o dist/wid-notifier_${VERSION}_linux_arm64

View file

@ -1,66 +0,0 @@
// Copyright (c) 2024 Julian MĂĽller (ChaoticByte)
package main
import (
"errors"
)
type Config struct {
ApiFetchInterval int `json:"api_fetch_interval"` // in seconds
EnabledApiEndpoints []string `json:"enabled_api_endpoints"`
PersistentDataFilePath string `json:"datafile"`
LogLevel int `json:"loglevel"`
Lists *[]NotifyList `json:"lists"`
SmtpConfiguration SmtpSettings `json:"smtp"`
Template MailTemplateConfig `json:"template"`
}
func NewConfig() Config {
// Initial config
c := Config{
ApiFetchInterval: 60 * 10, // every 10 minutes,
EnabledApiEndpoints: []string{"bay", "bund"},
PersistentDataFilePath: "data.json",
LogLevel: 2,
Lists: &[]NotifyList{
{ Name: "Example List",
Recipients: []string{},
Filter: []Filter{},},
},
SmtpConfiguration: SmtpSettings{
From: "user@localhost",
User: "user@localhost",
Password: "change me :)",
ServerHost: "127.0.0.1",
ServerPort: 587},
Template: MailTemplateConfig{
SubjectTemplate: "",
BodyTemplate: "",
},
}
return c
}
func checkConfig(config Config) {
if len(*config.Lists) < 1 {
logger.error("Configuration is incomplete")
panic(errors.New("no lists are configured"))
}
for _, l := range *config.Lists {
if len(l.Filter) < 1 {
logger.error("Configuration is incomplete")
panic(errors.New("list " + l.Name + " has no filter defined - at least [{'any': true/false}] should be configured"))
}
for _, r := range l.Recipients {
if !mailAddressIsValid(r) {
logger.error("Configuration includes invalid data")
panic(errors.New("'" + r + "' is not a valid e-mail address"))
}
}
}
if !mailAddressIsValid(config.SmtpConfiguration.From) {
logger.error("Configuration includes invalid data")
panic(errors.New("'" + config.SmtpConfiguration.From + "' is not a valid e-mail address"))
}
}

View file

@ -4,10 +4,79 @@ package main
import (
"encoding/json"
"errors"
"io/fs"
"os"
"time"
)
type Config struct {
ApiFetchInterval int `json:"api_fetch_interval"` // in seconds
EnabledApiEndpoints []string `json:"enabled_api_endpoints"`
PersistentDataFilePath string `json:"datafile"`
LogLevel int `json:"loglevel"`
Recipients []Recipient `json:"recipients"`
SmtpConfiguration SmtpSettings `json:"smtp"`
Template MailTemplateConfig `json:"template"`
}
func NewConfig() Config {
// Initial config
c := Config{
ApiFetchInterval: 60 * 10, // every 10 minutes,
EnabledApiEndpoints: []string{"bay", "bund"},
PersistentDataFilePath: "data",
LogLevel: 2,
Recipients: []Recipient{},
SmtpConfiguration: SmtpSettings{
From: "from@example.org",
User: "from@example.org",
Password: "SiEhAbEnMiChInSgEsIcHtGeFiLmTdAsDĂĽRfEnSiEnIcHt",
ServerHost: "example.org",
ServerPort: 587},
Template: MailTemplateConfig{
SubjectTemplate: "",
BodyTemplate: "",
},
}
return c
}
func checkConfig(config Config) {
if len(config.Recipients) < 1 {
logger.error("Configuration is incomplete")
panic(errors.New("no recipients are configured"))
}
for _, r := range config.Recipients {
if !mailAddressIsValid(r.Address) {
logger.error("Configuration includes invalid data")
panic(errors.New("'" + r.Address + "' is not a valid e-mail address"))
}
if len(r.Filters) < 1 {
logger.error("Configuration is incomplete")
panic(errors.New("recipient " + r.Address + " has no filter defined - at least {'any': true/false} should be configured"))
}
}
if !mailAddressIsValid(config.SmtpConfiguration.From) {
logger.error("Configuration includes invalid data")
panic(errors.New("'" + config.SmtpConfiguration.From + "' is not a valid e-mail address"))
}
}
type PersistentData struct {
// {endpoint id 1: time last published, endpoint id 2: ..., ...}
LastPublished map[string]time.Time `json:"last_published"`
}
func NewPersistentData(c Config) PersistentData {
// Initial persistent data
d := PersistentData{LastPublished: map[string]time.Time{}}
for _, e := range apiEndpoints {
d.LastPublished[e.Id] = time.Now().Add(-time.Hour * 24) // a day ago
}
return d
}
type DataStore struct {
filepath string
prettyJSON bool
@ -23,24 +92,32 @@ func (ds *DataStore) save() error {
} else {
data, err = json.Marshal(ds.data)
}
if err != nil { return err }
if err != nil {
return err
}
err = os.WriteFile(ds.filepath, data, ds.fileMode)
return err
}
func (ds *DataStore) load() error {
data, err := os.ReadFile(ds.filepath)
if err != nil { return err }
if err != nil {
return err
}
switch ds.data.(type) {
case Config:
d, _ := ds.data.(Config);
err = json.Unmarshal(data, &d)
if err != nil { return err }
if err != nil {
return err
}
ds.data = d
case PersistentData:
d, _ := ds.data.(PersistentData);
err = json.Unmarshal(data, &d)
if err != nil { return err }
if err != nil {
return err
}
ds.data = d
}
return err

2
go.mod
View file

@ -1,3 +1,3 @@
module github.com/ChaoticByte/wid-notifier
go 1.24
go 1.21

162
mail.go Normal file
View file

@ -0,0 +1,162 @@
// Copyright (c) 2023 Julian MĂĽller (ChaoticByte)
package main
import (
"crypto/tls"
"encoding/base64"
"fmt"
"net/mail"
"net/smtp"
"slices"
"time"
)
const MAIL_LINE_SEP = "\r\n"
type MailContent struct {
Subject string
Body string
}
func (c MailContent) serializeValidMail(from string, to string) []byte {
// We'll send base64 encoded Subject & Body, because we Dschörmäns have umlauts
// and I'm too lazy to encode ä into =E4 and so on
subjectEncoded := base64.StdEncoding.EncodeToString([]byte(c.Subject))
bodyEncoded := base64.StdEncoding.EncodeToString([]byte(c.Body))
data := []byte(fmt.Sprintf(
"Content-Type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: base64\r\nFrom: %v%vTo: %v%vSubject: =?utf-8?b?%v?=%v%v%v",
from, MAIL_LINE_SEP,
to, MAIL_LINE_SEP,
subjectEncoded, MAIL_LINE_SEP,
MAIL_LINE_SEP,
bodyEncoded))
// done, I guess
return data
}
type Recipient struct {
Address string `json:"address"`
// Must be a configured filter id
Filters []Filter `json:"include"`
}
type SmtpSettings struct {
From string `json:"from"`
ServerHost string `json:"host"`
ServerPort int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
}
func (r Recipient) filterAndSendNotices(notices []WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, cache *map[string][]byte) error {
filteredNotices := []WidNotice{}
for _, f := range r.Filters {
for _, n := range f.filter(notices) {
if !noticeSliceContains(filteredNotices, n) {
filteredNotices = append(filteredNotices, n)
}
}
}
slices.Reverse(filteredNotices)
logger.debug(fmt.Sprintf("Including %v of %v notices for recipient %v", len(filteredNotices), len(notices), r.Address))
logger.debug("Generating and sending mails to " + r.Address + " ...")
cacheHits := 0
cacheMisses := 0
mails := [][]byte{}
for _, n := range filteredNotices {
var data []byte
cacheResult := (*cache)[n.Uuid]
if len(cacheResult) > 0 {
cacheHits++
data = cacheResult
} else {
cacheMisses++
mailContent, err := template.generate(n)
if err != nil {
logger.error("Could not create mail from template")
logger.error(err)
}
// serialize & send mail
data = mailContent.serializeValidMail(smtpConfig.From, r.Address)
// add to cache
(*cache)[n.Uuid] = data
}
mails = append(mails, data)
}
logger.debug(fmt.Sprintf("%v mail cache hits, %v misses", cacheHits, cacheMisses))
err := sendMails(
smtpConfig,
auth,
r.Address,
mails,
)
if err != nil {
return err
}
logger.debug("Successfully sent all mails to " + r.Address)
return nil
}
func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, data [][]byte) error {
addr := fmt.Sprintf("%v:%v", smtpConf.ServerHost, smtpConf.ServerPort)
logger.debug("Connecting to mail server at " + addr + " ...")
connection, err := smtp.Dial(addr)
if err != nil {
return err
}
defer connection.Close()
// can leave out connection.Hello
hasTlsExt, _ := connection.Extension("starttls")
if hasTlsExt {
err = connection.StartTLS(&tls.Config{ServerName: smtpConf.ServerHost})
if err != nil {
return err
}
logger.debug("Mail Server supports TLS")
} else {
logger.debug("Mail Server doesn't support TLS")
}
logger.debug("Authenticating to mail server ...")
err = connection.Auth(auth)
if err != nil {
return err
}
if logger.LogLevel >= 3 {
fmt.Printf("DEBUG %v Sending mails to server ", time.Now().Format("2006/01/02 15:04:05.000000"))
}
for _, d := range data {
err = connection.Mail(smtpConf.From)
if err != nil {
return err
}
err = connection.Rcpt(to)
if err != nil {
return err
}
writer, err := connection.Data()
if err != nil {
return err
}
_, err = writer.Write(d)
if err != nil {
return err
}
err = writer.Close()
if err != nil {
return err
}
if logger.LogLevel >= 3 {
print(".")
}
}
if logger.LogLevel >= 3 {
print("\n")
}
return connection.Quit()
}
func mailAddressIsValid(address string) bool {
_, err := mail.ParseAddress(address);
return err == nil
}

View file

@ -1,90 +0,0 @@
// Copyright (c) 2023 Julian MĂĽller (ChaoticByte)
package main
import (
"fmt"
"mime"
"mime/quotedprintable"
"net/mail"
"net/smtp"
"strings"
)
type MailContent struct {
Subject string
Body string
}
func (c MailContent) serializeValidMail(from string, to string) []byte {
// format subject using Q Encoding from RFC2047
subjectEncoded := mime.QEncoding.Encode("utf-8", c.Subject)
// format body using Quoted-Printable Encoding from RFC2045
var bodyEncoded strings.Builder
bew := quotedprintable.NewWriter(&bodyEncoded)
bew.Write([]byte(c.Body))
bew.Close()
// glue it all together
data := fmt.Appendf(nil,
"Content-Type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: Quoted-Printable\r\nFrom: %v\r\nTo: %v\r\nSubject: %v\r\n\r\n%v",
from, to, subjectEncoded, bodyEncoded.String(),
)
return data
}
type NotifyList struct {
Name string `json:"name"`
Recipients []string `json:"recipients"`
// Must be a configured filter id
Filter []Filter `json:"filter"`
}
type SmtpSettings struct {
From string `json:"from"`
ServerHost string `json:"host"`
ServerPort int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
}
func sendNotices(recipient string, notices []*WidNotice, template MailTemplate, auth smtp.Auth, smtpConfig SmtpSettings, mailContentCache *map[string]*MailContent) error {
logger.debug("Generating and sending mails for recipient " + recipient + " ...")
cacheHits := 0
cacheMisses := 0
mails := []*MailContent{}
for _, n := range notices {
var mc *MailContent
cacheResult := (*mailContentCache)[n.Uuid]
if cacheResult != nil {
cacheHits++
mc = cacheResult
} else {
cacheMisses++
mc_, err := template.generate(TemplateData{n, Version})
if err != nil {
logger.error("Could not create mail from template")
logger.error(err)
} else {
mc = &mc_
// add to cache
(*mailContentCache)[n.Uuid] = mc
}
}
mails = append(mails, mc)
}
logger.debug(fmt.Sprintf("%v mail cache hits, %v misses", cacheHits, cacheMisses))
err := sendMails(
smtpConfig,
auth,
recipient,
mails,
)
if err != nil { return err }
logger.debug("Successfully sent all mails to " + recipient)
return nil
}
func mailAddressIsValid(address string) bool {
_, err := mail.ParseAddress(address);
return err == nil
}

View file

@ -1,57 +0,0 @@
// +build !debug_mail_transfer
// Copyright (c) 2023 Julian MĂĽller (ChaoticByte)
package main
import (
"crypto/tls"
"fmt"
"net/smtp"
"time"
)
func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, mails []*MailContent) error {
addr := fmt.Sprintf("%v:%v", smtpConf.ServerHost, smtpConf.ServerPort)
logger.debug("Connecting to mail server at " + addr + " ...")
connection, err := smtp.Dial(addr)
if err != nil { return err }
defer connection.Close()
// can leave out connection.Hello
hasTlsExt, _ := connection.Extension("starttls")
if hasTlsExt {
err = connection.StartTLS(&tls.Config{ServerName: smtpConf.ServerHost})
if err != nil { return err }
logger.debug("Mail Server supports StartTLS")
} else {
logger.debug("Mail Server doesn't support StartTLS")
}
logger.debug("Authenticating to mail server ...")
err = connection.Auth(auth)
if err != nil { return err }
if logger.LogLevel >= 3 {
fmt.Printf("DEBUG %v Sending mails to server ", time.Now().Format("2006/01/02 15:04:05.000000"))
}
for _, mc := range mails {
// serialize mail
d := mc.serializeValidMail(smtpConf.From, to)
// send mail
err = connection.Mail(smtpConf.From)
if err != nil { return err }
err = connection.Rcpt(to)
if err != nil { return err }
writer, err := connection.Data()
if err != nil { return err }
_, err = writer.Write(d)
if err != nil { return err }
err = writer.Close()
if err != nil { return err }
if logger.LogLevel >= 3 {
print(".")
}
}
if logger.LogLevel >= 3 {
print("\n")
}
return connection.Quit()
}

View file

@ -1,26 +0,0 @@
// +build debug_mail_transfer
// Copyright (c) 2023 Julian MĂĽller (ChaoticByte)
package main
import (
"fmt"
"net/smtp"
)
func sendMails(smtpConf SmtpSettings, auth smtp.Auth, to string, mails []*MailContent) error {
logger.warn("Mail Transfer Debugging is active. Not connecting.")
logger.info("MAIL TRANSFER: \n\n")
for _, mc := range mails {
// serialize mail
d := mc.serializeValidMail(smtpConf.From, to)
// output mail
fmt.Println("MAIL FROM:" + smtpConf.From)
fmt.Println("RCPT TO:" + to)
fmt.Println("DATA")
fmt.Println(string(d))
fmt.Println(".")
}
fmt.Print("\n\n")
return nil
}

92
main.go
View file

@ -6,55 +6,35 @@ import (
"fmt"
"net/smtp"
"os"
"slices"
"time"
)
var executableName string
var logger Logger
func showVersion() {
fmt.Printf("wid-notifier %s\n", Version)
}
func showHelp() {
fmt.Printf("Usage: %v <configfile>\n\nIf the config file doesn't exist, an incomplete \n" +
"configuration with default values is created.\n\n",
executableName)
showVersion()
}
func main() {
// get cli arguments
args := os.Args
executableName = args[0]
if len(args) < 2 {
showHelp()
fmt.Printf("Usage: %v <configfile>\nIf the config file doesn't exist, a incomplete configuration with default values is created.\n", args[0])
os.Exit(1)
}
for _, arg := range args {
if arg == "-h" || arg == "--help" {
showHelp()
os.Exit(0)
} else if arg == "--version" {
showVersion()
os.Exit(0)
}
}
configFilePath := args[1]
configFilePath := os.Args[1]
// create logger
logger = NewLogger(2)
// init
logger.info("Initializing ...")
defer logger.info("Exiting ...")
// open & check config
config := NewDataStore(
configFilePath,
NewConfig(),
true,
0600,
).data.(Config)
persistent := NewDataStore(
config.PersistentDataFilePath,
NewPersistentData(config),
false,
0640)
logger.LogLevel = config.LogLevel
logger.debug("Checking configuration file ...")
checkConfig(config)
@ -86,24 +66,17 @@ func main() {
}
}
}
// open data file
persistent := NewDataStore(
config.PersistentDataFilePath,
NewPersistentData(config),
false,
0640)
// main loop
logger.debug("Entering main loop ...")
for {
t1 := time.Now().UnixMilli()
newNotices := []WidNotice{}
lastPublished := map[string]time.Time{} // endpoint id : last published timestamp
cache := map[string]*MailContent{} // cache generated emails for reuse
cache := map[string][]byte{}
for _, a := range enabledApiEndpoints {
logger.info("Querying endpoint '" + a.Id + "' for new notices ...")
n, t, err := a.getNotices(persistent.data.(PersistentData).LastPublished[a.Id])
if err != nil {
// retry (once)
// retry
logger.warn("Couldn't query notices from API endpoint '" + a.Id + "'. Retrying ...")
logger.warn(err)
n, t, err = a.getNotices(persistent.data.(PersistentData).LastPublished[a.Id])
@ -114,59 +87,26 @@ func main() {
logger.error(err)
} else if len(n) > 0 {
newNotices = append(newNotices, n...)
lastPublished[a.Id] = t
persistent.data.(PersistentData).LastPublished[a.Id] = t
persistent.save()
}
}
logger.debug(fmt.Sprintf("Got %v new notices", len(newNotices)))
if len(newNotices) > 0 {
logger.info("Sending email notifications ...")
// mail recipient : pointer to slice of wid notices to be sent
noticesToBeSent := map[string][]*WidNotice{}
recipientsNotified := 0
var err error
for _, l := range *config.Lists {
// Filter notices for this list
for _, f := range l.Filter {
for _, n := range f.filter(newNotices) {
np := &n
for _, r := range l.Recipients {
if !noticeSliceContains(noticesToBeSent[r], np) {
noticesToBeSent[r] = append(noticesToBeSent[r], np)
}
}
}
}
}
for r, notices := range noticesToBeSent {
// sort by publish date
slices.SortFunc(notices, func(a *WidNotice, b *WidNotice) int {
if a.Published == b.Published {
return 0
} else if a.Published.After(b.Published) {
return 1
} else {
return -1
}
})
// send
err = sendNotices(r, notices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache)
for _, r := range config.Recipients {
err := r.filterAndSendNotices(newNotices, mailTemplate, mailAuth, config.SmtpConfiguration, &cache)
if err != nil {
logger.error(err)
} else {
recipientsNotified++
}
}
if recipientsNotified < 1 && err != nil {
logger.error("Couldn't send any mail notification!")
} else {
for id, t := range lastPublished {
persistent.data.(PersistentData).LastPublished[id] = t
persistent.save()
}
logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(noticesToBeSent)))
}
logger.info(fmt.Sprintf("Email notifications sent to %v of %v recipients", recipientsNotified, len(config.Recipients)))
}
dt := int(time.Now().UnixMilli() - t1)
t2 := time.Now().UnixMilli()
dt := int(t2 - t1)
time.Sleep(time.Millisecond * time.Duration((config.ApiFetchInterval * 1000) - dt))
}
}

View file

@ -25,7 +25,17 @@ type WidNotice struct {
PortalUrl string
}
func noticeSliceContains(notices []*WidNotice, notice *WidNotice) bool {
// func (n WidNotice) serialized() ([]byte, error) {
// return json.Marshal(n)
// }
// func NewWidNoticeFromJSON(data []byte) (WidNotice, error) {
// n := WidNotice{}
// err := json.Unmarshal(data, &n)
// return n, err
// }
func noticeSliceContains(notices []WidNotice, notice WidNotice) bool {
for _, x := range notices {
if x.Uuid == notice.Uuid {
return true

View file

@ -1,21 +0,0 @@
// Copyright (c) 2024 Julian MĂĽller (ChaoticByte)
package main
import (
"time"
)
type PersistentData struct {
// {endpoint id 1: time last published, endpoint id 2: ..., ...}
LastPublished map[string]time.Time `json:"last_published"`
}
func NewPersistentData(c Config) PersistentData {
// Initial persistent data
d := PersistentData{LastPublished: map[string]time.Time{}}
for _, e := range apiEndpoints {
d.LastPublished[e.Id] = time.Now().Add(-time.Hour * 24) // a day ago
}
return d
}

View file

@ -26,16 +26,7 @@ Affected Products:{{ range $product := .ProductNames }}
Assigned CVEs:{{ range $cve := .Cves }}
- {{ $cve }} -> https://www.cve.org/CVERecord?id={{ $cve }}
{{- end }}{{ end }}
Sent by WidNotifier {{ .WidNotifierVersion }}
`
type TemplateData struct {
*WidNotice
WidNotifierVersion string
}
{{- end }}{{ end }}`
type MailTemplateConfig struct {
SubjectTemplate string `json:"subject"`
@ -47,15 +38,19 @@ type MailTemplate struct {
BodyTemplate template.Template
}
func (t MailTemplate) generate(data TemplateData) (MailContent, error) {
func (t MailTemplate) generate(notice WidNotice) (MailContent, error) {
c := MailContent{}
buffer := &bytes.Buffer{}
err := t.SubjectTemplate.Execute(buffer, data)
if err != nil { return c, err }
err := t.SubjectTemplate.Execute(buffer, notice)
if err != nil {
return c, err
}
c.Subject = buffer.String()
buffer.Truncate(0) // we can recycle our buffer
err = t.BodyTemplate.Execute(buffer, data)
if err != nil { return c, err }
err = t.BodyTemplate.Execute(buffer, notice)
if err != nil {
return c, err
}
c.Body = buffer.String()
return c, nil
}

View file

@ -1,3 +0,0 @@
package main
var Version = "devel"

View file

@ -57,7 +57,9 @@ func (e ApiEndpoint) getNotices(since time.Time) ([]WidNotice, time.Time, error)
if err == nil {
if res.StatusCode == 200 {
resBody, err := io.ReadAll(res.Body)
if err != nil { return []WidNotice{}, since, err }
if err != nil {
return []WidNotice{}, since, err
}
var decodedData map[string]interface{}
if err = json.Unmarshal(resBody, &decodedData); err != nil {
return []WidNotice{}, since, err