alert-manager - Add strict CSP support.

See https://csp.withgoogle.com/docs/strict-csp.html for details
on strict CSP.

Bug: skia:
Change-Id: I6cd5243d47688b77263349ac971b270dc51241c6
Reviewed-on: https://skia-review.googlesource.com/c/180220
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/am/Makefile b/am/Makefile
index 4b23b42..6a938e9 100644
--- a/am/Makefile
+++ b/am/Makefile
@@ -11,7 +11,7 @@
 	./build_alert_to_pubsub_release
 
 debug:
-	npx webpack --mode=production
+	npx webpack --mode=development
 
 legacy_release: build package-lock.json
 	./build_legacy_alert_to_pubsub_release "`git log -n1 --format=%s`"
diff --git a/am/go/alert-manager/main.go b/am/go/alert-manager/main.go
index 5e84cc3..7a4efc7 100644
--- a/am/go/alert-manager/main.go
+++ b/am/go/alert-manager/main.go
@@ -16,10 +16,9 @@
 	"time"
 
 	"cloud.google.com/go/pubsub"
-	"github.com/99designs/goodies/http/secure_headers/csp"
 	"github.com/gorilla/csrf"
 	"github.com/gorilla/mux"
-	"github.com/unrolled/secure"
+	"github.com/skia-dev/secure"
 	"go.skia.org/infra/am/go/incident"
 	"go.skia.org/infra/am/go/note"
 	"go.skia.org/infra/am/go/silence"
@@ -235,6 +234,8 @@
 	if err := srv.templates.ExecuteTemplate(w, "index.html", map[string]string{
 		// base64 encode the csrf to avoid golang templating escaping.
 		"csrf": base64.StdEncoding.EncodeToString([]byte(csrf.Token(r))),
+		// Look in webpack.config.js for where the nonce templates are injected.
+		"nonce": secure.CSPNonce(r.Context()),
 	}); err != nil {
 		sklog.Errorf("Failed to expand template: %s", err)
 	}
@@ -586,20 +587,51 @@
 	}
 }
 
+// cspReportWrapper wraps a handler and intercepts csp failure reports and
+// turns them into structured log entries.
+//
+// Note this should be outside the csrf wrapper since execution may fail
+// before the csrf is in place.
+func cspReportWrapper(h http.Handler) http.Handler {
+	s := func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/cspreport" && r.Method == "POST" {
+			var body interface{}
+			if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+				sklog.Errorf("Failed to decode csp report: %s", err)
+				return
+			}
+			c := struct {
+				Type string      `json:"type"`
+				Body interface{} `json:"body"`
+			}{
+				Type: "csp",
+				Body: body,
+			}
+			b, err := json.Marshal(c)
+			if err != nil {
+				sklog.Errorf("Failed to marshal csp log entry: %s", err)
+				return
+			}
+			fmt.Println(string(b))
+			return
+		} else {
+			h.ServeHTTP(w, r)
+		}
+	}
+	return http.HandlerFunc(s)
+}
+
 func (srv *Server) applySecurityWrappers(h http.Handler) http.Handler {
 	// Configure Content Security Policy (CSP).
-	cspOpts := csp.Opts{
-		DefaultSrc: []string{csp.SourceNone},
-		ConnectSrc: []string{"https://skia.org", "https://skia-tree-status.appspot.com", csp.SourceSelf},
-		ImgSrc:     []string{csp.SourceSelf},
-		StyleSrc:   []string{csp.SourceSelf, csp.SourceUnsafeInline},
-		ScriptSrc:  []string{csp.SourceSelf},
-	}
-
+	addScriptSrc := ""
 	if *local {
 		// webpack uses eval() in development mode, so allow unsafe-eval when local.
-		cspOpts.ScriptSrc = append(cspOpts.ScriptSrc, "'unsafe-eval'")
+		addScriptSrc = "'unsafe-eval'"
 	}
+	// This non-local CSP string passes the tests at https://csp-evaluator.withgoogle.com/.
+	//
+	// See also: https://csp.withgoogle.com/docs/strict-csp.html
+	cspString := fmt.Sprintf("base-uri 'none';  img-src 'self' ; object-src 'none' ; style-src 'self' 'unsafe-inline' ; script-src 'strict-dynamic' $NONCE 'unsafe-inline' %s https: http: ; report-uri /cspreport ;", addScriptSrc)
 
 	// Apply CSP and other security minded headers.
 	secureMiddleware := secure.New(secure.Options{
@@ -609,7 +641,7 @@
 		SSLProxyHeaders:       map[string]string{"X-Forwarded-Proto": "https"},
 		STSSeconds:            60 * 60 * 24 * 365,
 		STSIncludeSubdomains:  true,
-		ContentSecurityPolicy: cspOpts.Header(),
+		ContentSecurityPolicy: cspString,
 		IsDevelopment:         *local,
 	})
 
@@ -670,6 +702,7 @@
 		h = login.ForceAuth(h, login.DEFAULT_REDIRECT_URL)
 		h = httputils.HealthzAndHTTPS(h)
 	}
+	h = cspReportWrapper(h)
 	http.Handle("/", h)
 	sklog.Infoln("Ready to serve.")
 	sklog.Fatal(http.ListenAndServe(*port, nil))
diff --git a/am/package.json b/am/package.json
index 9fb5cd9..f1fd543 100644
--- a/am/package.json
+++ b/am/package.json
@@ -11,13 +11,15 @@
   "dependencies": {
     "@webcomponents/custom-elements": "~1.2.1",
     "common-sk": "~3.1.0",
-    "infra-sk": "~0.8.1",
     "elements-sk": "~2.7.0",
+    "html-webpack-inject-attributes-plugin": "^1.0.1",
+    "infra-sk": "~0.8.1",
     "lit-html": "~0.14.0"
   },
   "devDependencies": {
     "chai": "~4.2.0",
     "copy-webpack-plugin": "~4.6.0",
+    "html-webpack-plugin": "^3.2.0",
     "karma": "~3.1.4",
     "karma-chai": "~0.1.0",
     "karma-chrome-launcher": "~2.2.0",
diff --git a/am/webpack.config.js b/am/webpack.config.js
index eb6174c..39e1f40 100644
--- a/am/webpack.config.js
+++ b/am/webpack.config.js
@@ -1,5 +1,6 @@
 const commonBuilder = require('pulito');
-const CopyWebpackPlugin = require('copy-webpack-plugin')
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin');
 
 module.exports = (env, argv) => {
   let config = commonBuilder(env, argv, __dirname);
@@ -17,5 +18,10 @@
       }
     ])
   );
+  config.plugins.push(
+    new HtmlWebpackInjectAttributesPlugin({
+      nonce: "{%.nonce%}",
+    }),
+  );
   return config;
 }