Select Git revision
-
ale authored
The app is now self-hosted instead of relying on the static-content standalone server, so we can eventually add dynamic code for graph serving. The static content serving has improved, with more consistent cache header management, as well as the capability of serving pre-compressed content. Additional code to implement the generation of dependency (flow) graphs in dot format was added (not hooked to the HTTP server yet).
ale authoredThe app is now self-hosted instead of relying on the static-content standalone server, so we can eventually add dynamic code for graph serving. The static content serving has improved, with more consistent cache header management, as well as the capability of serving pre-compressed content. Additional code to implement the generation of dependency (flow) graphs in dot format was added (not hooked to the HTTP server yet).
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
}