Skip to content
Snippets Groups Projects
collector_test.go 8.55 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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)
    	}
    }