From dc6fe6bd3273f887202b1ab19452e8d1918c5874 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Tue, 20 Oct 2020 10:06:24 +0100
Subject: [PATCH] Add support for legacy CSP reports

These originate from "report-uri" (not "report-to") tags in CSP
headers, and appear as application/csp-report requests.
---
 cmd/reports-collector/main.go |  1 +
 collector_test.go             | 28 ++++++++++++++++
 csp.go                        | 62 +++++++++++++++++++++++++++++++++++
 3 files changed, 91 insertions(+)
 create mode 100644 csp.go

diff --git a/cmd/reports-collector/main.go b/cmd/reports-collector/main.go
index f0cc25a..da1bd0c 100644
--- a/cmd/reports-collector/main.go
+++ b/cmd/reports-collector/main.go
@@ -35,6 +35,7 @@ func main() {
 		new(rc.LogSink),
 		new(rc.ReportHandler),
 		new(rc.TLSRPTHandler),
+		new(rc.LegacyCSPHandler),
 		new(rc.DMARCHandler),
 	)
 
diff --git a/collector_test.go b/collector_test.go
index d4c05aa..752fc23 100644
--- a/collector_test.go
+++ b/collector_test.go
@@ -21,6 +21,7 @@ func createTestCollector() (*countingSink, string, func()) {
 		sink,
 		new(ReportHandler),
 		new(TLSRPTHandler),
+		new(LegacyCSPHandler),
 		new(DMARCHandler),
 	)
 	srv := httptest.NewServer(c)
@@ -288,3 +289,30 @@ func TestDMARC_IgnoreSuccesses(t *testing.T) {
 		t.Fatalf("parsed %d events, expected 0", sink.counter)
 	}
 }
+
+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)
+	}
+}
diff --git a/csp.go b/csp.go
new file mode 100644
index 0000000..8fe37ec
--- /dev/null
+++ b/csp.go
@@ -0,0 +1,62 @@
+package reportscollector
+
+import (
+	"encoding/json"
+	"net/http"
+	"time"
+)
+
+type legacyCSPReport struct {
+	DocumentURI        string `json:"document-uri"`
+	Referrer           string `json:"referrer"`
+	ViolatedDirective  string `json:"violated-directive"`
+	EffectiveDirective string `json:"effective-directive"`
+	OriginalPolicy     string `json:"original-policy"`
+	Disposition        string `json:"disposition"`
+	BlockedURI         string `json:"blocked-uri"`
+	LineNumber         int    `json:"line-number"`
+	SourceFile         string `json:"source-file"`
+	StatusCode         int    `json:"status-code"`
+}
+
+type legacyCSPReportContainer struct {
+	Report *legacyCSPReport `json:"csp-report"`
+}
+
+type LegacyCSPHandler struct{}
+
+func (h *LegacyCSPHandler) Parse(contentType string, req *http.Request) ([]Event, error) {
+	if contentType != "application/csp-report" {
+		return nil, ErrNoMatch
+	}
+
+	var cnt legacyCSPReportContainer
+	if err := json.NewDecoder(req.Body).Decode(&cnt); err != nil {
+		return nil, err
+	}
+
+	if cnt.Report == nil {
+		return nil, nil
+	}
+	return []Event{h.eventFromReport(req, cnt.Report)}, nil
+}
+
+func (h *LegacyCSPHandler) eventFromReport(req *http.Request, report *legacyCSPReport) Event {
+	e := make(Event)
+	if asn, ok := lookupASN(getRemoteIP(req)); ok {
+		e.Set("asn", asn)
+	}
+	e.Set("type", "csp")
+	e.Set("event_timestamp", time.Now())
+	e.Set("url", report.DocumentURI)
+	e.Set("domain", domainFromURL(report.DocumentURI))
+	e.Set("user_agent", req.Header.Get("User-Agent"))
+
+	// TODO: use the same fields as report-to CSP reports.
+	e.Set("csp_violated_directive", report.ViolatedDirective)
+	e.Set("csp_blocked_uri", report.BlockedURI)
+	e.Set("csp_source_file", report.SourceFile)
+	e.Set("csp_line_number", report.LineNumber)
+	e.Set("csp_status_code", report.StatusCode)
+	return e
+}
-- 
GitLab