diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..693e9c1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,79 @@
+# Created by https://www.gitignore.io/api/eclipse,java,maven
+
+### Eclipse ###
+*.pydevproject
+.metadata
+.gradle
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+
+# Eclipse Core
+.project
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# TeXlipse plugin
+.texlipse
+
+
+### Java ###
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+
+### Vault ###
+.vlt
+
+### IntelliJ ###
+.idea/
+*.iml
+
+.DS_Store
+/TODO.txt
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..1a09b0c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,15 @@
+language: java
+jdk:
+- openjdk9
+addons:
+ sonarcloud:
+ organization: valtech-avs
+ token:
+ secure: gPXlvZfPVk1JC+swRzh4hDNRMeYPCmPhxOVjmKrizTOnK0T3TOIAY2reUH0+U61fAAfobC4sO5gPsx0cy/diN+b2Ln/mOIY4T4S7be7jlXXuySTYwZSeI9IIOs+T3/OPko850GBU7IYrSFOyzae3OXs3j5R+nzg6stW1PXV3WceF+rZ2aSJbmcMIv17GHSDtxyYQkc/R9WW8OH2DOMng5LzJ3b4qv20Xzamt1povDX69YX+DXuGYErjBzcivwu33yNDxjX0a8AT51cLl6l31AZM7Z0TsAKMn3tPNObwKSGVwDjTfhgTUyH57MFF389om1BgxnZERiiTwYXYuVhm4rdSl8usw351LUO7VGtUCA/exh74R8R7uro+buoGmeOBKLp/NtOpvvVg4/c7YUi4KjuGdVua7b5XgoRsbAjFzHR2TeFw5eH5AzpTS6j1ryM8p+Os29fBJ72lVD35iFMZ8Vut5Fiz3chVitI25/94leU5+qWq1Rfh9s+4jJL/HldlGgXjII0lCFDa8up9ezbOn/j+9zo9vl4WiJgl9fSZ2KAQuFAhGkGWIda8ctjqwZmOwX2vF0F3eAZjWT+jNbyuZhmnR/K156fnEQHpTHVZkpgnJgK0Hc6P2XISbl5ZRxMTtJCbnZhCFnXp7xYcIfbWkokHzmLPGi6i3lXWtJfiVmF8=
+script:
+- mvn test javadoc:javadoc -B
+- sonar-scanner
+cache:
+ directories:
+ - "$HOME/.sonar/cache"
+ - "$HOME/.m2"
diff --git a/HISTORY b/HISTORY
new file mode 100644
index 0000000..704033c
--- /dev/null
+++ b/HISTORY
@@ -0,0 +1,4 @@
+
+2020 1.0.0
+ - initial release
+
\ No newline at end of file
diff --git a/README.md b/README.md
index ceaf901..9fa6fb4 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,192 @@
-# -aem-virus-check
-AEM Virus Check adds malware scanning to your AEM instance.
+# AEM Virus Scan
+
+AEM Virus Scan adds malware scanning to your AEM instance. It supports Clam AV out-of-the-box and can be extended to support more scan engines.
+
+Features:
+
+* Scanning of asset uploads
+* API to run scan from custom code
+* API to add custom scan engines
+* Provided scan engines:
+ * Clam AV
+* Health checks
+
+
+
+# Requirements
+
+AVS requires Java 8 and AEM 6.4 or above.
+
+| AEM Version | AVS |
+| ------------- | --------- |
+| 6.4 | 1.x |
+| 6.5 | 1.x |
+
+
+
+# Installation
+
+You can download the package from [Maven Central](http://repo1.maven.org/maven2/de/valtech/avs/avs.ui.apps/) or our [releases section](https://github.com/valtech/aem-virus-scan/releases). The avs.ui.apps package will install the AVS software.
+
+```xml
+
+ de.valtech.avs
+ avs.ui.apps
+ LATEST
+ zip
+
+```
+
+
+## Uninstallation
+
+The application can be removed by deleting the following paths:
+* /apps/valtech/avs
+* /var/avs
+
+Afterwards, you can delete the "avs.ui.apps" package in package manager.
+
+# Scan File Uploads
+
+You can scan files in any POST request. E.g. this way you can scan files that are uploaded to DAM.
+
+You should provide a [configuration](#conf_filter) of the URL patterns for the filter.
+
+# Tools
+
+AVS adds tools to the AEM menu.
+
+
+
+
+## Perform a Manual Scan
+
+You can upload a file using AVS scan tool from the menu. This allows you to check if there is an issue with the file.
+
+
+
+
+## History
+
+This shows the history of the last found infections. Scans that did not lead to an alert are not listed.
+
+
+
+# Configuration
+
+You can see an example for each configuration in [example package](/examples/src/main/content/jcr_root/apps/valtech/avs-examples/config).
+
+
+
+## AVS Post Filter
+
+This filter can scan e.g. asset uploads for viruses.
+
+* includePatterns: List of regular expressions to match the URLs to check. If empty, all non-excluded URLs are scanned.
+* excludePatterns: List of regular expressions to match the URLs to ignore. Has higher priority than include patterns.
+
+PID: de.valtech.avs.core.filter.AvsPostFilter
+
+File name: de.valtech.avs.core.filter.AvsPostFilter.xml
+
+```
+
+
+```
+
+
+
+## AVS Notification Mailer
+
+This filter can scan e.g. asset uploads for viruses.
+
+* subject: mail subject
+* body: body for all virus notification emails. Wildcards are ${FILE_NAME} for uploaded file name and ${SCAN_OUTPUT} for scan details.
+* isHtml: specifies if mail format is HTML or TEXT
+* from: FROM address for notification emails
+* additionalRecipients: additional email recipients
+
+PID: de.valtech.avs.core.mail.AvsNotificationMailer
+
+File name: de.valtech.avs.core.mail.AvsNotificationMailer.xml
+
+```
+
+
+```
+
+
+
+## Clam Scanning Engine
+
+You need to provide a configuration for Clam AV in case you want to use this scan engine. If no configuration is provided then it will not be activated.
+
+* command: command to scan a single file. The file name will be added at the end of the command.
+
+PID: de.valtech.avs.core.service.scanner.ClamScannerEngine
+
+File name: de.valtech.avs.core.service.scanner.ClamScannerEngine.xml
+
+```
+
+
+```
+
+
+
+## Purge History
+
+This configures how long to keep the scan history. Older entries will be purged with maintenance task.
+
+PID: de.valtech.avs.core.maintenance.PurgeHistoryTask
+
+File name: de.valtech.avs.core.maintenance.PurgeHistoryTask.xml
+
+```
+
+
+```
+
+# Health Checks
+
+Health checks show you the status of AVS. This includes its service user, scan engines and a test scan.
+You can access them on the [status page](http://localhost:4502/libs/granite/operations/content/healthreports/healthreportlist.html/system/sling/monitoring/mbeans/org/apache/sling/healthcheck/HealthCheck/avsHealthCheckmBean).
+
+
+
+
+
+# API Documentation
+
+TODO
+
+
+
+# License
+
+The AVS tool is licensed under the [MIT LICENSE](LICENSE).
+
+
+
+# Changelog
+
+Please see our [history file](HISTORY).
+
+
+
+# Developers
+
+See our [developer zone](docs/developers.md).
diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644
index 0000000..4c28d87
--- /dev/null
+++ b/api/pom.xml
@@ -0,0 +1,114 @@
+
+
+ 4.0.0
+
+ de.valtech.avs
+ avs
+ 0.9.0
+
+
+ avs.api
+ bundle
+ AVS - API
+ Api bundle for AVS
+
+
+
+
+ org.apache.felix
+ osgicheck-maven-plugin
+
+
+ biz.aQute.bnd
+ bnd-baseline-maven-plugin
+
+ false
+
+
+
+ org.apache.sling
+ maven-sling-plugin
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+
+ javax.inject;version=0.0.0,
+ javax.annotation;version=0.0.0,
+ *
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+
+
+
+
+ org.osgi
+ osgi.core
+
+
+ org.osgi
+ osgi.cmpn
+
+
+ org.osgi
+ osgi.annotation
+
+
+ org.slf4j
+ slf4j-api
+
+
+ javax.jcr
+ jcr
+
+
+ javax.servlet
+ javax.servlet-api
+
+
+ com.adobe.aem
+ uber-jar
+ apis
+
+
+ org.apache.sling
+ org.apache.sling.models.api
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ junit
+ junit
+
+
+ org.mockito
+ mockito-core
+
+
+ junit-addons
+ junit-addons
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+
diff --git a/api/src/main/java/de/valtech/avs/api/history/HistoryEntry.java b/api/src/main/java/de/valtech/avs/api/history/HistoryEntry.java
new file mode 100644
index 0000000..f671cd2
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/history/HistoryEntry.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.api.history;
+
+import java.util.Date;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Represents an entry in the AVS history.
+ *
+ * @author Roland Gruber
+ */
+@ProviderType
+public interface HistoryEntry {
+
+ /**
+ * Returns the scan time.
+ *
+ * @return time
+ */
+ Date getTime();
+
+ /**
+ * Returns the scan output.
+ *
+ * @return output
+ */
+ String getOutput();
+
+ /**
+ * Returns if the file was clean.
+ *
+ * @return clean
+ */
+ boolean isClean();
+
+ /**
+ * Returns the scanned node path if available.
+ *
+ * @return path
+ */
+ String getPath();
+
+ /**
+ * Path in repository.
+ *
+ * @return path
+ */
+ String getRepositoryPath();
+
+ /**
+ * Returns the user name.
+ *
+ * @return user name
+ */
+ String getUserId();
+
+}
diff --git a/api/src/main/java/de/valtech/avs/api/service/AvsException.java b/api/src/main/java/de/valtech/avs/api/service/AvsException.java
new file mode 100644
index 0000000..76aa82e
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/service/AvsException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.api.service;
+
+/**
+ * Thrown when the AVS service faces an error.
+ *
+ * @author Roland Gruber
+ */
+public class AvsException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructor
+ *
+ * @param message error message
+ * @param e original exception
+ */
+ public AvsException(String message, Throwable e) {
+ super(message, e);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param message error message
+ */
+ public AvsException(String message) {
+ super(message);
+ }
+
+}
diff --git a/api/src/main/java/de/valtech/avs/api/service/AvsService.java b/api/src/main/java/de/valtech/avs/api/service/AvsService.java
new file mode 100644
index 0000000..13f8677
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/service/AvsService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.api.service;
+
+import java.io.InputStream;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+import de.valtech.avs.api.service.scanner.ScanResult;
+
+/**
+ * Scanner service interface. Use this to scan for viruses.
+ *
+ * @author Roland Gruber
+ */
+@ProviderType
+public interface AvsService {
+
+ /**
+ * Scans the given content for viruses.
+ *
+ * @param content content
+ * @param userId user name
+ * @return scan result
+ * @throws AvsException error during scan
+ */
+ public ScanResult scan(InputStream content, String userId) throws AvsException;
+
+ /**
+ * Scans the given content for viruses.
+ *
+ * @param content content
+ * @param userId user name
+ * @param path node path to add in history
+ * @return scan result
+ * @throws AvsException error during scan
+ */
+ public ScanResult scan(InputStream content, String userId, String path) throws AvsException;
+
+ /**
+ * Returns if there is at least one active scan engine.
+ *
+ * @return scan engines available
+ */
+ public boolean hasActiveScanEngines();
+
+}
diff --git a/api/src/main/java/de/valtech/avs/api/service/package-info.java b/api/src/main/java/de/valtech/avs/api/service/package-info.java
new file mode 100644
index 0000000..37c62be
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/service/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package contains the service API for AEM Virus Scan. You can use this to integrate AVS into
+ * your own software.
+ *
+ * @author Roland Gruber
+ */
+@Version("1.0.0")
+package de.valtech.avs.api.service;
+
+import org.osgi.annotation.versioning.Version;
diff --git a/api/src/main/java/de/valtech/avs/api/service/scanner/AvsScannerEnine.java b/api/src/main/java/de/valtech/avs/api/service/scanner/AvsScannerEnine.java
new file mode 100644
index 0000000..596861d
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/service/scanner/AvsScannerEnine.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.api.service.scanner;
+
+import java.io.InputStream;
+
+import org.osgi.annotation.versioning.ConsumerType;
+
+import de.valtech.avs.api.service.AvsException;
+
+/**
+ * Interface for scanner engines. Do not use directly for scanning but to implement new scanner
+ * engines.
+ *
+ * @author Roland Gruber
+ */
+@ConsumerType
+public interface AvsScannerEnine {
+
+ /**
+ * Scans the given content for viruses.
+ *
+ * @param content content
+ * @return scan result
+ * @throws AvsException error during scan
+ */
+ ScanResult scan(InputStream content) throws AvsException;
+
+}
diff --git a/api/src/main/java/de/valtech/avs/api/service/scanner/ScanResult.java b/api/src/main/java/de/valtech/avs/api/service/scanner/ScanResult.java
new file mode 100644
index 0000000..6d9a781
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/service/scanner/ScanResult.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.api.service.scanner;
+
+/**
+ * Result of a single file scan.
+ *
+ * @author Roland Gruber
+ */
+public class ScanResult {
+
+ private boolean clean;
+
+ private String output;
+
+ private String path;
+
+ private String userId;
+
+ /**
+ * Constructor
+ *
+ * @param output command output
+ * @param clean file is clean
+ */
+ public ScanResult(String output, boolean clean) {
+ this.clean = clean;
+ this.output = output;
+ }
+
+ /**
+ * Returns if the file is clean.
+ *
+ * @return clean
+ */
+ public boolean isClean() {
+ return clean;
+ }
+
+ /**
+ * Returns the command output.
+ *
+ * @return output
+ */
+ public String getOutput() {
+ return output;
+ }
+
+ @Override
+ public String toString() {
+ return "Clean: " + Boolean.toString(clean) + "\n" + "Output: " + output;
+ }
+
+ /**
+ * Returns the path of the scanned node.
+ *
+ * @return path
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Sets the path of the scanned node.
+ *
+ * @param path path
+ */
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ /**
+ * Sets the user id.
+ *
+ * @param userId user id
+ */
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ /**
+ * Returns the user id.
+ *
+ * @return user id
+ */
+ public String getUserId() {
+ return userId;
+ }
+
+}
diff --git a/api/src/main/java/de/valtech/avs/api/service/scanner/package-info.java b/api/src/main/java/de/valtech/avs/api/service/scanner/package-info.java
new file mode 100644
index 0000000..cc99bba
--- /dev/null
+++ b/api/src/main/java/de/valtech/avs/api/service/scanner/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/**
+ * This package contains the scanner interface.
+ *
+ * @author Roland Gruber
+ */
+@Version("1.0.0")
+package de.valtech.avs.api.service.scanner;
+
+import org.osgi.annotation.versioning.Version;
diff --git a/api/src/main/javadoc/overview.html b/api/src/main/javadoc/overview.html
new file mode 100644
index 0000000..cf52d41
--- /dev/null
+++ b/api/src/main/javadoc/overview.html
@@ -0,0 +1,12 @@
+
+
+
+
+ AVS API Overview
+
+
+
+ This is the AEM Virus Scan (AVS) API documentation.
+
+
+
\ No newline at end of file
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 0000000..f1a50a7
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,160 @@
+
+
+ 4.0.0
+
+ de.valtech.avs
+ avs
+ 0.9.0
+
+
+ avs.core
+ bundle
+ AVS - Core
+ Core bundle for AVS
+
+
+
+
+ org.apache.felix
+ osgicheck-maven-plugin
+
+
+ org.apache.sling
+ maven-sling-plugin
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ de.valtech.avs.core
+
+ true
+
+ artifactId=velocity-engine-core
+
+
+ javax.annotation;version=0.0.0,
+ *
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ prepare-package
+
+ report
+
+
+
+ post-unit-test
+ test
+
+ report
+
+
+
+
+
+ org.owasp
+ dependency-check-maven
+
+
+
+
+
+
+ de.valtech.avs
+ avs.api
+ ${project.version}
+
+
+ org.apache.velocity
+ velocity-engine-core
+
+
+ javax.mail
+ javax.mail-api
+
+
+ org.osgi
+ osgi.core
+
+
+ org.osgi
+ osgi.cmpn
+
+
+ org.osgi
+ osgi.annotation
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.slf4j
+ slf4j-simple
+
+
+ javax.jcr
+ jcr
+
+
+ javax.servlet
+ javax.servlet-api
+
+
+ com.adobe.aem
+ uber-jar
+ apis
+
+
+ org.apache.sling
+ org.apache.sling.models.api
+
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ junit
+ junit
+
+
+ org.mockito
+ mockito-core
+
+
+ junit-addons
+ junit-addons
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+
diff --git a/core/src/main/java/de/valtech/avs/core/filter/AvsPostFilter.java b/core/src/main/java/de/valtech/avs/core/filter/AvsPostFilter.java
new file mode 100644
index 0000000..95db2b9
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/filter/AvsPostFilter.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.filter;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Part;
+
+import org.apache.commons.fileupload.FileUploadBase;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.adobe.granite.security.user.UserProperties;
+
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.AvsService;
+import de.valtech.avs.api.service.scanner.ScanResult;
+import de.valtech.avs.core.history.HistoryService;
+import de.valtech.avs.core.mail.AvsNotificationMailer;
+import de.valtech.avs.core.serviceuser.ServiceResourceResolverService;
+
+/**
+ * Filter for POST requests. Checks included files.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = Filter.class, property = {"sling.filter.scope=REQUEST", Constants.SERVICE_RANKING + ":Integer=50000"})
+@Designate(ocd = AvsPostFilterConfig.class)
+public class AvsPostFilter implements Filter {
+
+ private static final String REQUEST_PARTS = "request-parts-iterator";
+
+ private static final Logger LOG = LoggerFactory.getLogger(AvsPostFilter.class);
+
+ @Reference
+ private AvsService avsService;
+
+ @Reference
+ private HistoryService historyService;
+
+ @Reference
+ private ServiceResourceResolverService serviceResolverService;
+
+ @Reference
+ private AvsNotificationMailer mailer;
+
+ private List includePatterns = new ArrayList<>();
+ private List excludePatterns = new ArrayList<>();
+
+ /**
+ * Setup service
+ *
+ * @param config configuration
+ */
+ @Activate
+ public void activate(AvsPostFilterConfig config) {
+ excludePatterns = new ArrayList<>();
+ if (config.excludePatterns() != null) {
+ for (String patternString : config.excludePatterns()) {
+ excludePatterns.add(Pattern.compile(patternString));
+ }
+ }
+ includePatterns = new ArrayList<>();
+ if (config.includePatterns() != null) {
+ for (String patternString : config.includePatterns()) {
+ includePatterns.add(Pattern.compile(patternString));
+ }
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
+ String contentType = slingRequest.getContentType();
+ if (!"POST".equals(slingRequest.getMethod()) || !isMultipartRequest(contentType) || isUrlToIgnore(slingRequest)) {
+ chain.doFilter(request, response);
+ return;
+ }
+ Iterator parts = (Iterator) request.getAttribute(REQUEST_PARTS);
+ if (parts == null) {
+ chain.doFilter(request, response);
+ return;
+ }
+ List parameterFiles = new ArrayList<>();
+ List newParts = new ArrayList<>();
+ List fileNames = new ArrayList<>();
+ while (parts.hasNext()) {
+ Part part = parts.next();
+ String partContentType = part.getContentType();
+ if (StringUtils.isEmpty(partContentType)) {
+ String partContent = IOUtils.toString(part.getInputStream(), StandardCharsets.UTF_8.name());
+ newParts.add(new PartWrapper(part, partContent.getBytes()));
+ continue;
+ }
+ if (StringUtils.isNotEmpty(part.getSubmittedFileName())) {
+ fileNames.add(part.getSubmittedFileName());
+ }
+ InputStream partStream = part.getInputStream();
+ File file = File.createTempFile("valtech-avs", ".tmp");
+ Files.copy(partStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ partStream.close();
+ parameterFiles.add(file);
+ PartWrapper wrapper = new PartWrapper(part, file);
+ newParts.add(wrapper);
+ }
+ request.setAttribute(REQUEST_PARTS, newParts.iterator());
+ List streams = new ArrayList<>();
+ for (File file : parameterFiles) {
+ streams.add(new FileInputStream(file));
+ }
+ if (streams.isEmpty()) {
+ chain.doFilter(request, response);
+ return;
+ }
+ SequenceInputStream combinedStream = new SequenceInputStream(Collections.enumeration(streams));
+ try {
+ Session session = slingRequest.getResourceResolver().adaptTo(Session.class);
+ String userId = (session != null) ? session.getUserID() : StringUtils.EMPTY;
+ ScanResult result = avsService.scan(combinedStream, userId);
+ if (!result.isClean()) {
+ for (File file : parameterFiles) {
+ if (!file.delete()) {
+ LOG.warn("Unable to remove temp file {}", file.getPath());
+ }
+ }
+ sendEmail(slingRequest, result, fileNames);
+ throw new ServletException("Uploaded file contains a virus");
+ }
+ } catch (AvsException e) {
+ LOG.error("Virus scan failed", e);
+ }
+ combinedStream.close();
+ chain.doFilter(request, response);
+ }
+
+ /**
+ * Checks if the request is multipart form-data.
+ *
+ * @param contentType content type
+ * @return is multipart form-data
+ */
+ private boolean isMultipartRequest(String contentType) {
+ if (StringUtils.isEmpty(contentType)) {
+ return false;
+ }
+ return contentType.contains(FileUploadBase.MULTIPART_FORM_DATA);
+ }
+
+ /**
+ * Checks if the request URL should be ignored from checking.
+ *
+ * @param request request
+ * @return ignore
+ */
+ protected boolean isUrlToIgnore(SlingHttpServletRequest request) {
+ String url = request.getRequestURI();
+ for (Pattern pattern : excludePatterns) {
+ if (pattern.matcher(url).matches()) {
+ return true;
+ }
+ }
+ if (includePatterns.isEmpty()) {
+ return false;
+ }
+ for (Pattern pattern : includePatterns) {
+ if (pattern.matcher(url).matches()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sends out an email to notify the author about the virus upload.
+ *
+ * @param slingRequest request
+ * @param result scan result
+ * @param fileNames file names
+ */
+ private void sendEmail(SlingHttpServletRequest slingRequest, ScanResult result, List fileNames) {
+ UserProperties properties = slingRequest.adaptTo(UserProperties.class);
+ List emails = new ArrayList<>();
+ try {
+ if ((properties != null) && StringUtils.isNotEmpty(properties.getProperty(UserProperties.EMAIL))) {
+ emails.add(properties.getProperty(UserProperties.EMAIL));
+ }
+ } catch (RepositoryException e) {
+ LOG.error("Cannot read email of user", e);
+ }
+ if (emails.isEmpty()) {
+ return;
+ }
+ mailer.sendEmail(emails, String.join(", ", fileNames), result);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // no action
+ }
+
+ @Override
+ public void destroy() {
+ // no action
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/filter/AvsPostFilterConfig.java b/core/src/main/java/de/valtech/avs/core/filter/AvsPostFilterConfig.java
new file mode 100644
index 0000000..8907617
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/filter/AvsPostFilterConfig.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.filter;
+
+import org.osgi.annotation.versioning.ProviderType;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.AttributeType;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * POST filter configuration.
+ *
+ * @author Roland Gruber
+ */
+@ObjectClassDefinition(name = "AVS POST filter configuration")
+@ProviderType
+public @interface AvsPostFilterConfig {
+
+ /**
+ * Returns the URL include pattern list.
+ *
+ * @return list
+ */
+ @AttributeDefinition(name = "URL include patterns", description = "List of regular expressions to match the URLs to check",
+ type = AttributeType.STRING)
+ String[] includePatterns();
+
+ /**
+ * Returns the URL exclude pattern list.
+ *
+ * @return list
+ */
+ @AttributeDefinition(name = "URL exclude patterns",
+ description = "List of regular expressions to match the URLs to ignore. Has higher priority than include patterns.",
+ type = AttributeType.STRING)
+ String[] excludePatterns();
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/filter/PartWrapper.java b/core/src/main/java/de/valtech/avs/core/filter/PartWrapper.java
new file mode 100644
index 0000000..0a0d8f2
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/filter/PartWrapper.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.filter;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Collection;
+
+import javax.servlet.http.Part;
+
+/**
+ * Wrapper to replace input stream from part.
+ *
+ * @author Roland Gruber
+ */
+public class PartWrapper implements Part {
+
+ private Part part;
+ private File file;
+ private byte[] content;
+
+ /**
+ * Constructor
+ *
+ * @param part original part
+ * @param file file
+ */
+ public PartWrapper(Part part, File file) {
+ this.part = part;
+ this.file = file;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param part original part
+ * @param content content
+ */
+ public PartWrapper(Part part, byte[] content) {
+ this.part = part;
+ this.content = content;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ if (file == null) {
+ return new ByteArrayInputStream(content);
+ }
+ return Files.newInputStream(Paths.get(file.getPath()), StandardOpenOption.DELETE_ON_CLOSE);
+ }
+
+ @Override
+ public String getContentType() {
+ return part.getContentType();
+ }
+
+ @Override
+ public String getName() {
+ return part.getName();
+ }
+
+ @Override
+ public long getSize() {
+ return part.getSize();
+ }
+
+ @Override
+ public void write(String fileName) throws IOException {
+ part.write(fileName);
+ }
+
+ @Override
+ public void delete() throws IOException {
+ part.delete();
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return part.getHeader(name);
+ }
+
+ @Override
+ public Collection getHeaders(String name) {
+ return part.getHeaders(name);
+ }
+
+ @Override
+ public Collection getHeaderNames() {
+ return part.getHeaderNames();
+ }
+
+ @Override
+ public String getSubmittedFileName() {
+ return part.getSubmittedFileName();
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/healthcheck/SelfCheckHealthCheck.java b/core/src/main/java/de/valtech/avs/core/healthcheck/SelfCheckHealthCheck.java
new file mode 100644
index 0000000..ac6bb08
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/healthcheck/SelfCheckHealthCheck.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.healthcheck;
+
+import java.io.ByteArrayInputStream;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.hc.api.HealthCheck;
+import org.apache.sling.hc.api.Result;
+import org.apache.sling.hc.api.Result.Status;
+import org.apache.sling.hc.util.FormattingResultLog;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.AvsService;
+import de.valtech.avs.api.service.scanner.ScanResult;
+import de.valtech.avs.core.serviceuser.ServiceResourceResolverService;
+
+/**
+ * Checks if the basic setup is ok.
+ *
+ * @author Roland Gruber
+ */
+@Component(immediate = true, service = HealthCheck.class, property = {HealthCheck.TAGS + "=avs",
+ HealthCheck.NAME + "=AVS Self Check", HealthCheck.MBEAN_NAME + "=avsSelfCheckHCmBean"})
+public class SelfCheckHealthCheck implements HealthCheck {
+
+ @Reference
+ private ServiceResourceResolverService resolverService;
+
+ @Reference
+ private AvsService avsService;
+
+ @Override
+ public Result execute() {
+ final FormattingResultLog resultLog = new FormattingResultLog();
+ checkServiceResolver(resultLog);
+ if (resultLog.getAggregateStatus().equals(Status.CRITICAL)) {
+ return new Result(resultLog);
+ }
+ checkActiveScannersAvailable(resultLog);
+ return new Result(resultLog);
+ }
+
+ /**
+ * Checks if the service resource resolvers are accessible.
+ *
+ * @param resultLog result log
+ */
+ private void checkServiceResolver(FormattingResultLog resultLog) {
+ try (ResourceResolver resolver = resolverService.getServiceResourceResolver()) {
+ if (resolver == null) {
+ resultLog.critical("Unable to open service resource resolver: null");
+ return;
+ }
+ resultLog.info("Service user ok");
+ } catch (LoginException e) {
+ resultLog.critical("Unable to open service resource resolver {}", e.getMessage());
+ }
+ }
+
+ /**
+ * Checks if there is at least one scan engine configured.
+ *
+ * @param resultLog result log
+ */
+ private void checkActiveScannersAvailable(FormattingResultLog resultLog) {
+ if (!avsService.hasActiveScanEngines()) {
+ resultLog.critical("No active scan engines available");
+ } else {
+ resultLog.info("At least one active scan engine available");
+ checkSampleScan(resultLog);
+ }
+ }
+
+ /**
+ * Performs a test scan.
+ *
+ * @param resultLog result log
+ */
+ private void checkSampleScan(FormattingResultLog resultLog) {
+ ByteArrayInputStream stream = new ByteArrayInputStream(SelfCheckHealthCheck.class.getName().getBytes());
+ try {
+ ScanResult result = avsService.scan(stream, "HEALTH CHECK");
+ if (result.isClean()) {
+ resultLog.info("Test scan ok");
+ } else {
+ resultLog.critical("False positive result: {}", result.getOutput());
+ }
+ } catch (AvsException e) {
+ resultLog.critical("Error running test scan: {}", e.getMessage());
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/history/HistoryEntryImpl.java b/core/src/main/java/de/valtech/avs/core/history/HistoryEntryImpl.java
new file mode 100644
index 0000000..bec5168
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/history/HistoryEntryImpl.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.history;
+
+import java.util.Date;
+
+import de.valtech.avs.api.history.HistoryEntry;
+
+/**
+ * Represents an entry in the AVS history.
+ *
+ * @author Roland Gruber
+ */
+public class HistoryEntryImpl implements HistoryEntry {
+
+ private Date time;
+
+ private String output;
+
+ private boolean clean;
+
+ private String path;
+
+ private String repositoryPath;
+
+ private String userId;
+
+ /**
+ * Constructor
+ *
+ * @param time time
+ * @param output output text
+ * @param clean is clean
+ * @param path path that was scanned
+ * @param repositoryPath path in crx
+ * @param userId user id
+ */
+ public HistoryEntryImpl(Date time, String output, boolean clean, String path, String repositoryPath, String userId) {
+ super();
+ this.time = time;
+ this.output = output;
+ this.clean = clean;
+ this.path = path;
+ this.repositoryPath = repositoryPath;
+ this.userId = userId;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see de.valtech.avs.api.history.HistoryEntry#getTime()
+ */
+ @Override
+ public Date getTime() {
+ return time;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see de.valtech.avs.api.history.HistoryEntry#getOutput()
+ */
+ @Override
+ public String getOutput() {
+ return output;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see de.valtech.avs.api.history.HistoryEntry#isClean()
+ */
+ @Override
+ public boolean isClean() {
+ return clean;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see de.valtech.avs.api.history.HistoryEntry#getPath()
+ */
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see de.valtech.avs.api.history.HistoryEntry#getRepositoryPath()
+ */
+ @Override
+ public String getRepositoryPath() {
+ return repositoryPath;
+ }
+
+ @Override
+ public String getUserId() {
+ return userId;
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/history/HistoryService.java b/core/src/main/java/de/valtech/avs/core/history/HistoryService.java
new file mode 100644
index 0000000..b0eba00
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/history/HistoryService.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.history;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants;
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ResourceUtil.BatchResourceRemover;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.jcr.resource.api.JcrResourceConstants;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.valtech.avs.api.history.HistoryEntry;
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.scanner.ScanResult;
+
+/**
+ * Reads and writes history entries.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = HistoryService.class)
+public class HistoryService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(HistoryService.class);
+
+ /**
+ * Base node for history entries
+ */
+ public static final String HISTORY_BASE = "/var/avs/history";
+
+ protected static final String ATTR_TIME = "time";
+ protected static final String ATTR_CLEAN = "clean";
+ protected static final String ATTR_OUTPUT = "output";
+ protected static final String ATTR_PATH = "path";
+ protected static final String ATTR_USER_ID = "userId";
+ private static final String NAME_INDEX = "oak:index";
+
+ private Random random = new Random();
+
+ /**
+ * Creates a new history entry.
+ *
+ * @param resolver resource resolver
+ * @param result scan result
+ * @throws AvsException error setting up entry
+ */
+ public void createHistoryEntry(ResourceResolver resolver, ScanResult result) throws AvsException {
+ Calendar now = new GregorianCalendar();
+ String basePath = HISTORY_BASE + "/" + now.get(Calendar.YEAR) + "/" + (now.get(Calendar.MONTH) + 1) + "/"
+ + now.get(Calendar.DAY_OF_MONTH);
+ String nodeName = generateHistoryNodeName();
+ String nodePath = basePath + "/" + nodeName;
+ createPath(basePath, resolver, JcrResourceConstants.NT_SLING_ORDERED_FOLDER);
+ createPath(nodePath, resolver, JcrConstants.NT_UNSTRUCTURED);
+ Resource resource = resolver.getResource(nodePath);
+ ModifiableValueMap values = resource.adaptTo(ModifiableValueMap.class);
+ values.put(ATTR_TIME, now);
+ values.put(ATTR_OUTPUT, result.getOutput());
+ values.put(ATTR_CLEAN, result.isClean());
+ values.put(ATTR_PATH, result.getPath());
+ values.put(ATTR_USER_ID, result.getUserId());
+ try {
+ resolver.commit();
+ } catch (PersistenceException e) {
+ throw new AvsException("Unable to story history entry", e);
+ }
+ }
+
+ /**
+ * Returns the last history entries. The search starts at the newest entry.
+ *
+ * @param startIndex start reading at this index (first is 0)
+ * @param count number of entries to read
+ * @param resolver resource resolver
+ * @return history entries (newest first)
+ */
+ public List getHistory(int startIndex, int count, ResourceResolver resolver) {
+ List entries = new ArrayList<>();
+ if (count == 0) {
+ return entries;
+ }
+ Resource base = resolver.getResource(HISTORY_BASE);
+ Resource current = getLatestHistoryEntry(base);
+ if (current == null) {
+ return entries;
+ }
+ // skip up to start index
+ for (int i = 0; i < startIndex; i++) {
+ current = getPreviousHistoryEntry(current);
+ }
+ for (int i = 0; i < count; i++) {
+ if (current == null) {
+ break;
+ }
+ entries.add(readHistoryEntry(current));
+ current = getPreviousHistoryEntry(current);
+ }
+ return entries;
+ }
+
+ /**
+ * Returns the run before the given one.
+ *
+ * @param current current run
+ * @return previous run
+ */
+ private Resource getPreviousHistoryEntry(Resource current) {
+ // check if the parent has a sibling before the current node
+ Resource previous = getPreviousSibling(current);
+ if (previous != null) {
+ return previous;
+ }
+ // go down till we find an earlier sibling
+ Resource base = descendToPreviousSiblingInHistory(current.getParent());
+ // go back up the folders
+ return ascendToLastRun(base);
+ }
+
+ /**
+ * Gos up the folders to last run.
+ *
+ * @param resource current node
+ * @return last run
+ */
+ private Resource ascendToLastRun(Resource resource) {
+ if (resource == null) {
+ return null;
+ }
+ Resource last = getLastChild(resource);
+ if (last == null) {
+ // stop if there is no child at all
+ return null;
+ }
+ ValueMap values = last.adaptTo(ValueMap.class);
+ if (JcrResourceConstants.NT_SLING_ORDERED_FOLDER.equals(values.get(JcrConstants.JCR_PRIMARYTYPE, String.class))) {
+ return ascendToLastRun(last);
+ }
+ return last;
+ }
+
+ /**
+ * Descends in history till a previous sibling is found. Descending stops at history base level
+ *
+ * @param current current resource
+ * @return previous sibling
+ */
+ private Resource descendToPreviousSiblingInHistory(Resource current) {
+ if ((current == null) || HISTORY_BASE.equals(current.getPath())) {
+ return null;
+ }
+ Resource previous = getPreviousSibling(current);
+ if (previous != null) {
+ return previous;
+ }
+ previous = descendToPreviousSiblingInHistory(current.getParent());
+ return previous;
+ }
+
+ /**
+ * Returns the previous sibling of the given node.
+ *
+ * @param resource current node
+ * @return last sibling or null
+ */
+ private Resource getPreviousSibling(Resource resource) {
+ Iterator siblings = resource.getParent().listChildren();
+ Resource previous = null;
+ while (siblings.hasNext()) {
+ Resource sibling = siblings.next();
+ if (sibling.getName().equals(resource.getName())) {
+ break;
+ }
+ if (!sibling.getName().equals(AccessControlConstants.REP_POLICY) && !sibling.getName().equals(NAME_INDEX)) {
+ previous = sibling;
+ }
+ }
+ return previous;
+ }
+
+ /**
+ * Returns the latest history entry.
+ *
+ * @param base base resource
+ * @return latest run resource
+ */
+ private Resource getLatestHistoryEntry(Resource base) {
+ if (base == null) {
+ return null;
+ }
+ return ascendToLastRun(base);
+ }
+
+ /**
+ * Returns the last child of the given resource.
+ *
+ * @param resource resource
+ * @return last child
+ */
+ private Resource getLastChild(Resource resource) {
+ if (resource == null) {
+ return null;
+ }
+ Resource last = null;
+ Iterator lastIterator = resource.listChildren();
+ while (lastIterator.hasNext()) {
+ Resource candidate = lastIterator.next();
+ if (!AccessControlConstants.REP_POLICY.equals(candidate.getName()) && !NAME_INDEX.equals(candidate.getName())) {
+ last = candidate;
+ }
+ }
+ return last;
+ }
+
+ /**
+ * Reads a history entry from JCR.
+ *
+ * @param resource history resource
+ * @return history entry
+ */
+ private HistoryEntry readHistoryEntry(Resource resource) {
+ ValueMap values = resource.adaptTo(ValueMap.class);
+ boolean clean = values.get(ATTR_CLEAN, false);
+ String output = values.get(ATTR_OUTPUT, "");
+ String path = values.get(ATTR_PATH, "");
+ String userId = values.get(ATTR_USER_ID, "");
+ Calendar date = values.get(ATTR_TIME, Calendar.class);
+ return new HistoryEntryImpl(date.getTime(), output, clean, path, resource.getPath(), userId);
+ }
+
+ /**
+ * Creates the folder at the given path if not yet existing.
+ *
+ * @param path path
+ * @param resolver resource resolver
+ * @param primaryType primary type
+ * @throws AvsException error creating folder
+ */
+ protected void createPath(String path, ResourceResolver resolver, String primaryType) throws AvsException {
+ Resource folder = resolver.getResource(path);
+ if (folder == null) {
+ String parent = path.substring(0, path.lastIndexOf('/'));
+ String name = path.substring(path.lastIndexOf('/') + 1);
+ if (resolver.getResource(parent) == null) {
+ createPath(parent, resolver, primaryType);
+ }
+ Map properties = new HashMap<>();
+ properties.put(JcrConstants.JCR_PRIMARYTYPE, primaryType);
+ try {
+ resolver.create(resolver.getResource(parent), name, properties);
+ } catch (PersistenceException e) {
+ throw new AvsException("Unable to create " + path, e);
+ }
+ }
+ }
+
+ /**
+ * Generates the node name for a history entry.
+ *
+ * @return name
+ */
+ private String generateHistoryNodeName() {
+ return System.currentTimeMillis() + "" + random.nextInt(100000);
+ }
+
+ /**
+ * Purges the history by keeping only entries within the set number of days.
+ *
+ * @param resolver resource resolver
+ * @param daysToKeep number of days to keep
+ * @throws PersistenceException error deleting node
+ */
+ public void purgeHistory(ResourceResolver resolver, int daysToKeep) throws PersistenceException {
+ Resource base = resolver.getResource(HISTORY_BASE);
+ Calendar calendar = new GregorianCalendar();
+ calendar.add(Calendar.DAY_OF_MONTH, -daysToKeep);
+ LOG.debug("Starting purge with limit {}", calendar.getTime());
+ deleteRecursive(base.listChildren(), calendar, new int[] {Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH});
+ }
+
+ /**
+ * Deletes the year resources that are too old.
+ *
+ * @param resources resources
+ * @param calendar time limit
+ * @param fields calendar fields
+ * @throws PersistenceException error deleting node
+ */
+ private void deleteRecursive(Iterator resources, Calendar calendar, int[] fields) throws PersistenceException {
+ int currentField = fields[0];
+ while (resources.hasNext()) {
+ Resource resource = resources.next();
+ String name = resource.getName();
+ // skip extra nodes such as ACLs
+ if (!StringUtils.isNumeric(name)) {
+ LOG.debug("Skipping purge of other node: {}", resource.getPath());
+ continue;
+ }
+ int nodeValue = Integer.parseInt(name);
+ int limit = calendar.get(currentField);
+ if (currentField == Calendar.MONTH) {
+ // months start with 0 but are stored beginning with 1 in CRX
+ limit++;
+ }
+ if (nodeValue > limit) {
+ LOG.debug("Skipping purge of too young node: {}", resource.getPath());
+ } else if (nodeValue == limit) {
+ LOG.debug("Skipping purge of too young node: {}", resource.getPath());
+ // check next level
+ if (fields.length == 1) {
+ return;
+ }
+ int[] fieldsNew = new int[fields.length - 1];
+ System.arraycopy(fields, 1, fieldsNew, 0, fieldsNew.length);
+ deleteRecursive(resource.listChildren(), calendar, fieldsNew);
+ } else {
+ LOG.debug("Purging node: {}", resource.getPath());
+ BatchResourceRemover remover = ResourceUtil.getBatchResourceRemover(1000);
+ remover.delete(resource);
+ }
+ }
+ }
+
+ /**
+ * Self test of history. Checks if the history node exists.
+ *
+ * @param resolver resource resolver
+ * @throws AvsException check failed
+ */
+ public void selfCheck(ResourceResolver resolver) throws AvsException {
+ Resource base = resolver.getResource(HISTORY_BASE);
+ if (base == null) {
+ throw new AvsException(HISTORY_BASE + " does not exist or is not accessible.");
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/jmx/AntiVirusScannerMBean.java b/core/src/main/java/de/valtech/avs/core/jmx/AntiVirusScannerMBean.java
new file mode 100644
index 0000000..87c3d86
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/jmx/AntiVirusScannerMBean.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.jmx;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+import com.adobe.granite.jmx.annotation.Description;
+import com.adobe.granite.jmx.annotation.Name;
+
+import de.valtech.avs.api.service.AvsException;
+
+/**
+ * JMX service to scan files via virus scan.
+ *
+ * @author Roland Gruber
+ */
+@Description("Anti virus scanner")
+@ProviderType
+public interface AntiVirusScannerMBean {
+
+ /**
+ * Scans a text content.
+ *
+ * @param content content
+ * @return result
+ * @throws AvsException error during scan
+ */
+ @Description("Scans a text content")
+ String scanContent(@Name("Content") @Description("Text content to be scanned") String content) throws AvsException;
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/jmx/AntiVirusScannerMBeanImpl.java b/core/src/main/java/de/valtech/avs/core/jmx/AntiVirusScannerMBeanImpl.java
new file mode 100644
index 0000000..18aef12
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/jmx/AntiVirusScannerMBeanImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.jmx;
+
+import java.io.ByteArrayInputStream;
+
+import javax.management.NotCompliantMBeanException;
+
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import com.adobe.granite.jmx.annotation.AnnotatedStandardMBean;
+
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.AvsService;
+
+/**
+ * JMX service to check virus scan.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = AntiVirusScannerMBean.class, immediate = true,
+ property = {"jmx.objectname=de.valtech:type=AVS", "pattern=/.*"})
+public class AntiVirusScannerMBeanImpl extends AnnotatedStandardMBean implements AntiVirusScannerMBean {
+
+ @Reference
+ private AvsService scanner;
+
+ /**
+ * Constructor
+ *
+ * @throws NotCompliantMBeanException exception
+ */
+ public AntiVirusScannerMBeanImpl() throws NotCompliantMBeanException {
+ super(AntiVirusScannerMBean.class);
+ }
+
+ @Override
+ public String scanContent(String content) {
+ ByteArrayInputStream stream = new ByteArrayInputStream(content.getBytes());
+ try {
+ return scanner.scan(stream, "JMX").toString();
+ } catch (AvsException e) {
+ return "Error: " + e.getMessage();
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/mail/AvsNotificationMailer.java b/core/src/main/java/de/valtech/avs/core/mail/AvsNotificationMailer.java
new file mode 100644
index 0000000..9bc3b47
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/mail/AvsNotificationMailer.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.mail;
+
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMultipart;
+
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.MultiPartEmail;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.day.cq.mailer.MailService;
+
+import de.valtech.avs.api.service.scanner.ScanResult;
+
+/**
+ * Sends out an email notification when a virus was found.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = AvsNotificationMailer.class)
+@Designate(ocd = AvsNotificationMailerConfig.class)
+public class AvsNotificationMailer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AvsNotificationMailer.class);
+
+ @Reference
+ private MailService mailService;
+
+ private AvsNotificationMailerConfig config;
+
+ /**
+ * Activation
+ *
+ * @param config config
+ */
+ @Activate
+ public void activate(AvsNotificationMailerConfig config) {
+ this.config = config;
+ }
+
+ /**
+ * Sends out the email to the recipients.
+ *
+ * @param emails email recipients
+ * @param fileName file name that was scanned
+ * @param result scan result
+ */
+ public void sendEmail(List emails, String fileName, ScanResult result) {
+ VelocityEngine ve = new VelocityEngine();
+ ve.init();
+ VelocityContext context = new VelocityContext();
+ StringWriter writer = new StringWriter();
+ if (fileName != null) {
+ context.put("FILE_NAME", fileName);
+ }
+ context.put("SCAN_OUTPUT", result.getOutput());
+ ve.evaluate(context, writer, "AvsNotificationMailer", getBodyText());
+ String body = writer.toString();
+ String subject = getSubject();
+ List emailTO = new ArrayList<>(emails);
+ if (config.additionalRecipients() != null) {
+ emailTO.addAll(Arrays.asList(config.additionalRecipients()));
+ }
+ try {
+ sendMail(emailTO, subject, body);
+ } catch (MessagingException | EmailException e) {
+ LOG.error("Unable to send virus notification", e);
+ }
+ }
+
+ /**
+ * Returns the raw body text incl. wildcards.
+ *
+ * @return body
+ */
+ private String getBodyText() {
+ return config.body();
+ }
+
+ /**
+ * Returns the email subject.
+ *
+ * @return subject
+ */
+ private String getSubject() {
+ return config.subject();
+ }
+
+ /**
+ * Sends out the email.
+ *
+ * @param emails email addresses
+ * @param subject subject
+ * @param body body text
+ * @throws MessagingException error sending mail
+ * @throws EmailException error configuring email
+ */
+ private void sendMail(List emails, String subject, String body) throws MessagingException, EmailException {
+ MultiPartEmail email = new MultiPartEmail();
+ MimeBodyPart messagePart = new MimeBodyPart();
+ if (config.isHtml()) {
+ messagePart.setContent(body, "text/html; charset=utf-8");
+ } else {
+ messagePart.setContent(body, "text/plain; charset=utf-8");
+ }
+ MimeMultipart multipart = new MimeMultipart();
+ multipart.addBodyPart(messagePart);
+ email.setSubject(subject);
+ for (String toAddress : emails) {
+ email.addTo(toAddress);
+ }
+ email.setFrom(config.from());
+ email.setContent(multipart);
+ mailService.send(email);
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/mail/AvsNotificationMailerConfig.java b/core/src/main/java/de/valtech/avs/core/mail/AvsNotificationMailerConfig.java
new file mode 100644
index 0000000..ce0745b
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/mail/AvsNotificationMailerConfig.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.mail;
+
+import org.osgi.annotation.versioning.ProviderType;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.AttributeType;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Configuration for mailer.
+ *
+ * @author Roland Gruber
+ */
+@ObjectClassDefinition(name = "AVS Notification Mailer Configuration")
+@ProviderType
+public @interface AvsNotificationMailerConfig {
+
+ /**
+ * Returns the mail subject.
+ *
+ * @return subject
+ */
+ @AttributeDefinition(name = "Email subject", description = "Subject for all virus notification emails",
+ type = AttributeType.STRING)
+ String subject() default "A virus was found in your file upload";
+
+ /**
+ * Returns the mail body.
+ *
+ * @return body
+ */
+ @AttributeDefinition(name = "Email body",
+ description = "Body for all virus notification emails. Wildcards are ${FILE_NAME} for uploaded file name and ${SCAN_OUTPUT} for scan details.",
+ type = AttributeType.STRING)
+ String body() default "Dear Sir or Madam,
a virus was detected in your AEM file upload.
File name: ${FILE_NAME} Scan report: ${SCAN_OUTPUT}";
+
+ /**
+ * Returns if email is HTML or TEXT.
+ *
+ * @return is HTML
+ */
+ @AttributeDefinition(name = "HTML format", description = "Specifies if email is sent as HTML or plain text.",
+ type = AttributeType.BOOLEAN)
+ boolean isHtml() default true;
+
+ /**
+ * Returns the FROM address.
+ *
+ * @return FROM
+ */
+ @AttributeDefinition(name = "Email FROM address", description = "FROM address for notification emails.",
+ type = AttributeType.STRING)
+ String from() default "do-not-reply@example.com";
+
+ /**
+ * Returns list of additional email recipients when an alert is sent.
+ *
+ * @return list
+ */
+ @AttributeDefinition(name = "Additional email recipients", description = "List of email addresses.",
+ type = AttributeType.STRING)
+ String[] additionalRecipients();
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/maintenance/PurgeHistoryConfiguration.java b/core/src/main/java/de/valtech/avs/core/maintenance/PurgeHistoryConfiguration.java
new file mode 100644
index 0000000..614f4fb
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/maintenance/PurgeHistoryConfiguration.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.maintenance;
+
+import org.osgi.annotation.versioning.ProviderType;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.AttributeType;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Configuration for purge task.
+ *
+ * @author Roland Gruber
+ */
+@ObjectClassDefinition(name = "AVS Purge history configuration")
+@ProviderType
+public @interface PurgeHistoryConfiguration {
+
+ @AttributeDefinition(type = AttributeType.INTEGER, name = "Days to keep",
+ description = "Entries younger than this will not be removed")
+ int daysToKeep() default 90;
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/maintenance/PurgeHistoryTask.java b/core/src/main/java/de/valtech/avs/core/maintenance/PurgeHistoryTask.java
new file mode 100644
index 0000000..ab5ffcb
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/maintenance/PurgeHistoryTask.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.maintenance;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.event.jobs.Job;
+import org.apache.sling.event.jobs.consumer.JobExecutionContext;
+import org.apache.sling.event.jobs.consumer.JobExecutionResult;
+import org.apache.sling.event.jobs.consumer.JobExecutor;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.Designate;
+
+import com.adobe.granite.maintenance.MaintenanceConstants;
+
+import de.valtech.avs.core.history.HistoryService;
+import de.valtech.avs.core.serviceuser.ServiceResourceResolverService;
+
+/**
+ * Purges old entries from the history.
+ *
+ * @author Roland Gruber
+ */
+@Component(property = {MaintenanceConstants.PROPERTY_TASK_NAME + "=AVSPurgeHistory",
+ MaintenanceConstants.PROPERTY_TASK_TITLE + "=AVS Purge History",
+ JobExecutor.PROPERTY_TOPICS + "=" + MaintenanceConstants.TASK_TOPIC_PREFIX + "AVSPurgeHistory",})
+@Designate(ocd = PurgeHistoryConfiguration.class)
+public class PurgeHistoryTask implements JobExecutor {
+
+ private PurgeHistoryConfiguration config;
+
+ @Reference
+ private ServiceResourceResolverService resolverService;
+
+ @Reference
+ private HistoryService historyService;
+
+ /**
+ * Activates the service.
+ *
+ * @param config configuration
+ */
+ @Activate
+ public void activate(PurgeHistoryConfiguration config) {
+ this.config = config;
+ }
+
+ @Override
+ public JobExecutionResult process(Job job, JobExecutionContext context) {
+ try (ResourceResolver resolver = resolverService.getServiceResourceResolver()) {
+ historyService.purgeHistory(resolver, config.daysToKeep());
+ resolver.commit();
+ return context.result().message("Purged AVS history entries").succeeded();
+ } catch (LoginException e) {
+ return context.result().message("Service resolver failed with " + e.getMessage()).failed();
+ } catch (PersistenceException e) {
+ return context.result().message("Purge failed with " + e.getMessage()).failed();
+ }
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/model/history/HistoryDataItem.java b/core/src/main/java/de/valtech/avs/core/model/history/HistoryDataItem.java
new file mode 100644
index 0000000..45d79b5
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/model/history/HistoryDataItem.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.model.history;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.SlingObject;
+
+import de.valtech.avs.api.history.HistoryEntry;
+
+/**
+ * Model class for a single history item.
+ *
+ * @author Roland Gruber
+ */
+@Model(adaptables = Resource.class)
+public class HistoryDataItem {
+
+ private final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ @SlingObject
+ private Resource resource;
+
+ protected HistoryEntry history = null;
+
+ @PostConstruct
+ public void setup() {
+ history = resource.getValueMap().get(HistoryDataSource.ATTR_HISTORY, HistoryEntry.class);
+ }
+
+ /**
+ * Returns the date of the scan.
+ *
+ * @return date
+ */
+ public String getDate() {
+ return format.format(history.getTime());
+ }
+
+ /**
+ * Returns the output of the scan.
+ *
+ * @return output
+ */
+ public String getOutput() {
+ return history.getOutput();
+ }
+
+ /**
+ * Returns the path of the scan.
+ *
+ * @return path
+ */
+ public String getPath() {
+ return history.getPath();
+ }
+
+ /**
+ * Returns the user id that caused the scan.
+ *
+ * @return path
+ */
+ public String getUserId() {
+ return history.getUserId();
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/model/history/HistoryDataSource.java b/core/src/main/java/de/valtech/avs/core/model/history/HistoryDataSource.java
new file mode 100644
index 0000000..d73c0f5
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/model/history/HistoryDataSource.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.model.history;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.OSGiService;
+import org.apache.sling.models.annotations.injectorspecific.SlingObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.adobe.granite.ui.components.ds.AbstractDataSource;
+import com.adobe.granite.ui.components.ds.DataSource;
+import com.adobe.granite.ui.components.ds.ValueMapResource;
+
+import de.valtech.avs.api.history.HistoryEntry;
+import de.valtech.avs.core.history.HistoryService;
+import de.valtech.avs.core.serviceuser.ServiceResourceResolverService;
+
+/**
+ * Datasource model for history page.
+ *
+ * @author Roland Gruber
+ */
+@Model(adaptables = SlingHttpServletRequest.class)
+public class HistoryDataSource {
+
+ private static final String ITEM_TYPE = "valtech/avs/tools/history/dataitem";
+ public static final String ATTR_HISTORY = "history";
+
+ private static final Logger LOG = LoggerFactory.getLogger(DataSource.class);
+
+ @SlingObject
+ private SlingHttpServletRequest request;
+
+ @OSGiService
+ private HistoryService historyService;
+
+ @OSGiService
+ private ServiceResourceResolverService serviceResourceResolverService;
+
+ @PostConstruct
+ public void setup() {
+ String[] selectors = request.getRequestPathInfo().getSelectors();
+ int offset = 0;
+ int limit = 50;
+ if (selectors.length > 1) {
+ offset = Integer.parseInt(selectors[0]);
+ limit = Integer.parseInt(selectors[1]);
+ }
+ request.setAttribute(DataSource.class.getName(), getResourceIterator(offset, limit));
+ }
+
+ /**
+ * Returns the history entries.
+ *
+ * @param offset offset where to start reading
+ * @param limit maximum number of entries to return
+ * @return entries
+ */
+ private DataSource getResourceIterator(int offset, int limit) {
+ return new AbstractDataSource() {
+
+ @Override
+ public Iterator iterator() {
+ List entries = new ArrayList<>();
+ try (ResourceResolver resolver = serviceResourceResolverService.getServiceResourceResolver()) {
+ List historyEntries = historyService.getHistory(offset, limit + 1, resolver);
+ for (HistoryEntry historyEntry : historyEntries) {
+ ValueMap vm = new ValueMapDecorator(new HashMap());
+ vm.put(ATTR_HISTORY, historyEntry);
+ entries.add(new ValueMapResource(request.getResourceResolver(), historyEntry.getRepositoryPath(),
+ ITEM_TYPE, vm));
+ }
+ } catch (LoginException e) {
+ LOG.error("Unable to read history entries", e);
+ }
+ return entries.iterator();
+ }
+
+ };
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/model/scan/ScanModel.java b/core/src/main/java/de/valtech/avs/core/model/scan/ScanModel.java
new file mode 100644
index 0000000..94e55cc
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/model/scan/ScanModel.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.model.scan;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.annotation.PostConstruct;
+import javax.jcr.Session;
+import javax.servlet.ServletException;
+import javax.servlet.http.Part;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.OSGiService;
+import org.apache.sling.models.annotations.injectorspecific.SlingObject;
+
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.AvsService;
+import de.valtech.avs.api.service.scanner.ScanResult;
+
+/**
+ * Sling model for scan tool.
+ *
+ * @author Roland Gruber
+ */
+@Model(adaptables = SlingHttpServletRequest.class)
+public class ScanModel {
+
+ private static final String FILE_PART = "scanfile";
+
+ @SlingObject
+ private SlingHttpServletRequest request;
+
+ @OSGiService
+ private AvsService avsService;
+
+ private boolean scanDone = false;
+
+ private String resultOutput;
+
+ private boolean scanFailed = false;
+
+ private boolean clean = true;
+
+ @PostConstruct
+ protected void init() {
+ try {
+ Part filePart = request.getPart(FILE_PART);
+ if (filePart != null) {
+ InputStream inputStream = filePart.getInputStream();
+ if (inputStream != null) {
+ scanDone = true;
+ String userId = request.getResourceResolver().adaptTo(Session.class).getUserID();
+ ScanResult result = avsService.scan(inputStream, userId);
+ clean = result.isClean();
+ resultOutput = result.getOutput();
+ }
+ }
+ } catch (IOException | ServletException | AvsException e) {
+ scanFailed = true;
+ resultOutput = e.getMessage();
+ }
+ }
+
+ /**
+ * Returns the scan result text.
+ *
+ * @return result
+ */
+ public String getResult() {
+ return resultOutput;
+ }
+
+ /**
+ * Returns if a scan was performed.
+ *
+ * @return scan done
+ */
+ public boolean isScanDone() {
+ return scanDone;
+ }
+
+ /**
+ * Returns if the scan failed.
+ *
+ * @return failed
+ */
+ public boolean isScanFailed() {
+ return scanFailed;
+ }
+
+ /**
+ * Returns if the file was clean.
+ *
+ * @return clean
+ */
+ public boolean isClean() {
+ return clean;
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/service/AvsServiceImpl.java b/core/src/main/java/de/valtech/avs/core/service/AvsServiceImpl.java
new file mode 100644
index 0000000..2107638
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/service/AvsServiceImpl.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.service;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.AvsService;
+import de.valtech.avs.api.service.scanner.AvsScannerEnine;
+import de.valtech.avs.api.service.scanner.ScanResult;
+import de.valtech.avs.core.history.HistoryService;
+import de.valtech.avs.core.serviceuser.ServiceResourceResolverService;
+
+/**
+ * AVS scan service.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = AvsService.class)
+public class AvsServiceImpl implements AvsService {
+
+ @Reference
+ private HistoryService historyService;
+
+ @Reference
+ private ServiceResourceResolverService serviceResourceResolverService;
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, bind = "bindEngine",
+ unbind = "unbindEngine")
+ private List engines = new ArrayList<>();
+
+ /**
+ * Adds a scanner engine.
+ *
+ * @param engine engine
+ */
+ protected synchronized void bindEngine(AvsScannerEnine engine) {
+ engines.add(engine);
+ }
+
+ /**
+ * Removes a scanner engine.
+ *
+ * @param engine engine
+ */
+ protected synchronized void unbindEngine(AvsScannerEnine engine) {
+ engines.remove(engine);
+ }
+
+ @Override
+ public ScanResult scan(InputStream content, String userId) throws AvsException {
+ return scan(content, userId, StringUtils.EMPTY);
+ }
+
+ @Override
+ public ScanResult scan(InputStream content, String userId, String path) throws AvsException {
+ if (engines.isEmpty()) {
+ throw new AvsException("No scanning engines available");
+ }
+ if (content == null) {
+ // skip empty content
+ return new ScanResult(null, true);
+ }
+ ScanResult result = null;
+ try (ResourceResolver resolver = serviceResourceResolverService.getServiceResourceResolver()) {
+ for (AvsScannerEnine engine : engines) {
+ result = engine.scan(content);
+ result.setPath(path);
+ result.setUserId(userId);
+ if (!result.isClean()) {
+ historyService.createHistoryEntry(resolver, result);
+ }
+ }
+ } catch (LoginException e) {
+ throw new AvsException("Unable to access service resolver", e);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean hasActiveScanEngines() {
+ return !engines.isEmpty();
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/service/scanner/ClamScannerConfig.java b/core/src/main/java/de/valtech/avs/core/service/scanner/ClamScannerConfig.java
new file mode 100644
index 0000000..e49926e
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/service/scanner/ClamScannerConfig.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.service.scanner;
+
+import org.osgi.annotation.versioning.ProviderType;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.AttributeType;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Clam scanner configuration.
+ *
+ * @author Roland Gruber
+ */
+@ObjectClassDefinition(name = "AVS ClamAV configuration")
+@ProviderType
+public @interface ClamScannerConfig {
+
+ /**
+ * Returns the scan command.
+ *
+ * @return command
+ */
+ @AttributeDefinition(name = "Scan command",
+ description = "Command to scan a single file. The file name will be added at the end of the command.",
+ type = AttributeType.STRING)
+ String command();
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/service/scanner/ClamScannerEngine.java b/core/src/main/java/de/valtech/avs/core/service/scanner/ClamScannerEngine.java
new file mode 100644
index 0000000..d75999d
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/service/scanner/ClamScannerEngine.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.service.scanner;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.valtech.avs.api.service.AvsException;
+import de.valtech.avs.api.service.scanner.AvsScannerEnine;
+import de.valtech.avs.api.service.scanner.ScanResult;
+
+/**
+ * AVS scan engine using ClamAV.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = AvsScannerEnine.class, configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
+@Designate(ocd = ClamScannerConfig.class)
+public class ClamScannerEngine implements AvsScannerEnine {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClamScannerEngine.class);
+
+ private ClamScannerConfig config;
+
+ /**
+ * Setup service
+ *
+ * @param config configuration
+ */
+ @Activate
+ public void activate(ClamScannerConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public ScanResult scan(InputStream content) throws AvsException {
+ try {
+ File tempFile = createTemporaryFile(content);
+ Runtime runtime = Runtime.getRuntime();
+ String command = config.command() + " " + tempFile.getPath();
+ Process process = runtime.exec(command);
+ InputStream in = process.getInputStream();
+ InputStream err = process.getErrorStream();
+ int returnCode = process.waitFor();
+ String output = IOUtils.toString(in, Charset.forName(StandardCharsets.UTF_8.name()));
+ String error = IOUtils.toString(err, Charset.forName(StandardCharsets.UTF_8.name()));
+ in.close();
+ err.close();
+ output = output.replace(tempFile.getPath(), "SCANNED_FILE");
+ Files.delete(Paths.get(tempFile.getPath()));
+ if ((returnCode == 0) && StringUtils.isBlank(error)) {
+ return new ScanResult(output, true);
+ }
+ return new ScanResult(output + "\n" + error, false);
+ } catch (IOException | InterruptedException e) {
+ LOG.error("Error during scanning", e);
+ Thread.currentThread().interrupt();
+ throw new AvsException("Error during scanning", e);
+ }
+ }
+
+ /**
+ * Creates a temporary file with the given content.
+ *
+ * @param content content
+ * @return file handle
+ * @throws IOException error creating file
+ */
+ private File createTemporaryFile(InputStream content) throws IOException {
+ File file = File.createTempFile("valtech-avs", ".tmp");
+ Files.copy(content, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ content.close();
+ return file;
+ }
+
+}
diff --git a/core/src/main/java/de/valtech/avs/core/serviceuser/ServiceResourceResolverService.java b/core/src/main/java/de/valtech/avs/core/serviceuser/ServiceResourceResolverService.java
new file mode 100644
index 0000000..99975fb
--- /dev/null
+++ b/core/src/main/java/de/valtech/avs/core/serviceuser/ServiceResourceResolverService.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.serviceuser;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Provides the service resource resolver.
+ *
+ * @author Roland Gruber
+ */
+@Component(service = ServiceResourceResolverService.class)
+public class ServiceResourceResolverService {
+
+ private static final String SUBSERVICE_AVS = "avs";
+
+ @Reference
+ ResourceResolverFactory resolverFactory;
+
+ /**
+ * Returns a resource resolver of the AVS service user.
+ *
+ * @return service resource resolver
+ * @throws LoginException error opening resource resolver
+ */
+ public ResourceResolver getServiceResourceResolver() throws LoginException {
+ final Map authenticationInfo = new HashMap<>();
+ authenticationInfo.put(ResourceResolverFactory.SUBSERVICE, SUBSERVICE_AVS);
+ return resolverFactory.getServiceResourceResolver(authenticationInfo);
+ }
+
+}
diff --git a/core/src/test/java/de/valtech/avs/core/filter/AvsPostFilterTest.java b/core/src/test/java/de/valtech/avs/core/filter/AvsPostFilterTest.java
new file mode 100644
index 0000000..550b042
--- /dev/null
+++ b/core/src/test/java/de/valtech/avs/core/filter/AvsPostFilterTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 Valtech GmbH
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+ * associated documentation files (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge, publish, distribute,
+ * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+ * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.valtech.avs.core.filter;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * Tests AvsPostFilter
+ *
+ * @author Roland Gruber
+ *
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class AvsPostFilterTest {
+
+ @Mock
+ private SlingHttpServletRequest request;
+
+ @Mock
+ private AvsPostFilterConfig config;
+
+ @InjectMocks
+ private AvsPostFilter filter;
+
+ @Before
+ public void setup() {
+ when(request.getRequestURI()).thenReturn("/some/sample/request.html");
+ }
+
+ @Test
+ public void isUrlToIgnore() {
+ filter.activate(config);
+
+ assertFalse(filter.isUrlToIgnore(request));
+ }
+
+ @Test
+ public void isUrlToIgnore_excludeFilter_match() {
+ when(config.excludePatterns()).thenReturn(new String[] {"/some/.*"});
+ filter.activate(config);
+
+ assertTrue(filter.isUrlToIgnore(request));
+ }
+
+ @Test
+ public void isUrlToIgnore_excludeFilter_nomatch() {
+ when(config.excludePatterns()).thenReturn(new String[] {"/bla"});
+ filter.activate(config);
+
+ assertFalse(filter.isUrlToIgnore(request));
+ }
+
+ @Test
+ public void isUrlToIgnore_includeFilter_match() {
+ when(config.includePatterns()).thenReturn(new String[] {"/some/.*"});
+ filter.activate(config);
+
+ assertFalse(filter.isUrlToIgnore(request));
+ }
+
+ @Test
+ public void isUrlToIgnore_includeFilter_nomatch() {
+ when(config.includePatterns()).thenReturn(new String[] {"/bla"});
+ filter.activate(config);
+
+ assertTrue(filter.isUrlToIgnore(request));
+ }
+
+}
diff --git a/docs/developers.md b/docs/developers.md
new file mode 100644
index 0000000..07f141a
--- /dev/null
+++ b/docs/developers.md
@@ -0,0 +1,35 @@
+# AEM Server Setup
+
+By default AEM is expected to listen on localhost on port 5702. This setting can be overridden by adding parameters:
+* -Daem.port=4502
+* -Daem.host=localhost
+* -Daem.publish.port=4503
+* -Daem.publish.host=localhost
+
+You need AEM 6.4 with service pack 2 or AEM 6.5.
+
+# Build and Deploy
+
+To build and deploy run this in the base (aem-virus-scan) or ui.apps/examples folder:
+
+```bash
+mvn clean install -PautoInstallPackage
+```
+
+In case you want to deploy core only you can use this command in core folder:
+
+```bash
+mvn clean install -PautoInstallBundle
+```
+
+To build and deploy on publish instance run this in the base (aem-virus-scan) or ui.apps/examples folder:
+
+```bash
+mvn clean install -PautoInstallPackagePublish
+```
+
+
+# Code Formatting
+
+Please use our standard code formatters for [Eclipse](formatter/eclipse-avs.xml)
+and [IntelliJ](formatter/intellij-avs.xml).
diff --git a/docs/formatter/eclipse-avs.xml b/docs/formatter/eclipse-avs.xml
new file mode 100644
index 0000000..ff0ca7b
--- /dev/null
+++ b/docs/formatter/eclipse-avs.xml
@@ -0,0 +1,318 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/formatter/intellij-avs.xml b/docs/formatter/intellij-avs.xml
new file mode 100644
index 0000000..49df1d8
--- /dev/null
+++ b/docs/formatter/intellij-avs.xml
@@ -0,0 +1,476 @@
+
+
+
+