WW-5400 Extend default configuration options for the CSP interceptor.
Previously, it was impossible to set global options for the CSP interceptor. The only options was to have every action individually implement CspSettingsAware.

To fix this, we add an interceptor parameter of defaultCspSettingsClassName. Values from this class will be used in the CSP header instead of DefaultCspSettings. Users may define their own custom class which implements CspSettings, and that will be the default for all actions that do not implement the CspSettingsAware interface. It is now possible to create this custom class by simply extending DefaultCspSettings.

I have fixed a spelling error in DefaultCspSettings.java -- cratePolicyFormat renamed to createPolicyFormat.
diff --git a/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java
index 32d6777..d382dce 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java
@@ -46,6 +46,9 @@
     private boolean prependServletContext = true;
     private boolean enforcingMode;
     private String reportUri;
+    private String reportTo;
+
+    private String defaultCspSettingsClassName = DefaultCspSettings.class.getName();
 
     @Override
     public String intercept(ActionInvocation invocation) throws Exception {
@@ -54,8 +57,24 @@
             LOG.trace("Using CspSettings provided by the action: {}", action);
             applySettings(invocation, ((CspSettingsAware) action).getCspSettings());
         } else {
-            LOG.trace("Using DefaultCspSettings with action: {}", action);
-            applySettings(invocation, new DefaultCspSettings());
+            LOG.trace("Using {} with action: {}", defaultCspSettingsClassName, action);
+
+            // if the defaultCspSettingsClassName is not a real class, throw an exception
+            try {
+                Class.forName(defaultCspSettingsClassName, false, Thread.currentThread().getContextClassLoader());
+            }
+            catch (ClassNotFoundException e) {
+                throw new IllegalArgumentException("The defaultCspSettingsClassName must be a real class.");
+            }
+
+            // if defaultCspSettingsClassName does not implement CspSettings, throw an exception
+            if (!CspSettings.class.isAssignableFrom(Class.forName(defaultCspSettingsClassName))) {
+                throw new IllegalArgumentException("The defaultCspSettingsClassName must implement CspSettings.");
+            }
+
+            CspSettings cspSettings = (CspSettings) Class.forName(defaultCspSettingsClassName)
+                    .getDeclaredConstructor().newInstance();
+            applySettings(invocation, cspSettings);
         }
         return invocation.invoke();
     }
@@ -76,6 +95,12 @@
             }
 
             cspSettings.setReportUri(finalReportUri);
+
+            // apply reportTo if set
+            if (reportTo != null) {
+                LOG.trace("Applying: {} to reportTo", reportTo);
+                cspSettings.setReportTo(reportTo);
+            }
         }
 
         invocation.addPreResultListener((actionInvocation, resultCode) -> {
@@ -97,6 +122,10 @@
         this.reportUri = reportUri;
     }
 
+    public void setReportTo(String reportTo) {
+        this.reportTo = reportTo;
+    }
+
     private Optional<URI> buildUri(String reportUri) {
         try {
             return Optional.of(URI.create(reportUri));
@@ -124,4 +153,11 @@
         this.prependServletContext = prependServletContext;
     }
 
-}
+    /**
+     * Sets the class name of the default {@link CspSettings} implementation to use when the action does not
+     * set its own values. If not set, the default is {@link DefaultCspSettings}.
+     */
+    public void setDefaultCspSettingsClassName(String defaultCspSettingsClassName) {
+        this.defaultCspSettingsClassName = defaultCspSettingsClassName;
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java b/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java
index acb1429..3b2cadb 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java
@@ -37,6 +37,7 @@
     String SCRIPT_SRC = "script-src";
     String BASE_URI = "base-uri";
     String REPORT_URI = "report-uri";
+    String REPORT_TO = "report-to";
     String NONE = "none";
     String STRICT_DYNAMIC = "strict-dynamic";
     String HTTP = "http:";
@@ -57,6 +58,11 @@
     void setReportUri(String uri);
 
     /**
+     * Sets the report group where csp violation reports will be sent
+     */
+    void setReportTo(String group);
+
+    /**
      * Sets CSP headers in enforcing mode when true, and report-only when false
      */
     void setEnforcingMode(boolean value);
diff --git a/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java b/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java
index d1768e8..51c76cf 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java
@@ -20,6 +20,7 @@
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.apache.struts2.action.CspSettingsAware;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -31,7 +32,11 @@
 
 /**
  * Default implementation of {@link CspSettings}.
- * The default policy implements strict CSP with a nonce based approach and follows the guide: <a href="https://csp.withgoogle.com/docs/index.html">https://csp.withgoogle.com/docs/index.html/</a>
+ * The default policy implements strict CSP with a nonce based approach and follows the guide:
+ * <a href="https://csp.withgoogle.com/docs/index.html">https://csp.withgoogle.com/docs/index.html/</a>
+ * You may extend or replace this class if you wish to customize the default policy further, and use your class
+ * by setting the {@link CspInterceptor} defaultCspSettingsClassName parameter. Actions that
+ * implement the {@link CspSettingsAware} interface will ignore the defaultCspSettingsClassName parameter.
  *
  * @see CspSettings
  * @see CspInterceptor
@@ -42,20 +47,22 @@
 
     private final SecureRandom sRand = new SecureRandom();
 
-    private String reportUri;
+    protected String reportUri;
+    protected String reportTo;
     // default to reporting mode
-    private String cspHeader = CSP_REPORT_HEADER;
+    protected String cspHeader = CSP_REPORT_HEADER;
 
     @Override
     public void addCspHeaders(HttpServletResponse response) {
         throw new UnsupportedOperationException("Unsupported implementation, use #addCspHeaders(HttpServletRequest request, HttpServletResponse response)");
     }
 
+    @Override
     public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) {
         if (isSessionActive(request)) {
             LOG.trace("Session is active, applying CSP settings");
             associateNonceWithSession(request);
-            response.setHeader(cspHeader, cratePolicyFormat(request));
+            response.setHeader(cspHeader, createPolicyFormat(request));
         } else {
             LOG.trace("Session is not active, ignoring CSP settings");
         }
@@ -70,7 +77,7 @@
         request.getSession().setAttribute("nonce", nonceValue);
     }
 
-    private String cratePolicyFormat(HttpServletRequest request) {
+    protected String createPolicyFormat(HttpServletRequest request) {
         StringBuilder policyFormatBuilder = new StringBuilder()
             .append(OBJECT_SRC)
             .append(format(" '%s'; ", NONE))
@@ -84,13 +91,18 @@
         if (reportUri != null) {
             policyFormatBuilder
                 .append(REPORT_URI)
-                .append(format(" %s", reportUri));
+                .append(format(" %s; ", reportUri));
+            if(reportTo != null) {
+                policyFormatBuilder
+                        .append(REPORT_TO)
+                        .append(format(" %s; ", reportTo));
+            }
         }
 
         return format(policyFormatBuilder.toString(), getNonceString(request));
     }
 
-    private String getNonceString(HttpServletRequest request) {
+    protected String getNonceString(HttpServletRequest request) {
         Object nonce = request.getSession().getAttribute("nonce");
         return Objects.toString(nonce);
     }
@@ -101,20 +113,28 @@
         return ret;
     }
 
+    @Override
     public void setEnforcingMode(boolean enforcingMode) {
         if (enforcingMode) {
             cspHeader = CSP_ENFORCE_HEADER;
         }
     }
 
+    @Override
     public void setReportUri(String reportUri) {
         this.reportUri = reportUri;
     }
 
     @Override
+    public void setReportTo(String reportTo) {
+        this.reportTo = reportTo;
+    }
+
+    @Override
     public String toString() {
         return "DefaultCspSettings{" +
             "reportUri='" + reportUri + '\'' +
+            "reportTo='" + reportTo + '\'' +
             ", cspHeader='" + cspHeader + '\'' +
             '}';
     }
diff --git a/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java
index 0b03c6e..cd59c34 100644
--- a/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java
+++ b/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java
@@ -31,6 +31,7 @@
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
 import static org.junit.Assert.assertNotEquals;
@@ -74,8 +75,10 @@
 
     public void testEnforcingCspHeadersSet() throws Exception {
         String reportUri = "/csp-reports";
+        String reportTo =  "csp-group";
         boolean enforcingMode = true;
         interceptor.setReportUri(reportUri);
+        interceptor.setReportTo(reportTo);
         interceptor.setEnforcingMode(enforcingMode);
         session.setAttribute("nonce", "foo");
 
@@ -84,13 +87,15 @@
         assertNotNull("Nonce key does not exist", session.getAttribute("nonce"));
         assertFalse("Nonce value is empty", Strings.isEmpty((String) session.getAttribute("nonce")));
         assertNotEquals("New nonce value couldn't be set", "foo", session.getAttribute("nonce"));
-        checkHeader(reportUri, enforcingMode);
+        checkHeader(reportUri, reportTo, enforcingMode);
     }
 
     public void testReportingCspHeadersSet() throws Exception {
         String reportUri = "/csp-reports";
+        String reportTo =  "csp-group";
         boolean enforcingMode = false;
         interceptor.setReportUri(reportUri);
+        interceptor.setReportTo(reportTo);
         interceptor.setEnforcingMode(enforcingMode);
         session.setAttribute("nonce", "foo");
 
@@ -98,7 +103,7 @@
 
         assertNotNull("Nonce value is empty", session.getAttribute("nonce"));
         assertNotEquals("New nonce value couldn't be set", "foo", session.getAttribute("nonce"));
-        checkHeader(reportUri, enforcingMode);
+        checkHeader(reportUri, reportTo, enforcingMode);
     }
 
     public void test_uriSetOnlyWhenSetIsCalled() throws Exception {
@@ -174,7 +179,47 @@
         checkHeader("/report-uri", enforcingMode);
     }
 
+    public void testInvalidDefaultCspSettingsClassName() throws Exception {
+        boolean enforcingMode = true;
+        mai.setAction(new TestAction());
+        request.setContextPath("/app");
+
+        interceptor.setEnforcingMode(enforcingMode);
+        interceptor.setReportUri("/report-uri");
+        interceptor.setPrependServletContext(false);
+
+        try {
+            interceptor.setDefaultCspSettingsClassName("foo");
+            interceptor.intercept(mai);
+            assert (false);
+        } catch (IllegalArgumentException e) {
+            assert (true);
+        }
+    }
+
+    public void testCustomDefaultCspSettingsClassName() throws Exception {
+        boolean enforcingMode = true;
+        mai.setAction(new TestAction());
+        request.setContextPath("/app");
+
+        interceptor.setEnforcingMode(enforcingMode);
+        interceptor.setReportUri("/report-uri");
+        interceptor.setPrependServletContext(false);
+        interceptor.setDefaultCspSettingsClassName(CustomDefaultCspSettings.class.getName());
+
+        interceptor.intercept(mai);
+
+        String header = response.getHeader(CspSettings.CSP_ENFORCE_HEADER);
+
+        // no other customization matters for this particular class
+        assertEquals("foo", header);
+    }
+
     public void checkHeader(String reportUri, boolean enforcingMode) {
+        checkHeader(reportUri, null, enforcingMode);
+    }
+
+    public void checkHeader(String reportUri, String reportTo, boolean enforcingMode) {
         String expectedCspHeader;
         if (Strings.isEmpty(reportUri)) {
             expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; ",
@@ -183,12 +228,23 @@
                 CspSettings.BASE_URI, CspSettings.NONE
             );
         } else {
-            expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s",
-                CspSettings.OBJECT_SRC, CspSettings.NONE,
-                CspSettings.SCRIPT_SRC, session.getAttribute("nonce"), CspSettings.STRICT_DYNAMIC, CspSettings.HTTP, CspSettings.HTTPS,
-                CspSettings.BASE_URI, CspSettings.NONE,
-                CspSettings.REPORT_URI, reportUri
-            );
+            if (Strings.isEmpty(reportTo)) {
+                expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s; ",
+                        CspSettings.OBJECT_SRC, CspSettings.NONE,
+                        CspSettings.SCRIPT_SRC, session.getAttribute("nonce"), CspSettings.STRICT_DYNAMIC, CspSettings.HTTP, CspSettings.HTTPS,
+                        CspSettings.BASE_URI, CspSettings.NONE,
+                        CspSettings.REPORT_URI, reportUri
+                );
+            }
+            else {
+                expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s; %s %s; ",
+                        CspSettings.OBJECT_SRC, CspSettings.NONE,
+                        CspSettings.SCRIPT_SRC, session.getAttribute("nonce"), CspSettings.STRICT_DYNAMIC, CspSettings.HTTP, CspSettings.HTTPS,
+                        CspSettings.BASE_URI, CspSettings.NONE,
+                        CspSettings.REPORT_URI, reportUri,
+                        CspSettings.REPORT_TO, reportTo
+                );
+            }
         }
 
         String header;
@@ -230,4 +286,15 @@
             return settings;
         }
     }
+
+    /**
+     * Custom DefaultCspSettings class that overrides the createPolicyFormat method
+     * to return a fixed value.
+     */
+    public static class CustomDefaultCspSettings extends DefaultCspSettings {
+
+        protected String createPolicyFormat(HttpServletRequest request) {
+            return "foo";
+        }
+    }
 }