Skip to content
Snippets Groups Projects
collector_test.go 8.55 KiB
Newer Older
ale's avatar
ale committed
package reportscollector

import (
	"io/ioutil"
ale's avatar
ale committed
	"net/http"
	"net/http/httptest"
	"path/filepath"
ale's avatar
ale committed
	"strings"
	"testing"

	"github.com/chrj/smtpd"
ale's avatar
ale committed
)

type countingSink struct {
	counter int
}

func (c *countingSink) Send(e Event) {
	c.counter++
}

func (c *countingSink) reset() { c.counter = 0 }

ale's avatar
ale committed
func createTestCollector() (*countingSink, string, func()) {
	sink := new(countingSink)
	c := NewCollector(
		sink,
		new(ReportHandler),
		new(TLSRPTHandler),
ale's avatar
ale committed
		new(LegacyCSPHandler),
ale's avatar
ale committed
		new(DMARCHandler),
	)
	srv := httptest.NewServer(c)
	return sink, srv.URL, func() {
		srv.Close()
	}
}

func doRequest(t *testing.T, uri, contentType, data string) {
	req, err := http.NewRequest("POST", uri, strings.NewReader(data))
	if err != nil {
		t.Fatalf("NewRequest failed: %v", err)
	}
	req.Header.Set("Content-Type", contentType)
	client := new(http.Client)
	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		t.Fatalf("request failed with status %d: %s", resp.StatusCode, resp.Status)
	}
}

ale's avatar
ale committed
var reportsTestData = `[{
  "type": "csp",
  "age": 10,
  "url": "https://example.com/vulnerable-page/",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
  "body": {
    "blocked": "https://evil.com/evil.js",
    "directive": "script-src",
    "policy": "script-src 'self'; object-src 'none'",
    "status": 200,
    "referrer": "https://evil.com/"
  }
}, {
  "type": "hpkp",
  "age": 32,
  "url": "https://www.example.com/",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
  "body": {
    "date-time": "2014-04-06T13:00:50Z",
    "hostname": "www.example.com",
    "port": 443,
    "effective-expiration-date": "2014-05-01T12:40:50Z",
    "include-subdomains": false,
    "served-certificate-chain": [
      "-----BEGIN CERTIFICATE-----\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\n-----END CERTIFICATE-----"
    ]
  }
}, {
  "type": "nel",
  "age": 29,
  "url": "https://example.com/thing.js",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
  "body": {
    "referrer": "https://www.example.com/",
    "server-ip": "234.233.232.231",
    "protocol": "",
    "status-code": 0,
    "elapsed-time": 143,
    "age": 0,
    "type": "http.dns.name_not_resolved"
  }
}]`

func TestBrowserReports(t *testing.T) {
ale's avatar
ale committed
	sink, uri, cleanup := createTestCollector()
	defer cleanup()
ale's avatar
ale committed

ale's avatar
ale committed
	doRequest(t, uri, "application/reports+json", reportsTestData)
ale's avatar
ale committed

	if sink.counter != 3 {
		t.Fatalf("parsed %d events, expected 3", sink.counter)
	}
}

var tlsrptTestData = `{
  "organization-name": "Company-X",
  "date-range": {
    "start-datetime": "2016-04-01T00:00:00Z",
    "end-datetime": "2016-04-01T23:59:59Z"
  },
  "contact-info": "sts-reporting@company-x.example",
  "report-id": "5065427c-23d3-47ca-b6e0-946ea0e8c4be",
  "policies": [{
    "policy": {
      "policy-type": "sts",
      "policy-string": ["version: STSv1","mode: testing",
	    "mx: *.mail.company-y.example","max_age: 86400"],
      "policy-domain": "company-y.example",
      "mx-host": "*.mail.company-y.example"
    },
    "summary": {
      "total-successful-session-count": 5326,
      "total-failure-session-count": 303
    },
    "failure-details": [{
      "result-type": "certificate-expired",
      "sending-mta-ip": "2001:db8:abcd:0012::1",
      "receiving-mx-hostname": "mx1.mail.company-y.example",
      "failed-session-count": 100
    }, {
      "result-type": "starttls-not-supported",
      "sending-mta-ip": "2001:db8:abcd:0013::1",
      "receiving-mx-hostname": "mx2.mail.company-y.example",
      "receiving-ip": "203.0.113.56",
      "failed-session-count": 200,
      "additional-information": "https://reports.company-x.example/\n        report_info ? id = 5065427 c - 23 d3# StarttlsNotSupported "
    }, {
      "result-type": "validation-failure",
      "sending-mta-ip": "198.51.100.62",
      "receiving-ip": "203.0.113.58",
      "receiving-mx-hostname": "mx-backup.mail.company-y.example",
      "failed-session-count": 3,
      "failure-reason-code": "X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED"
    }]
  }]
}`

ale's avatar
ale committed
func TestTLSRPT(t *testing.T) {
	sink, uri, cleanup := createTestCollector()
	defer cleanup()
ale's avatar
ale committed

ale's avatar
ale committed
	doRequest(t, uri, "application/tlsrpt+json", tlsrptTestData)
ale's avatar
ale committed

	if sink.counter != 3 {
		t.Fatalf("parsed %d events, expected 3", sink.counter)
	}
}
ale's avatar
ale committed

var dmarcTestData1 = `<?xml version="1.0"?>
<feedback>
	<version>0.1</version>
	<report_metadata>
		<org_name>AMAZON-SES</org_name>
		<email>postmaster@amazonses.com</email>
		<report_id>bf9cb2b6-cdd5-49c3-bfb4-38cc8c29b8e4</report_id>
		<date_range>
			<begin>1602806400</begin>
			<end>1602892800</end>
		</date_range>
	</report_metadata>
	<policy_published>
		<domain>example.com</domain>
		<adkim>r</adkim>
		<aspf>r</aspf>
		<p>none</p>
		<sp>none</sp>
		<pct>0</pct>
		<fo>0</fo>
	</policy_published>
	<record>
		<row>
			<source_ip>1.2.3.4</source_ip>
			<count>2</count>
			<policy_evaluated>
				<disposition>none</disposition>
				<dkim>fail</dkim>
				<spf>fail</spf>
			</policy_evaluated>
		</row>
		<identifiers>
			<envelope_from>example.com</envelope_from>
			<header_from>example.com</header_from>
		</identifiers>
		<auth_results>
			<spf>
				<domain>example.com</domain>
				<result>fail</result>
			</spf>
		</auth_results>
	</record>
</feedback>`

var dmarcTestData2 = `<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
 <report_metadata>
  <org_name>vodafone.it</org_name>
  <email>noreply-dmarc-support@vodafone.it</email>
  <report_id>lists.example.com:1602943261</report_id>
  <date_range>
   <begin>1602857214</begin>
   <end>1602940407</end>
  </date_range>
 </report_metadata>
 <policy_published>
  <domain>lists.example.com</domain>
  <adkim>r</adkim>
  <aspf>r</aspf>
  <p>none</p>
  <sp>none</sp>
  <pct>0</pct>
 </policy_published>
 <record>
  <row>
   <source_ip>1.2.3.4</source_ip>
   <count>1</count>
   <policy_evaluated>
    <disposition>none</disposition>
    <dkim>pass</dkim>
    <spf>pass</spf>
   </policy_evaluated>
  </row>
  <identifiers>
   <header_from>lists.example.com</header_from>
  </identifiers>
  <auth_results>
   <spf>
    <domain>lists.example.com</domain>
    <result>pass</result>
   </spf>
   <dkim>
    <domain>lists.example.com</domain>
    <result>pass</result>
   </dkim>
  </auth_results>
 </record>
 <record>
  <row>
   <source_ip>1.2.3.4</source_ip>
   <count>1</count>
   <policy_evaluated>
    <disposition>none</disposition>
    <dkim>pass</dkim>
    <spf>pass</spf>
   </policy_evaluated>
  </row>
  <identifiers>
   <header_from>lists.example.com</header_from>
  </identifiers>
  <auth_results>
   <spf>
    <domain>lists.example.com</domain>
    <result>pass</result>
   </spf>
   <dkim>
    <domain>lists.example.com</domain>
    <result>pass</result>
   </dkim>
  </auth_results>
 </record>
</feedback>`

func TestDMARC(t *testing.T) {
	sink, uri, cleanup := createTestCollector()
	defer cleanup()

	doRequest(t, uri, "text/xml", dmarcTestData1)

	if sink.counter != 1 {
		t.Fatalf("parsed %d events, expected 1", sink.counter)
	}
}

func TestDMARC_IgnoreSuccesses(t *testing.T) {
	sink, uri, cleanup := createTestCollector()
	defer cleanup()

	doRequest(t, uri, "text/xml", dmarcTestData2)

	if sink.counter != 0 {
		t.Fatalf("parsed %d events, expected 0", sink.counter)
	}
}
func TestDMARC_Emails(t *testing.T) {
	files, err := filepath.Glob("dmarc/email-samples/*.txt")
	if err != nil {
		t.Skip()
	}

	sink := new(countingSink)
	c := NewCollector(
		sink,
		new(DMARCHandler),
	)

	for _, testf := range files {
		data, _ := ioutil.ReadFile(testf)

		sink.reset()
		err := c.ServeSMTP(
			smtpd.Peer{},
			smtpd.Envelope{Data: data},
		)
		if err != nil {
			t.Errorf("error parsing %s: %v", testf, err)
			continue
		}
	}
}

ale's avatar
ale committed
var cspTestData = `{
  "csp-report": {
    "document-uri": "http://localhost:3000/content",
    "referrer": "",
    "violated-directive": "frame-src",
    "effective-directive": "frame-src",
    "original-policy": "default-src 'self'; frame-src https://*.mozilla.org; report-uri /report",
    "disposition": "enforce",
    "blocked-uri": "https://mozilla.org",
    "line-number": 1,
    "source-file": "http://localhost:3000/content",
    "status-code": 200,
    "script-sample": ""
  }
}`

func TestCSP(t *testing.T) {
	sink, uri, cleanup := createTestCollector()
	defer cleanup()

	doRequest(t, uri, "application/csp-report", cspTestData)

	if sink.counter != 1 {
		t.Fatalf("parsed %d events, expected 1", sink.counter)
	}
}