From b1dfcf915363bc8c704bf04c0ad91fea3738b63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Hedenstr=C3=B6m?= <erik@hedenstroem.com> Date: Wed, 16 Feb 2022 01:25:51 +0100 Subject: [PATCH] Unzipping and XML parsing completed --- cmd/subscribe.go | 96 ++++++++++++++++++++++++++++++ go.mod | 4 ++ go.sum | 6 ++ types/dmarc.go | 149 +++++++++++++++++++++++++++++++++++++++++++++++ types/email.go | 15 +++++ types/zip.go | 12 ++++ utils/zip.go | 34 +++++++++++ 7 files changed, 316 insertions(+) create mode 100644 cmd/subscribe.go create mode 100644 types/dmarc.go create mode 100644 types/email.go create mode 100644 types/zip.go create mode 100644 utils/zip.go diff --git a/cmd/subscribe.go b/cmd/subscribe.go new file mode 100644 index 0000000..3fa6ed2 --- /dev/null +++ b/cmd/subscribe.go @@ -0,0 +1,96 @@ +package cmd + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "net/url" + "os" + "os/signal" + "syscall" + + "github.com/davecgh/go-spew/spew" + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gitlab.hedenstroem.com/go/dmarc-prometheus-exporter/types" + "gitlab.hedenstroem.com/go/dmarc-prometheus-exporter/utils" +) + +var subscribeCmd = &cobra.Command{ + Use: "subscribe", + Short: "Subscribe to emails on an MQTT topic", + RunE: func(cmd *cobra.Command, args []string) error { + + opts, err := createClientOptions() + if err != nil { + return err + } + client := mqtt.NewClient(opts) + if token := client.Connect(); token.Wait() && token.Error() != nil { + return token.Error() + } + client.Subscribe(viper.GetString("MQTT_TOPIC"), 0, callback) + + keepAlive := make(chan os.Signal) + signal.Notify(keepAlive, os.Interrupt, syscall.SIGTERM) + <-keepAlive + + return nil + }, + DisableAutoGenTag: true, +} + +func callback(client mqtt.Client, msg mqtt.Message) { + var email types.Email + if err := json.Unmarshal(msg.Payload(), &email); err == nil { + for _, attachment := range email.Attachments { + if attachment.ContentType == "application/zip" { + files, _ := utils.UnzipBytes(attachment.Content.Data) + for _, file := range files { + if file.HasSuffix(".xml") { + var feedback types.Feedback + feedback.FromFile = file.Name + if err := xml.Unmarshal(file.Data, &feedback); err == nil { + spew.Dump(feedback) + } + } + } + } + } + } +} + +func createClientOptions() (*mqtt.ClientOptions, error) { + uri, err := url.Parse(viper.GetString("MQTT_URL")) + if err != nil { + return nil, err + } + var server string + switch uri.Scheme { + case "mqtts": + server = fmt.Sprintf("ssl://%s", uri.Host) + case "ws": + server = fmt.Sprintf("ws://%s", uri.Host) + default: + server = fmt.Sprintf("tcp://%s", uri.Host) + } + opts := mqtt.NewClientOptions() + opts.AddBroker(server) + if password, passwordSet := uri.User.Password(); passwordSet { + opts.SetUsername(uri.User.Username()) + opts.SetPassword(password) + } + opts.SetClientID(viper.GetString("MQTT_CLIENT_ID")) + return opts, nil +} + +func init() { + subscribeCmd.Flags().StringP("url", "u", "tcp://127.0.0.1:1883", "MQTT Broker URL") + subscribeCmd.Flags().StringP("topic", "t", "", "MQTT Topic") + subscribeCmd.Flags().StringP("id", "i", "dmarc-prometheus-exporter", "MQTT Client ID") + viper.BindPFlag("MQTT_URL", subscribeCmd.Flags().Lookup("url")) + viper.BindPFlag("MQTT_TOPIC", subscribeCmd.Flags().Lookup("topic")) + viper.BindPFlag("MQTT_CLIENT_ID", subscribeCmd.Flags().Lookup("id")) + RootCmd.AddCommand(subscribeCmd) +} diff --git a/go.mod b/go.mod index 537868d..66e0bd7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module gitlab.hedenstroem.com/go/dmarc-prometheus-exporter go 1.17 require ( + github.com/davecgh/go-spew v1.1.1 + github.com/eclipse/paho.mqtt.golang v1.3.5 github.com/joho/godotenv v1.4.0 github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.1 @@ -11,6 +13,7 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -22,6 +25,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect + golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index ac029e5..77ea11e 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= +github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -191,6 +193,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= @@ -428,6 +432,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -445,6 +450,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/types/dmarc.go b/types/dmarc.go new file mode 100644 index 0000000..276e7f7 --- /dev/null +++ b/types/dmarc.go @@ -0,0 +1,149 @@ +package types + +import ( + "bytes" + "encoding/xml" + "time" +) + +// Content is the structure for processing data +type Content struct { + From string + Name string + Data *bytes.Buffer +} + +// Row is the dmarc row in a report +type Row struct { + SourceIP string + Count int64 + EvalDisposition string + EvalSPFAlign string + EvalDKIMAalign string + Reason string + DKIMDomain string + DKIMResult string + SPFDomain string + SPFResult string + IdentifierHFrom string +} + +// Rows is jus the report and the rows of a report +type Rows struct { + Report Report + Rows []Row +} + +// Report is the content of the report +type Report struct { + ID int64 + ReportBegin time.Time + ReportEnd time.Time + PolicyDomain string + ReportOrg string + ReportID string + ReportEmail string + ReportExtraContactInfo string + PolicyAdkim string + PolicyAspf string + PolicyP string + PolicySP string + PolicyPCT string + Count int64 + DKIMResult string + SPFResult string + Items int +} + +// Reports is the collection of reports +type Reports struct { + Reports []Report + LastPage int + CurPage int + NextPage int + TotalPages int + Pages []int +} + +type dateRange struct { + XMLName xml.Name `xml:"date_range"` + Begin int64 `xml:"begin"` + End int64 `xml:"end"` +} + +type reportMetadata struct { + XMLName xml.Name `xml:"report_metadata"` + OrgName string `xml:"org_name"` + Email string `xml:"email"` + ExtraContactInfo string `xml:"extra_contact_info,omitempty"` + ReportID string `xml:"report_id"` + DateRange dateRange `xml:"date_range"` +} + +type policyPublished struct { + XMLName xml.Name `xml:"policy_published"` + Domain string `xml:"domain"` + ADKIM string `xml:"adkim"` + ASPF string `xml:"aspf"` + P string `xml:"p"` + SP string `xml:"sp"` + PCT string `xml:"pct"` +} + +type reason struct { + XMLName xml.Name `xml:"reason"` + Type string `xml:"type"` + Comment string `xml:"comment"` +} + +type policyEvaluated struct { + XMLName xml.Name `xml:"policy_evaluated"` + Disposition string `xml:"disposition"` + DKIM string `xml:"dkim"` + SPF string `xml:"spf"` + Reasons []reason `xml:"reason"` +} + +type row struct { + XMLName xml.Name `xml:"row"` + SourceIP string `xml:"source_ip"` + Count int64 `xml:"count"` + PolicyEvaluated policyEvaluated `xml:"policy_evaluated"` +} + +type identify struct { + XMLName xml.Name `xml:"identifiers"` + HeaderFrom string `xml:"header_from"` +} + +type spf struct { + XMLName xml.Name `xml:"spf"` + Result string `xml:"result"` +} + +type dkim struct { + XMLName xml.Name `xml:"dkim"` + Result string `xml:"result"` +} + +type authResult struct { + XMLName xml.Name `xml:"auth_results"` + SPF []spf `xml:"spf"` + DKIM []dkim `xml:"dkim"` +} + +type record struct { + XMLName xml.Name `xml:"record"` + Rows []row `xml:"row"` + Identifiers identify `xml:"identifiers"` + AuthResults authResult `xml:"auth_results"` +} + +// Feedback contains the reports and file information +type Feedback struct { + XMLName xml.Name `xml:"feedback"` + FromFile string + ReportMetadata reportMetadata `xml:"report_metadata"` + PolicyPublished policyPublished `xml:"policy_published"` + Records []record `xml:"record"` +} diff --git a/types/email.go b/types/email.go new file mode 100644 index 0000000..7ae1126 --- /dev/null +++ b/types/email.go @@ -0,0 +1,15 @@ +package types + +type Email struct { + Attachments []struct { + Type string `json:"type"` + ContentType string `json:"contentType"` + Filename string `json:"filename"` + Checksum string `json:"checksum"` + Size int `json:"size"` + Content struct { + Type string `json:"type"` + Data []byte `json:"data"` + } `json:"content"` + } `json:"attachments"` +} diff --git a/types/zip.go b/types/zip.go new file mode 100644 index 0000000..197da19 --- /dev/null +++ b/types/zip.go @@ -0,0 +1,12 @@ +package types + +import "strings" + +type ZipFile struct { + Name string + Data []byte +} + +func (z ZipFile) HasSuffix(suf string) bool { + return strings.HasSuffix(z.Name, suf) +} diff --git a/utils/zip.go b/utils/zip.go new file mode 100644 index 0000000..fd60f6f --- /dev/null +++ b/utils/zip.go @@ -0,0 +1,34 @@ +package utils + +import ( + "archive/zip" + "bytes" + "io/ioutil" + + "gitlab.hedenstroem.com/go/dmarc-prometheus-exporter/types" +) + +func UnzipBytes(b []byte) ([]types.ZipFile, error) { + zipReader, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) + if err != nil { + return nil, err + } + zfiles := make([]types.ZipFile, len(zipReader.File)) + for i, zipFile := range zipReader.File { + zfiles[i].Name = zipFile.Name + zfiles[i].Data, err = readZipFile(zipFile) + if err != nil { + return nil, err + } + } + return zfiles, nil +} + +func readZipFile(zf *zip.File) ([]byte, error) { + f, err := zf.Open() + if err != nil { + return nil, err + } + defer f.Close() + return ioutil.ReadAll(f) +} -- GitLab