You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2020/01/07 17:38:13 UTC

[sling-org-apache-sling-xss] 01/01: SLING-8866 - Add reporting info in the XSS Protection API bundle

This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch issue/SLING-8866
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git

commit d8a4829e78b0f8f26e115e52f8e01a6f7c5d73e1
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Tue Jan 7 18:37:45 2020 +0100

    SLING-8866 - Add reporting info in the XSS Protection API bundle
    
    * added a counter metric (sling:xss.invalid_hrefs) to track the number of invalid
    URLs detected by org.apache.sling.xss.XSSFilter#isValidHref
    * enhanced the webconsole plugin to provide a detailed report of the blocked
    URLs
    * allow a system administrator to download the active AntiSamy configuration
---
 pom.xml                                            |  12 ++
 .../java/org/apache/sling/xss/impl/XSSAPIImpl.java |   5 -
 .../org/apache/sling/xss/impl/XSSFilterImpl.java   |  27 ++-
 .../xss/impl/XSSProtectionAPIWebConsolePlugin.java | 117 -------------
 .../apache/sling/xss/impl/status/FixedSizeMap.java |  57 +++++++
 .../sling/xss/impl/status/XSSStatusService.java    | 107 ++++++++++++
 .../XSSProtectionAPIWebConsolePlugin.java          | 185 +++++++++++++++++++++
 src/main/resources/webconsole/blocked.js           |  21 +++
 src/main/resources/webconsole/config.js            |  24 +++
 .../resources/{res/ui => webconsole}/prettify.css  |   6 +
 .../resources/{res/ui => webconsole}/prettify.js   |   0
 src/main/resources/webconsole/xss.css              |  24 +++
 src/main/resources/webconsole/xss.js               |  21 +++
 .../org/apache/sling/xss/impl/XSSAPIImplTest.java  |  19 ++-
 .../apache/sling/xss/impl/XSSFilterImplTest.java   |  18 +-
 15 files changed, 510 insertions(+), 133 deletions(-)

diff --git a/pom.xml b/pom.xml
index 370aac4..db431fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -258,6 +258,18 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.metrics</artifactId>
+            <version>1.2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+            <version>3.2.3</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
         </dependency>
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
index 958e365..68ed5c9 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
@@ -18,13 +18,8 @@ package org.apache.sling.xss.impl;
 
 import java.io.StringReader;
 import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.json.Json;
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
index 64cc434..ad9e5cd 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
@@ -35,9 +35,12 @@ import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
 import org.apache.sling.api.resource.observation.ResourceChange;
 import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
 import org.apache.sling.serviceusermapping.ServiceUserMapped;
 import org.apache.sling.xss.ProtectionContext;
 import org.apache.sling.xss.XSSFilter;
+import org.apache.sling.xss.impl.status.XSSStatusService;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.component.ComponentContext;
@@ -164,6 +167,15 @@ public class XSSFilterImpl implements XSSFilter {
     @Reference
     private ServiceUserMapped serviceUserMapped;
 
+    @Reference
+    private MetricsService metricsService;
+
+    @Reference
+    private XSSStatusService statusService;
+
+    private Counter invalidHrefs;
+    private static final String COUNTER_INVALID_HREFS = "xss.invalid_hrefs";
+
     @Override
     public boolean check(final ProtectionContext context, final String src) {
         final XSSFilterRule ctx = this.getFilterRule(context);
@@ -204,7 +216,7 @@ public class XSSFilterImpl implements XSSFilter {
         return false;
     }
 
-    AntiSamyPolicy getActivePolicy() {
+    public AntiSamyPolicy getActivePolicy() {
         return activePolicy;
     }
 
@@ -228,12 +240,17 @@ public class XSSFilterImpl implements XSSFilter {
                 }
             }
         }
+        if (!isValid) {
+            statusService.reportInvalidUrl(url);
+            invalidHrefs.increment();
+        }
         return isValid;
     }
 
     @Activate
     @Modified
     protected void activate(ComponentContext componentContext, Configuration configuration) {
+        invalidHrefs = metricsService.counter(COUNTER_INVALID_HREFS);
         // load default handler
         policyPath = configuration.policyPath();
         updatePolicy();
@@ -340,7 +357,7 @@ public class XSSFilterImpl implements XSSFilter {
         }
     }
 
-    class AntiSamyPolicy {
+    public class AntiSamyPolicy {
         private final boolean embedded;
         private final String path;
 
@@ -349,7 +366,7 @@ public class XSSFilterImpl implements XSSFilter {
             this.path = path;
         }
 
-        InputStream read() {
+        public InputStream read() {
             if (embedded) {
                 return this.getClass().getClassLoader().getResourceAsStream(EMBEDDED_POLICY_PATH);
             }
@@ -365,11 +382,11 @@ public class XSSFilterImpl implements XSSFilter {
             }
         }
 
-        boolean isEmbedded() {
+        public boolean isEmbedded() {
             return embedded;
         }
 
-        String getPath() {
+        public String getPath() {
             return path;
         }
     }
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSProtectionAPIWebConsolePlugin.java b/src/main/java/org/apache/sling/xss/impl/XSSProtectionAPIWebConsolePlugin.java
deleted file mode 100644
index 8e77e28..0000000
--- a/src/main/java/org/apache/sling/xss/impl/XSSProtectionAPIWebConsolePlugin.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- ~ Licensed to the Apache Software Foundation (ASF) under one
- ~ or more contributor license agreements.  See the NOTICE file
- ~ distributed with this work for additional information
- ~ regarding copyright ownership.  The ASF licenses this file
- ~ to you under the Apache License, Version 2.0 (the
- ~ "License"); you may not use this file except in compliance
- ~ with the License.  You may obtain a copy of the License at
- ~
- ~   http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing,
- ~ software distributed under the License is distributed on an
- ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- ~ KIND, either express or implied.  See the License for the
- ~ specific language governing permissions and limitations
- ~ under the License.
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
-package org.apache.sling.xss.impl;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-
-import javax.servlet.Servlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.apache.sling.xss.XSSFilter;
-import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.Reference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Component(
-        service = Servlet.class,
-        property = {
-                XSSProtectionAPIWebConsolePlugin.REG_PROP_LABEL + "=" + XSSProtectionAPIWebConsolePlugin.LABEL,
-                XSSProtectionAPIWebConsolePlugin.REG_PROP_TITLE + "=" + XSSProtectionAPIWebConsolePlugin.TITLE,
-                XSSProtectionAPIWebConsolePlugin.REG_PROP_CATEGORY + "=Sling"
-        }
-)
-public class XSSProtectionAPIWebConsolePlugin extends HttpServlet {
-
-    /*
-        do not replace the following constants with the ones from org.apache.felix, since you'll create a wiring to those APIs; the
-        current way this plugin is written allows it to optionally be available, if the Felix Web Console is installed on the OSGi
-        platform where this bundle will be deployed
-     */
-    static final String REG_PROP_LABEL = "felix.webconsole.label";
-    static final String REG_PROP_TITLE = "felix.webconsole.title";
-    static final String REG_PROP_CATEGORY = "felix.webconsole.category";
-    static final String LABEL = "xssprotection";
-    static final String TITLE= "XSS Protection";
-
-    private static final String RES_LOC = LABEL + "/res/ui";
-    private static final Logger LOGGER = LoggerFactory.getLogger(XSSProtectionAPIWebConsolePlugin.class);
-
-    @Reference(target = "(component.name=org.apache.sling.xss.impl.XSSFilterImpl)")
-    private XSSFilter xssFilter;
-
-    @Override
-    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
-        if (request.getRequestURI().endsWith(RES_LOC + "/prettify.css")) {
-            try(InputStream cssStream = getClass().getClassLoader().getResourceAsStream("/res/ui/prettify.css")) {
-                if (cssStream != null) {
-                    response.setContentType("text/css");
-                    IOUtils.copy(cssStream, response.getOutputStream());
-                }
-            }
-        } else if (request.getRequestURI().endsWith(RES_LOC + "/prettify.js")) {
-            try(InputStream jsStream = getClass().getClassLoader().getResourceAsStream("/res/ui/prettify.js")) {
-                if (jsStream != null) {
-                    response.setContentType("application/javascript");
-                    IOUtils.copy(jsStream, response.getOutputStream());
-                }
-            }
-        } else {
-            if (xssFilter != null) {
-                XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
-                XSSFilterImpl.AntiSamyPolicy antiSamyPolicy = xssFilterImpl.getActivePolicy();
-                if (antiSamyPolicy != null) {
-                    Writer w = response.getWriter();
-                    w.write("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + RES_LOC + "/prettify.css\"></link>");
-                    w.write("<script type=\"text/javascript\" src=\"" + RES_LOC + "/prettify.js\"></script>");
-                    w.write("<script type=\"text/javascript\" src=\"" + RES_LOC + "/fsclassloader.js\"></script>");
-                    w.write("<script>$(document).ready(prettyPrint);</script>");
-                    w.write("<style>.prettyprint ol.linenums > li { list-style-type: decimal; } pre.prettyprint { white-space: pre-wrap; " +
-                            "}</style>");
-                    w.write("<p class=\"statline ui-state-highlight\">The current AntiSamy configuration ");
-                    if (antiSamyPolicy.isEmbedded()) {
-                        w.write("is the default one embedded in the org.apache.sling.xss bundle.");
-                    } else {
-                        w.write("is loaded from ");
-                        w.write(antiSamyPolicy.getPath());
-                        w.write(".");
-                    }
-                    w.write("</p>");
-                    String contents = "";
-                    try(InputStream configurationStream = antiSamyPolicy.read()) {
-                        contents = IOUtils.toString(configurationStream, StandardCharsets.UTF_8);
-                    } catch (Throwable t) {
-                        LOGGER.error("Unable to read policy file.", t);
-                    }
-                    w.write("<pre class=\"prettyprint linenums\">");
-                    w.write(StringEscapeUtils.escapeHtml4(contents));
-                    w.write("</pre>");
-                }
-            }
-
-        }
-    }
-}
diff --git a/src/main/java/org/apache/sling/xss/impl/status/FixedSizeMap.java b/src/main/java/org/apache/sling/xss/impl/status/FixedSizeMap.java
new file mode 100644
index 0000000..6504a22
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/status/FixedSizeMap.java
@@ -0,0 +1,57 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.status;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class FixedSizeMap<K, V> extends LinkedHashMap<K, V> {
+
+    private final int maxSize;
+
+    public FixedSizeMap(int initialCapacity, float loadFactor, int maxSize) {
+        super(initialCapacity, loadFactor);
+        this.maxSize = maxSize;
+    }
+
+    public FixedSizeMap(int initialCapacity, int maxSize) {
+        super(initialCapacity);
+        this.maxSize = maxSize;
+    }
+
+    public FixedSizeMap(int maxSize) {
+        super();
+        this.maxSize = maxSize;
+    }
+
+    public FixedSizeMap(Map<? extends K, ? extends V> m, int maxSize) {
+        super(m);
+        this.maxSize = maxSize;
+    }
+
+    public FixedSizeMap(int initialCapacity, float loadFactor, boolean accessOrder, int maxSize) {
+        super(initialCapacity, loadFactor, accessOrder);
+        this.maxSize = maxSize;
+    }
+
+    @Override
+    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+        return size() > maxSize;
+    }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java b/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java
new file mode 100644
index 0000000..d0e28f9
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java
@@ -0,0 +1,107 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * The {@code XSSLibraryStatusService} collects information about the way the XSS Protection API library is used.
+ */
+@Component(service = XSSStatusService.class)
+@Designate(ocd = XSSStatusService.Configuration.class)
+public class XSSStatusService {
+
+    @ObjectClassDefinition(
+            name = "Apache Sling XSS Status Service",
+            description = "The XSS Protection API Status Service provides various statistics about how the library was used."
+    )
+    @interface Configuration {
+        @AttributeDefinition(
+                name = "Maximum number of recorded invalid URLs",
+                description = "Once this number is reached, previously recorded invalid URLs will be discarded."
+        )
+        int maxNumberOfInvalidUrlsRecorded() default MAX_INVALID_URLS_RECORDED;
+    }
+
+    public static final int MAX_INVALID_URLS_RECORDED = 1000;
+
+    private Map<String, AtomicInteger> invalidUrls;
+
+    public void reportInvalidUrl(@NotNull String url) {
+        if (invalidUrls.containsKey(url)) {
+            invalidUrls.get(url).incrementAndGet();
+        } else {
+            invalidUrls.put(url, new AtomicInteger(1));
+        }
+    }
+
+    public Map<String, AtomicInteger> getInvalidUrls() {
+        synchronized (invalidUrls) {
+            return sortByNumericValue(invalidUrls);
+        }
+    }
+
+    @Activate
+    private void activate(Configuration configuration) {
+        invalidUrls = Collections.synchronizedMap(new FixedSizeMap<>(configuration.maxNumberOfInvalidUrlsRecorded()));
+    }
+
+    private static <K, V extends Comparable<? super V>> Map<K, V> sortByComparableValue(Map<K, V> map) {
+        List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
+        list.sort(Map.Entry.comparingByValue());
+
+        Map<K, V> result = new LinkedHashMap<>();
+        for (Map.Entry<K, V> entry : list) {
+            result.put(entry.getKey(), entry.getValue());
+        }
+        return result;
+    }
+
+    private static <K, V extends Number> Map<K, V> sortByNumericValue(Map<K, V> map) {
+        List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
+        list.sort((left, right) -> {
+            double leftNumber = left.getValue().doubleValue();
+            double rightNumber = right.getValue().doubleValue();
+            if (leftNumber < rightNumber) {
+                return -1;
+            } else if (leftNumber > rightNumber) {
+                return 1;
+            }
+            return 0;
+        });
+
+        Map<K, V> result = new LinkedHashMap<>();
+        for (Map.Entry<K, V> entry : list) {
+            result.put(entry.getKey(), entry.getValue());
+        }
+        return result;
+    }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java
new file mode 100644
index 0000000..c915993
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java
@@ -0,0 +1,185 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.webconsole;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.sling.xss.XSSFilter;
+import org.apache.sling.xss.impl.XSSFilterImpl;
+import org.apache.sling.xss.impl.status.XSSStatusService;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(
+        service = Servlet.class,
+        property = {
+                XSSProtectionAPIWebConsolePlugin.REG_PROP_LABEL + "=" + XSSProtectionAPIWebConsolePlugin.LABEL,
+                XSSProtectionAPIWebConsolePlugin.REG_PROP_TITLE + "=" + XSSProtectionAPIWebConsolePlugin.TITLE,
+                XSSProtectionAPIWebConsolePlugin.REG_PROP_CATEGORY + "=Sling"
+        }
+)
+public class XSSProtectionAPIWebConsolePlugin extends HttpServlet {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(XSSProtectionAPIWebConsolePlugin.class);
+    /*
+        do not replace the following constants with the ones from org.apache.felix, since you'll create a wiring to those APIs; the
+        current way this plugin is written allows it to optionally be available, if the Felix Web Console is installed on the OSGi
+        platform where this bundle will be deployed
+     */
+    static final String REG_PROP_LABEL = "felix.webconsole.label";
+    static final String REG_PROP_TITLE = "felix.webconsole.title";
+    static final String REG_PROP_CATEGORY = "felix.webconsole.category";
+    static final String LABEL = "xssprotection";
+    static final String TITLE= "XSS Protection";
+
+    private static final String URI_ROOT = "/system/console/" + LABEL;
+    private static final String URI_CONFIG_XHR = URI_ROOT + "/config.xhr";
+    private static final String URI_BLOCKED_XHR = URI_ROOT + "/blocked.xhr";
+    private static final String URI_CONFIG_XML = URI_ROOT + "/config.xml";
+    private static final String INTERNAL_RESOURCES_FOLDER = "/webconsole";
+    private static final String RES_ROOT = URI_ROOT + INTERNAL_RESOURCES_FOLDER;
+    private static final String RES_URI_PRETTIFY_CSS = RES_ROOT + "/prettify.css";
+    private static final String RES_URI_PRETTIFY_JS = RES_ROOT + "/prettify.js";
+    private static final String RES_URI_XSS_CSS = RES_ROOT + "/xss.css";
+    private static final String RES_URI_XSS_JS = RES_ROOT + "/xss.js";
+    private static final String RES_URI_BLOCKED_JS = RES_ROOT + "/blocked.js";
+    private static final String RES_URI_CONFIG_JS = RES_ROOT + "/config.js";
+
+    @Reference(target = "(component.name=org.apache.sling.xss.impl.XSSFilterImpl)")
+    private XSSFilter xssFilter;
+
+    @Reference
+    private XSSStatusService statusService;
+
+    private static final Set<String> CSS_RESOURCES = new HashSet<>(Arrays.asList(RES_URI_PRETTIFY_CSS, RES_URI_XSS_CSS));
+    private static final Set<String> JS_RESOURCES = new HashSet<>(Arrays.asList(RES_URI_PRETTIFY_JS, RES_URI_XSS_JS, RES_URI_BLOCKED_JS,
+            RES_URI_CONFIG_JS));
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        String file = FilenameUtils.getName(request.getRequestURI());
+        if (file != null && CSS_RESOURCES.contains(request.getRequestURI())) {
+            try(InputStream cssStream =
+                        getClass().getClassLoader().getResourceAsStream(INTERNAL_RESOURCES_FOLDER + "/" + file)) {
+                if (cssStream != null) {
+                    response.setContentType("text/css");
+                    IOUtils.copy(cssStream, response.getOutputStream());
+                }
+            }
+        } else if (file != null && JS_RESOURCES.contains(request.getRequestURI())) {
+            try (InputStream jsStream =
+                         getClass().getClassLoader().getResourceAsStream(INTERNAL_RESOURCES_FOLDER + "/" + file)) {
+                if (jsStream != null) {
+                    response.setContentType("application/javascript");
+                    IOUtils.copy(jsStream, response.getOutputStream());
+                }
+            }
+        } else if (URI_CONFIG_XHR.equalsIgnoreCase(request.getRequestURI()) && xssFilter != null) {
+            response.setContentType("text/html");
+            XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
+            XSSFilterImpl.AntiSamyPolicy antiSamyPolicy = xssFilterImpl.getActivePolicy();
+            if (antiSamyPolicy != null) {
+                PrintWriter printWriter = response.getWriter();
+                printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_CONFIG_JS);
+                printWriter.write("<div id='config'>");
+                printWriter.printf("<link rel='stylesheet' type='text/css' href='%s'></link>\n", RES_URI_PRETTIFY_CSS);
+                printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_PRETTIFY_JS);
+                printWriter.write("<p class='statline ui-state-highlight'>The current AntiSamy configuration ");
+                if (antiSamyPolicy.isEmbedded()) {
+                    printWriter.write("is the default one embedded in the org.apache.sling.xss bundle.");
+                } else {
+                    printWriter.printf("is loaded from %s.", antiSamyPolicy.getPath());
+                }
+                printWriter.write("<button style='float:right' type='button' id='download-config'>Download</button></p>");
+                String contents = "";
+                try (InputStream configurationStream = antiSamyPolicy.read()) {
+                    contents = IOUtils.toString(configurationStream, StandardCharsets.UTF_8);
+                } catch (Throwable t) {
+                    LOGGER.error("Unable to read policy file.", t);
+                }
+                printWriter.write("<pre class='prettyprint linenums'>");
+                printWriter.write(StringEscapeUtils.escapeHtml4(contents));
+                printWriter.write("</pre>");
+                printWriter.write("</div>");
+            }
+        } else if (URI_BLOCKED_XHR.equalsIgnoreCase(request.getRequestURI())) {
+            response.setContentType("text/html");
+            PrintWriter printWriter = response.getWriter();
+            printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_BLOCKED_JS);
+            printWriter.println("<div id='blocked'>");
+            printWriter.println("<div class='table'>");
+            printWriter.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Blocked URLs</div>");
+            printWriter.println("<table class='nicetable tablesorter' id='invalid-urls'>");
+            printWriter.println("<thead>");
+            printWriter.println("<tr>");
+            printWriter.println("<th class='header'>URL</th>");
+            printWriter.println("<th class='header'>Times Blocked</th>");
+            printWriter.println("</tr>");
+            printWriter.println("</thead>");
+            printWriter.println("<tbody>");
+            int i = 1;
+            for (Map.Entry<String, AtomicInteger> entry : statusService.getInvalidUrls().entrySet()) {
+                String cssClass = ((i++ %2) == 0 ? "even" : "odd");
+                printWriter.printf("<tr class='%s ui-state-default'>%n<td>%s</td><td>%d</td></tr>", cssClass, entry.getKey(),
+                        entry.getValue().intValue());
+            }
+            printWriter.println("</tbody>");
+            printWriter.println("</table>");
+            printWriter.println("</div>");
+            printWriter.println("</div>");
+            printWriter.println("</div>");
+        } else if (URI_CONFIG_XML.equalsIgnoreCase(request.getRequestURI()) && xssFilter != null) {
+            response.setContentType("application/xml");
+            response.setHeader("Content-Disposition", "attachment; filename=config.xml");
+            XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
+            IOUtils.copy(xssFilterImpl.getActivePolicy().read(), response.getOutputStream());
+            response.setStatus(HttpServletResponse.SC_OK);
+        } else {
+            PrintWriter printWriter = response.getWriter();
+            printWriter.printf("<link rel='stylesheet' type='text/css' href='%s'>\n", RES_URI_XSS_CSS);
+            printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_XSS_JS);
+            printWriter.println("<div id='xss-tabs'>");
+            printWriter.println("<ul>");
+            printWriter.printf("<li><a href='%s'><span>Blocked URLs</span></a></li>\n", URI_BLOCKED_XHR);
+            if (xssFilter != null) {
+                printWriter.printf("<li><a href='%s'><span>Active Configuration</span></a></li>\n", URI_CONFIG_XHR);
+            }
+            printWriter.println("</ul>");
+            printWriter.println("</div>");
+        }
+    }
+}
diff --git a/src/main/resources/webconsole/blocked.js b/src/main/resources/webconsole/blocked.js
new file mode 100644
index 0000000..cc26b4c
--- /dev/null
+++ b/src/main/resources/webconsole/blocked.js
@@ -0,0 +1,21 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+$(document).ready(function() {
+    $('#invalid-urls').tablesorter();
+});
diff --git a/src/main/resources/webconsole/config.js b/src/main/resources/webconsole/config.js
new file mode 100644
index 0000000..4080fa6
--- /dev/null
+++ b/src/main/resources/webconsole/config.js
@@ -0,0 +1,24 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+$(document).ready(function() {
+    prettyPrint();
+    $('#download-config').on('click', function () {
+        window.location = window.location + '/config.xml';
+    });
+});
diff --git a/src/main/resources/res/ui/prettify.css b/src/main/resources/webconsole/prettify.css
similarity index 91%
rename from src/main/resources/res/ui/prettify.css
rename to src/main/resources/webconsole/prettify.css
index 0920749..ebcc414 100644
--- a/src/main/resources/res/ui/prettify.css
+++ b/src/main/resources/webconsole/prettify.css
@@ -15,3 +15,9 @@
  * limitations under the License.
  */
 .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.clo,.opn,.pun{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.kwd,.tag,.typ{font-weight:700}.str{color:#060}.kwd{color:#006}.com{color:#600;font-style:italic}.typ{color:#404}.lit{color:#044}.clo,.opn,.pun{color:#440}.tag{color:#006}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px soli [...]
+.prettyprint ol.linenums > li {
+    list-style-type: decimal;
+}
+pre.prettyprint {
+    white-space: pre-wrap;
+}
diff --git a/src/main/resources/res/ui/prettify.js b/src/main/resources/webconsole/prettify.js
similarity index 100%
rename from src/main/resources/res/ui/prettify.js
rename to src/main/resources/webconsole/prettify.js
diff --git a/src/main/resources/webconsole/xss.css b/src/main/resources/webconsole/xss.css
new file mode 100644
index 0000000..91c83fc
--- /dev/null
+++ b/src/main/resources/webconsole/xss.css
@@ -0,0 +1,24 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+.ui-tabs .ui-tabs-panel {
+    display: block;
+    border-width: 0;
+    padding: 1em 4em;
+    background: none;
+}
diff --git a/src/main/resources/webconsole/xss.js b/src/main/resources/webconsole/xss.js
new file mode 100644
index 0000000..8da50e4
--- /dev/null
+++ b/src/main/resources/webconsole/xss.js
@@ -0,0 +1,21 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+$(document).ready(function() {
+    $('#xss-tabs').tabs();
+});
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
index 43981f5..e480b66 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
@@ -20,10 +20,14 @@ import java.util.HashMap;
 import java.util.regex.Pattern;
 
 import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
 import org.apache.sling.serviceusermapping.ServiceUserMapped;
 import org.apache.sling.testing.mock.sling.junit.SlingContext;
 import org.apache.sling.xss.XSSAPI;
+import org.apache.sling.xss.impl.status.XSSStatusService;
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.osgi.framework.ServiceReference;
@@ -35,7 +39,9 @@ import junit.framework.TestCase;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class XSSAPIImplTest {
 
@@ -55,12 +61,20 @@ public class XSSAPIImplTest {
      * The only exception currently is {@link #testGetValidHrefWithoutHrefConfig()}.
      */
     private void setUp() {
-        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
         context.registerInjectActivateService(new XSSFilterImpl());
         context.registerInjectActivateService(new XSSAPIImpl());
         xssAPI = context.getService(XSSAPI.class);
     }
 
+    @Before
+    public void before() {
+        MetricsService metricsService = mock(MetricsService.class);
+        when(metricsService.counter(anyString())).thenReturn(mock(Counter.class));
+        context.registerService(MetricsService.class, metricsService);
+        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
+        context.registerService(new XSSStatusService());
+    }
+
     @After
     public void tearDown() {
         xssAPI = null;
@@ -350,8 +364,7 @@ public class XSSAPIImplTest {
     }
 
     @Test
-    public void testGetValidHrefWithoutHrefConfig() throws Exception {
-        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
+    public void testGetValidHrefWithoutHrefConfig() {
         context.load().binaryFile("/configWithoutHref.xml", "/apps/sling/xss/configWithoutHref.xml");
         context.registerInjectActivateService(new XSSFilterImpl(), new HashMap<String, Object>(){{
             put("policyPath", "/apps/sling/xss/configWithoutHref.xml");
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
index f1f26e9..4c6ead3 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
@@ -18,17 +18,23 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.xss.impl;
 
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
 import org.apache.sling.serviceusermapping.ServiceUserMapped;
 import org.apache.sling.testing.mock.sling.junit.SlingContext;
 import org.apache.sling.xss.XSSFilter;
+import org.apache.sling.xss.impl.status.XSSStatusService;
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class XSSFilterImplTest {
 
@@ -42,11 +48,19 @@ public class XSSFilterImplTest {
         xssFilter = null;
     }
 
+    @Before
+    public void setUp() {
+        MetricsService metricsService = mock(MetricsService.class);
+        when(metricsService.counter(anyString())).thenReturn(mock(Counter.class));
+        context.registerService(MetricsService.class, metricsService);
+        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
+        context.registerService(new XSSStatusService());
+    }
+
     @Test
     public void testResourceBasedPolicy() {
         context.load().binaryFile(this.getClass().getClassLoader().getResourceAsStream(XSSFilterImpl.EMBEDDED_POLICY_PATH),
                 "/libs/" + XSSFilterImpl.DEFAULT_POLICY_PATH);
-        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
         context.registerInjectActivateService(new XSSFilterImpl());
         xssFilter = context.getService(XSSFilter.class);
         XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
@@ -57,7 +71,6 @@ public class XSSFilterImplTest {
 
     @Test
     public void testDefaultEmbeddedPolicy() {
-        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
         context.registerInjectActivateService(new XSSFilterImpl());
         xssFilter = context.getService(XSSFilter.class);
         XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
@@ -68,7 +81,6 @@ public class XSSFilterImplTest {
 
     @Test
     public void isValidHref() {
-        context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
         context.registerInjectActivateService(new XSSFilterImpl());
         xssFilter = context.getService(XSSFilter.class);
         checkIsValid("javascript:alert(1)", false);