forked from jacobbednarz/go-csp-collector
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcsp_collector.go
233 lines (199 loc) · 5.96 KB
/
csp_collector.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
// CSPReport is the structure of the HTTP payload the system receives.
type CSPReport struct {
Body CSPReportBody `json:"csp-report"`
}
// CSPReportBody contains the fields that are nested within the
// violation report.
type CSPReportBody struct {
DocumentURI string `json:"document-uri"`
Referrer string `json:"referrer"`
BlockedURI string `json:"blocked-uri"`
ViolatedDirective string `json:"violated-directive"`
EffectiveDirective string `json:"effective-directive"`
OriginalPolicy string `json:"original-policy"`
Disposition string `json:"disposition"`
ScriptSample string `json:"script-sample"`
StatusCode interface{} `json:"status-code"`
}
var (
// Rev is set at build time and holds the revision that the package
// was created at.
Rev = "dev"
// Flag for toggling verbose output.
debugFlag bool
// Flag for toggling output format.
outputFormat string
// Flag for health check url.
healthCheckPath = "/_healthcheck"
// Shared defaults for the logger output. This ensures that we are
// using the same keys for the `FieldKey` values across both formatters.
logFieldMapDefaults = log.FieldMap{
log.FieldKeyTime: "timestamp",
log.FieldKeyLevel: "level",
log.FieldKeyMsg: "message",
}
// Path to file which has blocked URI's per line.
blockedURIfile string
// Default URI Filter list.
ignoredBlockedURIs = []string{
"resource://",
"chromenull://",
"chrome-extension://",
"safari-extension://",
"mxjscall://",
"webviewprogressproxy://",
"res://",
"mx://",
"safari-resource://",
"chromeinvoke://",
"chromeinvokeimmediate://",
"mbinit://",
"opera://",
"ms-appx://",
"ms-appx-web://",
"localhost",
"127.0.0.1",
"none://",
"about:blank",
"android-webview",
"ms-browser-extension",
"wvjbscheme://__wvjb_queue_message__",
"nativebaiduhd://adblock",
"bdvideo://error",
}
// TCP Port to listen on.
listenPort int
)
func init() {
log.SetOutput(os.Stdout)
log.SetLevel(log.InfoLevel)
}
func trimEmptyAndComments(s []string) []string {
var r []string
for _, str := range s {
if str == "" {
continue
}
// ignore comments
if strings.HasPrefix(str, "#") {
continue
}
r = append(r, str)
}
return r
}
func main() {
version := flag.Bool("version", false, "Display the version")
flag.BoolVar(&debugFlag, "debug", false, "Output additional logging for debugging")
flag.StringVar(&outputFormat, "output-format", "text", "Define how the violation reports are formatted for output.\nDefaults to 'text'. Valid options are 'text' or 'json'")
flag.StringVar(&blockedURIfile, "filter-file", "", "Blocked URI Filter file")
flag.IntVar(&listenPort, "port", 8080, "Port to listen on")
flag.StringVar(&healthCheckPath, "health-check-path", healthCheckPath, "Health checker path")
flag.Parse()
if *version {
fmt.Printf("csp-collector (%s)\n", Rev)
os.Exit(0)
}
if blockedURIfile != "" {
content, err := ioutil.ReadFile(blockedURIfile)
if err != nil {
fmt.Printf("Error reading Blocked File list: %s", blockedURIfile)
}
ignoredBlockedURIs = trimEmptyAndComments(strings.Split(string(content), "\n"))
}
if debugFlag {
log.SetLevel(log.DebugLevel)
}
if outputFormat == "json" {
log.SetFormatter(&log.JSONFormatter{
FieldMap: logFieldMapDefaults,
})
} else {
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
DisableLevelTruncation: true,
QuoteEmptyFields: true,
DisableColors: true,
FieldMap: logFieldMapDefaults,
})
}
log.Debug("Starting up...")
if blockedURIfile != "" {
log.Debugf("Using Filter list from file at: %s\n", blockedURIfile)
} else {
log.Debug("Using Filter list from internal list")
}
log.Debugf("Blocked URI List: %s", ignoredBlockedURIs)
log.Debugf("Listening on TCP Port: %s", strconv.Itoa(listenPort))
http.HandleFunc("/", handleViolationReport)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", strconv.Itoa(listenPort)), nil))
}
func handleViolationReport(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.URL.Path == healthCheckPath {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
log.WithFields(log.Fields{
"http_method": r.Method,
}).Debug("Received invalid HTTP method")
return
}
decoder := json.NewDecoder(r.Body)
var report CSPReport
err := decoder.Decode(&report)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
log.Debug(fmt.Sprintf("Unable to decode invalid JSON payload: %s", err))
return
}
defer r.Body.Close()
reportValidation := validateViolation(report)
if reportValidation != nil {
http.Error(w, reportValidation.Error(), http.StatusBadRequest)
log.Debug(fmt.Sprintf("Received invalid payload: %s", reportValidation.Error()))
return
}
metadatas, gotMetadata := r.URL.Query()["metadata"]
var metadata string
if gotMetadata {
metadata = metadatas[0]
}
log.WithFields(log.Fields{
"document_uri": report.Body.DocumentURI,
"referrer": report.Body.Referrer,
"blocked_uri": report.Body.BlockedURI,
"violated_directive": report.Body.ViolatedDirective,
"effective_directive": report.Body.EffectiveDirective,
"original_policy": report.Body.OriginalPolicy,
"disposition": report.Body.Disposition,
"script_sample": report.Body.ScriptSample,
"status_code": report.Body.StatusCode,
"metadata": metadata,
}).Info()
}
func validateViolation(r CSPReport) error {
for _, value := range ignoredBlockedURIs {
if strings.HasPrefix(r.Body.BlockedURI, value) {
err := fmt.Errorf("blocked URI ('%s') is an invalid resource", value)
return err
}
}
if !strings.HasPrefix(r.Body.DocumentURI, "http") {
return fmt.Errorf("document URI ('%s') is invalid", r.Body.DocumentURI)
}
return nil
}