package reportscollector import ( "io/ioutil" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/chrj/smtpd" ) type countingSink struct { counter int } func (c *countingSink) Send(e Event) { c.counter++ } func (c *countingSink) reset() { c.counter = 0 } func createTestCollector() (*countingSink, string, func()) { sink := new(countingSink) c := NewCollector( sink, new(ReportHandler), new(TLSRPTHandler), new(LegacyCSPHandler), 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) } } 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) { sink, uri, cleanup := createTestCollector() defer cleanup() doRequest(t, uri, "application/reports+json", reportsTestData) 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" }] }] }` func TestTLSRPT(t *testing.T) { sink, uri, cleanup := createTestCollector() defer cleanup() doRequest(t, uri, "application/tlsrpt+json", tlsrptTestData) if sink.counter != 3 { t.Fatalf("parsed %d events, expected 3", sink.counter) } } 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 } } } 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) } }