diff --git a/cmd/reports-collector/main.go b/cmd/reports-collector/main.go index f0cc25a5a9086ef2ae2a405ec9b6483ffeacd326..da1bd0c753f9ddf51076e1458bfdc08a6fc5a661 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 d4c05aa9192840a5795d97209171fb4570efe463..752fc23eb2ccbf1d788dcce03b63c4c99c63adbc 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 0000000000000000000000000000000000000000..8fe37ecca6a88d546060169b8af57e76e722a30a --- /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 +}