Skip to content
Snippets Groups Projects
Select Git revision
  • a0e592ffe282c847c5ca7c0956aa70f8106ecc23
  • master default protected
  • renovate/bootstrap-5.x
  • renovate/purgecss-webpack-plugin-7.x
  • renovate/mini-css-extract-plugin-2.x
  • renovate/html-webpack-plugin-5.x
  • renovate/golang-1.x
  • renovate/css-loader-6.x
8 results

convert.go

Blame
    • ale's avatar
      0def9013
      Switch to a self-hosted binary, add graph-related code · 0def9013
      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).
      0def9013
      History
      Switch to a self-hosted binary, add graph-related code
      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).
    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
    }