Select Git revision
jquery.cookie.min.js
dmarc.go 4.74 KiB
package reportscollector
import (
"archive/zip"
"bytes"
"compress/gzip"
"encoding/xml"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/jhillyerd/enmime"
)
type dmarcRecord struct {
Row struct {
SourceIP string `xml:"source_ip"`
Count int `xml:"count"`
PolicyEvaluated struct {
Disposition string `xml:"disposition"`
DKIM string `xml:"dkim"`
SPF string `xml:"spf"`
} `xml:"policy_evaluated"`
}
Identifiers struct {
EnvelopeFrom string `xml:"envelope_from"`
HeaderFrom string `xml:"header_from"`
} `xml:"identifiers"`
AuthResults struct {
DKIM struct {
Domain string `xml:"domain"`
Result string `xml:"result"`
} `xml:"dkim"`
SPF struct {
Domain string `xml:"domain"`
Result string `xml:"result"`
} `xml:"spf"`
} `xml:"auth_results"`
}
func (r *dmarcRecord) isFailure() bool {
return (r.AuthResults.DKIM.Result == "fail" ||
r.AuthResults.SPF.Result == "fail")
}
type dmarcPolicy struct {
Domain string `xml:"domain"`
ADKIM string `xml:"adkim"`
ASPF string `xml:"aspf"`
P string `xml:"p"`
SP string `xml:"sp"`
PCT float64 `xml:"pct"`
FO float64 `xml:"fo"`
}
type dmarcReport struct {
Name xml.Name `xml:"feedback"`
Version string `xml:"version"`
Metadata struct {
ReportID string `xml:"report_id"`
Organization string `xml:"org_name"`
Email string `xml:"email"`
DateRange struct {
StartSecs int64 `xml:"begin"`
EndSecs int64 `xml:"end"`
} `xml:"date_range"`
} `xml:"report_metadata"`
Policy *dmarcPolicy `xml:"policy_published"`
Records []*dmarcRecord `xml:"record"`
}
type DMARCHandler struct{}
func (h *DMARCHandler) Name() string { return "dmarc" }
func (h *DMARCHandler) parseDMARC(r io.Reader) ([]Event, error) {
var report dmarcReport
if err := xml.NewDecoder(r).Decode(&report); err != nil {
return nil, err
}
var events []Event
for _, rec := range report.Records {
if rec.isFailure() {
events = append(events, h.eventFromRecord(&report, rec))
}
}
return events, nil
}
func (h *DMARCHandler) Parse(contentType string, req *http.Request) ([]Event, error) {
var r io.Reader
switch contentType {
case "text/xml":
r = req.Body
case "application/gzip":
var err error
r, err = gzip.NewReader(req.Body)
if err != nil {
return nil, err
}
default:
return nil, ErrNoMatch
}
return h.parseDMARC(r)
}
func (h *DMARCHandler) ParseMIME(msg *enmime.Part) ([]Event, error) {
// Try plain text/xml first.
if part := msg.DepthMatchFirst(func(p *enmime.Part) bool {
return p.ContentType == "text/xml"
}); part != nil {
return h.parseDMARC(bytes.NewReader(part.Content))
}
// Try to decode a gzipped attachment (also detected based on
// the filename).
if part := msg.DepthMatchFirst(func(p *enmime.Part) bool {
return (p.ContentType == "application/gzip" ||
strings.HasSuffix(p.FileName, ".xml.gz"))
}); part != nil {
gz, err := gzip.NewReader(bytes.NewReader(part.Content))
if err != nil {
return nil, err
}
return h.parseDMARC(gz)
}
// Try to decode a ZIP attachment.
if part := msg.DepthMatchFirst(func(p *enmime.Part) bool {
return (p.ContentType == "application/zip" ||
strings.HasSuffix(p.FileName, ".zip"))
}); part != nil {
data, ok := extractXMLFromZip(part.Content)
if ok {
return h.parseDMARC(bytes.NewReader(data))
}
}
return nil, ErrNoMatch
}
func (h *DMARCHandler) eventFromRecord(report *dmarcReport, rec *dmarcRecord) Event {
e := make(Event)
e.Set("type", "dmarc")
e.Set("event_timestamp", time.Unix(report.Metadata.DateRange.EndSecs, 0))
e.Set("domain", report.Policy.Domain)
e.Set("report_id", report.Metadata.ReportID)
e.Set("report_organization", report.Metadata.Organization)
if asn, ok := lookupASN(rec.Row.SourceIP); ok {
e.Set("asn", asn)
}
e.Set("policy_evaluated_disposition", rec.Row.PolicyEvaluated.Disposition)
e.Set("policy_evaluated_dkim", rec.Row.PolicyEvaluated.DKIM)
e.Set("policy_evaluated_spf", rec.Row.PolicyEvaluated.SPF)
e.Set("dmarc_envelope_from", rec.Identifiers.EnvelopeFrom)
e.Set("dmarc_header_from", rec.Identifiers.HeaderFrom)
e.Set("dmarc_dkim", rec.AuthResults.DKIM.Result)
e.Set("dmarc_spf", rec.AuthResults.SPF.Result)
e.Set("failed_session_count", rec.Row.Count)
return e
}
// Extract the first file with a .xml extension from a ZIP archive.
func extractXMLFromZip(zipData []byte) ([]byte, bool) {
zr, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, false
}
for _, file := range zr.File {
if strings.HasSuffix(file.Name, ".xml") {
f, err := file.Open()
if err != nil {
return nil, false
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, false
}
return data, true
}
}
return nil, false
}