diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg
index a491f0b..0b8d9dd 100644
--- a/.github/badges/branches.svg
+++ b/.github/badges/branches.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="branches: 67.8%"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="45" height="20" fill="#fe7d37"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="507">branches</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="507">branches</text><text aria-hidden="true" x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">67.8%</text><text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="350">67.8%</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="branches: 60.6%"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="45" height="20" fill="#fe7d37"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="507">branches</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="507">branches</text><text aria-hidden="true" x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">60.6%</text><text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="350">60.6%</text></g></svg>
\ No newline at end of file
diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg
index 4d8f34e..de06873 100644
--- a/.github/badges/jacoco.svg
+++ b/.github/badges/jacoco.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="coverage: 84.1%"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="45" height="20" fill="#a4a61d"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">84.1%</text><text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="350">84.1%</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="coverage: 81.1%"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="45" height="20" fill="#a4a61d"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">81.1%</text><text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="350">81.1%</text></g></svg>
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a4f4b4c..33f43ef 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,21 +7,28 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
-      - name: "Set up JDK 11"
-        uses: actions/setup-java@v2
+      - uses: actions/checkout@v3
+      - name: Set up JDKs 8, 11, 17
+        uses: actions/setup-java@v3
         with:
-          java-version: '11'
-          distribution: 'adopt'
+          distribution: 'temurin'
+          java-version: |
+            8
+            17
+            11
 
-      - name: "Build with Maven"
-        run: mvn -Pci verify
+      - name: Setup Gradle
+        uses: gradle/gradle-build-action@v2
+
+      - name: Execute Gradle build
+        run: ./gradlew build
 
       - name: "Generate Coverage Badge"
         id: jacoco
         uses: cicirello/jacoco-badge-generator@v2
         with:
           generate-branches-badge: true
+          jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv
 
       - name: "Log coverage percentage"
         run: |
@@ -40,4 +47,4 @@ jobs:
         uses: actions/upload-artifact@v2
         with:
           name: jacoco-report
-          path: target/site/jacoco/
+          path: build/reports/jacoco/test/html/
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 7d4810c..8c0b83e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,24 +10,21 @@ jobs:
     runs-on: "ubuntu-latest"
 
     steps:
-      - uses: actions/checkout@v2
-
-      - name: "Setup Java"
-        uses: actions/setup-java@v2
+      - uses: actions/checkout@v3
+      - name: Set up JDKs 8, 11, 17
+        uses: actions/setup-java@v3
         with:
-          java-version: '11'
-          distribution: 'adopt'
-          server-id: ossrh
-          server-username: MAVEN_USERNAME
-          server-password: MAVEN_PASSWORD
-          gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
-          gpg-passphrase: MAVEN_GPG_PASSPHRASE
+          distribution: 'temurin'
+          java-version: |
+            8
+            17
+            11
 
-      - name: "Build with Maven"
-        run: mvn -Pci verify
-        env:
-          MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
-          MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
+      - name: Setup Gradle
+        uses: gradle/gradle-build-action@v2
+
+      - name: Execute Gradle build
+        run: ./gradlew build
 
       - name: "Create release"
         uses: "marvinpinto/action-automatic-releases@latest"
@@ -35,18 +32,12 @@ jobs:
           repo_token: "${{ secrets.GITHUB_TOKEN }}"
           prerelease: false
           files: |
-            target/java-security-*.jar
+            build/libs/java-security-*.jar
 
       - name: "Publish to Maven Central"
-        run: |
-          mvn \
-            -Prelease \
-            -X \
-            --no-transfer-progress \
-            --batch-mode \
-            clean \
-            deploy
+        run: ./gradlew publish
         env:
-          MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
-          MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
-          MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSPHRASE }}
+          ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }}
+          ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSPHRASE }}
+          ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }}
+          ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 9823d5a..1864627 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 # Compiled class file
 *.class
-
+build
+.gradle
 # Log file
 *.log
 
@@ -12,6 +13,7 @@
 
 # Package Files #
 *.jar
+!gradle/wrapper/gradle-wrapper.jar
 *.war
 *.nar
 *.ear
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..21e2ff3
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,236 @@
+import org.javamodularity.moduleplugin.extensions.ModularityExtension
+
+plugins {
+    `java-library`
+    `maven-publish`
+    signing
+    jacoco
+    `jvm-test-suite`
+    id("com.netflix.nebula.contacts") version "7.0.1"
+    id("com.netflix.nebula.source-jar") version "20.3.0"
+    id("com.netflix.nebula.javadoc-jar") version "20.3.0"
+    id("com.netflix.nebula.maven-publish") version "20.3.0"
+    id("com.netflix.nebula.publish-verification") version "20.3.0"
+    id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
+    id("org.javamodularity.moduleplugin") version "1.8.12"
+}
+
+tasks.named<Jar>("javadocJar") {
+    exclude("module-info.class")
+}
+
+tasks.named<Jar>("sourcesJar") {
+    dependsOn("compileModuleInfoJava")
+    exclude("module-info.class")
+}
+
+tasks.named<JavaCompile>("compileJava") {
+    options.release.set(null as Int?)
+}
+
+configure<ModularityExtension> {
+    mixedJavaRelease(8)
+}
+
+tasks.named<JavaCompile>("compileModuleInfoJava") {
+    options.release.set(null as Int?)
+}
+
+repositories {
+   mavenCentral()
+}
+
+val java11SourceSet = sourceSets.create("java11") {
+    java.srcDir("src/java11/main")
+    compileClasspath += sourceSets.main.get().output
+}
+
+java {
+    withSourcesJar()
+    withJavadocJar()
+    toolchain {
+        languageVersion.set(JavaLanguageVersion.of(11))
+    }
+
+    registerFeature("java11") {
+        capability("io.github.pixee", "java11-support", version.toString())
+        usingSourceSet(java11SourceSet)
+    }
+}
+
+dependencies {
+    api("com.martiansoftware:jsap:2.1")
+    api("commons-io:commons-io:2.11.0")
+    java11SourceSet.apiConfigurationName("commons-io:commons-io:2.11.0")
+    testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
+    testImplementation("org.junit.jupiter:junit-jupiter-params")
+    testImplementation("commons-fileupload:commons-fileupload:1.3.3")
+    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+    testImplementation("org.hamcrest:hamcrest-all:1.3")
+    testImplementation("org.mockito:mockito-core:4.0.0")
+}
+
+tasks.named<JavaCompile>(java11SourceSet.compileJavaTaskName) {
+    options.release.set(9)
+}
+
+tasks.jar {
+    into("META-INF/versions/11") {
+        from(java11SourceSet.output)
+    }
+    manifest.attributes(
+        Pair("Multi-Release", "true")
+    )
+
+    inputs.files(tasks.named(java11SourceSet.compileJavaTaskName).map { it.outputs.files })
+}
+
+tasks.named(java11SourceSet.jarTaskName) {
+    // disabled because we don't want to publish this separately
+    enabled = false
+}
+
+group = "io.github.pixee"
+version = "1.0.7"
+description = "java-security-toolkit"
+
+
+extensions.getByType<nebula.plugin.contacts.ContactsExtension>().run {
+    addPerson(
+        "support@pixee.ai",
+        delegateClosureOf<nebula.plugin.contacts.Contact> {
+            moniker("Pixee")
+            github("pixee")
+        },
+    )
+}
+
+val publicationName = "nebula"
+signing {
+    if (providers.environmentVariable("CI").isPresent) {
+        val signingKey: String? by project
+        val signingPassword: String? by project
+        useInMemoryPgpKeys(signingKey, signingPassword)
+    }
+    sign(extensions.getByType<PublishingExtension>().publications.getByName(publicationName))
+}
+
+nexusPublishing {
+    repositories {
+        sonatype {
+            nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
+            snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
+        }
+    }
+}
+
+publishing {
+    publications {
+        named<MavenPublication>(publicationName) {
+            pom {
+                licenses {
+                    license {
+                        name.set("MIT License")
+                        url.set("http://www.opensource.org/licenses/mit-license.php")
+                    }
+                }
+                val scmHost = "github.com"
+                val scmProject = "pixee/java-security-toolkit"
+                val projectUrl = "https://$scmHost/$scmProject"
+                url.set(projectUrl)
+                scm {
+                    url.set(projectUrl)
+                    connection.set("scm:git:git@$scmHost:$scmProject")
+                    developerConnection.set(connection)
+                }
+            }
+        }
+    }
+}
+
+tasks.jacocoTestReport {
+    dependsOn(tasks.test)
+    reports {
+        csv.required.set(true)
+    }
+}
+
+tasks.test {
+    useJUnitPlatform()
+    finalizedBy(tasks.jacocoTestReport)
+    extensions.configure(org.javamodularity.moduleplugin.extensions.TestModuleOptions::class) {
+        // Avoid modules in tests so we can test against Java/JDK 8.
+        setRunOnClasspath(true)
+    }
+
+    javaLauncher.set(javaToolchains.launcherFor {
+        languageVersion.set(JavaLanguageVersion.of(8))
+    })
+}
+
+testing {
+    suites {
+        @Suppress("UnstableApiUsage")
+        register<JvmTestSuite>("java11Test") {
+            useJUnitJupiter()
+            dependencies {
+                runtimeOnly(project())
+                implementation(project()) {
+                    capabilities {
+                        requireCapabilities("io.github.pixee:java11-support")
+                    }
+                }
+                implementation("org.hamcrest:hamcrest-all:1.3")
+                implementation("org.mockito:mockito-core:4.0.0")
+                implementation("commons-fileupload:commons-fileupload:1.3.3")
+            }
+        }
+
+        register<JvmTestSuite>("integrationTest") {
+            useJUnitJupiter()
+            dependencies {
+                implementation("org.junit.jupiter:junit-jupiter-params")
+                implementation("org.testcontainers:testcontainers:1.19.0")
+                implementation("ch.qos.logback:logback-classic:1.2.6")
+            }
+        }
+    }
+}
+
+tasks.named<Test>("java11Test") {
+    systemProperty("org.apache.commons.fileupload.disk.DiskFileItem.serializable", "true")
+}
+
+val java11Test = tasks.register<Test>("testOn11") {
+    useJUnitPlatform()
+    javaLauncher.set(javaToolchains.launcherFor {
+        languageVersion.set(JavaLanguageVersion.of(11))
+    })
+}
+
+val java17Test = tasks.register<Test>("testOn17") {
+    useJUnitPlatform()
+    javaLauncher.set(javaToolchains.launcherFor {
+        languageVersion.set(JavaLanguageVersion.of(17))
+    })
+}
+
+tasks.named<Test>("integrationTest") {
+    this.inputs.file(tasks.jar.map { it.archiveFile} )
+    dependsOn(":test-apps:hello-world:jibDockerBuild")
+    dependsOn(":test-apps:hello-world-modules:jibDockerBuild")
+    systemProperty("securityToolkitJarPath", tasks.jar.get().archiveFile.get().asFile.relativeTo(projectDir).path)
+}
+
+tasks.check {
+    @Suppress("UnstableApiUsage")
+    dependsOn(java11Test, java17Test, testing.suites.named("java11Test"), testing.suites.named("integrationTest"))
+}
+
+tasks.compileTestJava {
+    extensions.configure(org.javamodularity.moduleplugin.extensions.CompileTestModuleOptions::class) {
+        // Avoid modules in tests so we can test against Java/JDK 8.
+        setCompileOnClasspath(true)
+    }
+    options.release.set(8)
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7f93135
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ac72c34
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..0adc8e1
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..6689b85
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 5d1001c..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,356 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <groupId>io.github.pixee</groupId>
-    <artifactId>java-security-toolkit</artifactId>
-    <version>1.0.7</version>
-
-    <name>java-security-toolkit</name>
-    <description>a library with common security controls</description>
-    <url>https://github.com/pixee/java-security-toolkit</url>
-
-    <scm>
-        <connection>scm:git:git://github.com/pixee/java-security-toolkit.git</connection>
-        <developerConnection>scm:git:ssh://github.com:pixee/java-security-toolkit.git</developerConnection>
-        <url>https://github.com/pixee/java-security-toolkit/tree/main</url>
-    </scm>
-
-    <licenses>
-        <license>
-            <name>MIT License</name>
-            <url>http://www.opensource.org/licenses/mit-license.php</url>
-        </license>
-    </licenses>
-
-    <distributionManagement>
-        <snapshotRepository>
-            <id>ossrh</id>
-            <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
-        </snapshotRepository>
-        <repository>
-            <id>ossrh</id>
-            <url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-        </repository>
-    </distributionManagement>
-
-    <developers>
-        <developer>
-            <name>Arshan Dabirsiaghi</name>
-            <organization>pixee</organization>
-            <organizationUrl>https://pixee.ai/</organizationUrl>
-        </developer>
-    </developers>
-
-    <properties>
-        <fmt.goal>format</fmt.goal>
-
-        <versions.junit-jupiter>5.8.1</versions.junit-jupiter>
-        <versions.hamcrest>1.3</versions.hamcrest>
-
-        <versions.maven-compiler-plugin>3.8.1</versions.maven-compiler-plugin>
-        <versions.maven-surefire-plugin>3.0.0-M4</versions.maven-surefire-plugin>
-
-        <versions.fmt-maven-plugin>2.18</versions.fmt-maven-plugin>
-        <versions.maven-shade-plugin>3.2.4</versions.maven-shade-plugin>
-        <versions.maven-source-plugin>2.2.1</versions.maven-source-plugin>
-        <versions.maven-gpg-plugin>3.0.1</versions.maven-gpg-plugin>
-        <versions.jacoco-maven-plugin>0.8.7</versions.jacoco-maven-plugin>
-
-        <versions.maven-javadoc-plugin>2.9.1</versions.maven-javadoc-plugin>
-        <versions.nexus-staging-maven-plugin>1.6.13</versions.nexus-staging-maven-plugin>
-    </properties>
-
-    <dependencies>
-
-        <!-- needed for XSS escapers -->
-        <dependency>
-            <groupId>com.coverity.security</groupId>
-            <artifactId>coverity-escapers</artifactId>
-            <version>1.1.1</version>
-        </dependency>
-
-        <!-- needed for command line parsers -->
-        <dependency>
-            <groupId>com.martiansoftware</groupId>
-            <artifactId>jsap</artifactId>
-            <version>2.1</version>
-        </dependency>
-
-        <!-- needed for ValidatingObjectInputStream in deserialization -->
-        <dependency>
-            <groupId>commons-io</groupId>
-            <artifactId>commons-io</artifactId>
-            <version>2.11.0</version>
-        </dependency>
-        <dependency>
-            <groupId>org.codehaus.mojo</groupId>
-            <artifactId>animal-sniffer-annotations</artifactId>
-            <version>1.23</version>
-            <optional>true</optional>
-        </dependency>
-        <!-- needed for testing deserialization protection -->
-        <dependency>
-            <groupId>commons-fileupload</groupId>
-            <artifactId>commons-fileupload</artifactId>
-            <version>1.5</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-api</artifactId>
-            <version>${versions.junit-jupiter}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-params</artifactId>
-            <version>${versions.junit-jupiter}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.hamcrest</groupId>
-            <artifactId>hamcrest-all</artifactId>
-            <version>${versions.hamcrest}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>4.0.0</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>4.0.0</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <pluginManagement>
-            <plugins>
-                <plugin>
-                    <groupId>org.apache.maven.plugins</groupId>
-                    <artifactId>maven-source-plugin</artifactId>
-                    <version>${versions.maven-source-plugin}</version>
-                    <executions>
-                        <execution>
-                            <id>attach-sources</id>
-                            <goals>
-                                <goal>jar-no-fork</goal>
-                            </goals>
-                        </execution>
-                    </executions>
-                </plugin>
-                <plugin>
-                    <groupId>org.apache.maven.plugins</groupId>
-                    <artifactId>maven-javadoc-plugin</artifactId>
-                    <version>${versions.maven-javadoc-plugin}</version>
-                    <executions>
-                        <execution>
-                            <id>attach-javadocs</id>
-                            <goals>
-                                <goal>jar</goal>
-                            </goals>
-                        </execution>
-                    </executions>
-                </plugin>
-                <plugin>
-                    <artifactId>maven-compiler-plugin</artifactId>
-                    <version>${versions.maven-compiler-plugin}</version>
-                    <configuration>
-                        <source>8</source>
-                        <target>8</target>
-                    </configuration>
-                </plugin>
-                <plugin>
-                    <groupId>org.codehaus.mojo</groupId>
-                    <artifactId>animal-sniffer-maven-plugin</artifactId>
-                    <version>1.23</version>
-                    <configuration>
-                        <signature>
-                            <groupId>org.codehaus.mojo.signature</groupId>
-                            <artifactId>java18</artifactId>
-                            <version>1.0</version>
-                        </signature>
-                    </configuration>
-                    <executions>
-                        <execution>
-                            <id>animal-sniffer</id>
-                            <phase>test</phase>
-                            <goals>
-                                <goal>check</goal>
-                            </goals>
-                        </execution>
-                    </executions>
-                </plugin>
-                <plugin>
-                    <groupId>org.jacoco</groupId>
-                    <artifactId>jacoco-maven-plugin</artifactId>
-                    <version>${versions.jacoco-maven-plugin}</version>
-                    <executions>
-                        <execution>
-                            <goals>
-                                <goal>prepare-agent</goal>
-                            </goals>
-                        </execution>
-                        <execution>
-                            <id>generate-code-coverage-report</id>
-                            <phase>test</phase>
-                            <goals>
-                                <goal>report</goal>
-                            </goals>
-                        </execution>
-                    </executions>
-                </plugin>
-                <plugin>
-                    <artifactId>maven-surefire-plugin</artifactId>
-                    <version>${versions.maven-surefire-plugin}</version>
-                </plugin>
-                <plugin>
-                    <groupId>com.spotify.fmt</groupId>
-                    <artifactId>fmt-maven-plugin</artifactId>
-                    <version>${versions.fmt-maven-plugin}</version>
-                    <executions>
-                        <execution>
-                            <id>format-java</id>
-                            <phase>validate</phase>
-                            <goals>
-                                <goal>${fmt.goal}</goal>
-                            </goals>
-                        </execution>
-                    </executions>
-                </plugin>
-                <!-- Create a runnable JAR -->
-                <plugin>
-                    <groupId>org.apache.maven.plugins</groupId>
-                    <artifactId>maven-shade-plugin</artifactId>
-                    <version>${versions.maven-shade-plugin}</version>
-                    <executions>
-                        <execution>
-                            <phase>package</phase>
-                            <goals>
-                                <goal>shade</goal>
-                            </goals>
-                            <configuration>
-                                <filters>
-                                    <filter>
-                                        <artifact>*:*</artifact>
-                                        <excludes>
-                                            <exclude>META-INF/*.SF</exclude>
-                                            <exclude>META-INF/*.DSA</exclude>
-                                            <exclude>META-INF/*.RSA</exclude>
-                                        </excludes>
-                                    </filter>
-                                </filters>
-                            </configuration>
-                        </execution>
-                    </executions>
-                </plugin>
-                <plugin>
-                    <groupId>org.sonatype.plugins</groupId>
-                    <artifactId>nexus-staging-maven-plugin</artifactId>
-                    <version>${versions.nexus-staging-maven-plugin}</version>
-                    <extensions>true</extensions>
-                    <configuration>
-                        <serverId>ossrh</serverId>
-                        <nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
-                        <keepStagingRepositoryOnCloseRuleFailure>true</keepStagingRepositoryOnCloseRuleFailure>
-                        <autoReleaseAfterClose>true</autoReleaseAfterClose>
-                    </configuration>
-                </plugin>
-            </plugins>
-        </pluginManagement>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <artifactId>maven-compiler-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>animal-sniffer-maven-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.jacoco</groupId>
-                <artifactId>jacoco-maven-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <artifactId>maven-surefire-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>com.spotify.fmt</groupId>
-                <artifactId>fmt-maven-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-shade-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-gpg-plugin</artifactId>
-            </plugin>
-            <plugin>
-                <groupId>org.sonatype.plugins</groupId>
-                <artifactId>nexus-staging-maven-plugin</artifactId>
-            </plugin>
-        </plugins>
-    </build>
-
-    <profiles>
-        <profile>
-            <id>ci</id>
-            <activation>
-                <property>
-                    <name>env.CI</name>
-                </property>
-            </activation>
-            <properties>
-                <!-- when running in CI we enforce formatting rather than applying it -->
-                <fmt.goal>check</fmt.goal>
-            </properties>
-        </profile>
-        <profile>
-            <id>release</id>
-            <activation>
-                <property>
-                    <name>env.RELEASE</name>
-                </property>
-            </activation>
-            <build>
-                <plugins>
-                    <plugin>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-gpg-plugin</artifactId>
-                        <version>${versions.maven-gpg-plugin}</version>
-                        <executions>
-                            <execution>
-                                <id>sign-artifacts</id>
-                                <phase>verify</phase>
-                                <goals>
-                                    <goal>sign</goal>
-                                </goals>
-                                <configuration>
-                                    <gpgArguments>
-                                        <arg>--pinentry-mode</arg>
-                                        <arg>loopback</arg>
-                                    </gpgArguments>
-                                </configuration>
-                            </execution>
-                        </executions>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-    </profiles>
-
-</project>
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..3b9f119
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,25 @@
+rootProject.name = "java-security-toolkit"
+
+plugins {
+    id("com.gradle.enterprise") version "3.14.1"
+}
+
+val isCI = providers.environmentVariable("CI").isPresent
+
+include("test-apps:hello-world")
+include("test-apps:hello-world-modules")
+gradleEnterprise {
+    buildScan {
+        termsOfServiceUrl = "https://gradle.com/terms-of-service"
+        termsOfServiceAgree = "yes"
+        isUploadInBackground = !isCI
+
+        if (isCI) {
+            publishAlways()
+        }
+
+        capture {
+            isTaskInputFiles = true
+        }
+    }
+}
diff --git a/src/integrationTest/java/io/github/pixee/SmokeTest.java b/src/integrationTest/java/io/github/pixee/SmokeTest.java
new file mode 100644
index 0000000..5eadd0d
--- /dev/null
+++ b/src/integrationTest/java/io/github/pixee/SmokeTest.java
@@ -0,0 +1,26 @@
+package io.github.pixee;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+import java.nio.file.Path;
+
+
+final class SmokeTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = {"pixee/hello-world-modules", "pixee/hello-world"})
+    void containerStarts(final String imageName) {
+        Path securityToolkitJarPath = Path.of(System.getProperty("securityToolkitJarPath"));
+
+        try (
+                GenericContainer<?> container = new GenericContainer<>(imageName)
+                        .withCopyFileToContainer(MountableFile.forHostPath(securityToolkitJarPath), "/app/libs/")
+                        .waitingFor(Wait.forLogMessage("Hello, World!\n", 1))
+        ) {
+            container.start();
+        }
+    }
+}
diff --git a/src/main/java/io/github/pixee/security/ObjectInputFilters.java b/src/java11/main/io/github/pixee/security/ObjectInputFilters.java
similarity index 75%
rename from src/main/java/io/github/pixee/security/ObjectInputFilters.java
rename to src/java11/main/io/github/pixee/security/ObjectInputFilters.java
index 9f2d7ab..2299104 100644
--- a/src/main/java/io/github/pixee/security/ObjectInputFilters.java
+++ b/src/java11/main/io/github/pixee/security/ObjectInputFilters.java
@@ -1,21 +1,17 @@
 package io.github.pixee.security;
 
-import java.io.IOException;
-import java.io.InputStream;
 import java.io.ObjectInputFilter;
 import java.io.ObjectInputStream;
 import java.util.Objects;
-import org.apache.commons.io.serialization.ValidatingObjectInputStream;
-import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
 
 /**
- * This type exposes helper methods that will help defend against Java deserialization attacks.
+ * This type exposes helper methods that will help defend against Java deserialization attacks
+ * leveraging the Java 9+ API {@link ObjectInputFilter}.
  *
  * <p>For more information on deserialization checkout the <a
  * href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html">OWASP
  * Cheat Sheet</a>.
  */
-@IgnoreJRERequirement
 public final class ObjectInputFilters {
 
   private ObjectInputFilters() {}
@@ -80,7 +76,6 @@ public static ObjectInputFilter createCombinedHardenedObjectFilter(
     return new CombinedObjectInputFilter(existingFilter);
   }
 
-  @IgnoreJRERequirement
   private static class CombinedObjectInputFilter implements ObjectInputFilter {
     private final ObjectInputFilter originalFilter;
 
@@ -97,30 +92,6 @@ public Status checkInput(final FilterInfo filterInfo) {
     }
   }
 
-  /**
-   * This method returns a wrapped {@link ObjectInputStream} that protects against deserialization
-   * code execution attacks. This method can be used in Java 8 and previous.
-   *
-   * @param ois the stream to wrap and harden
-   * @return an {@link ObjectInputStream} which is safe against all publicly known gadgets
-   * @throws IOException if the underlying creation of {@link ObjectInputStream} fails
-   */
-  public static ObjectInputStream createSafeObjectInputStream(final InputStream ois)
-      throws IOException {
-    try {
-      final ValidatingObjectInputStream is = new ValidatingObjectInputStream(ois);
-      for (String gadget : UnwantedTypes.dangerousClassNameTokens()) {
-        is.reject("*" + gadget + "*");
-      }
-      return is;
-    } catch (IOException e) {
-      // ignored
-    }
-
-    // if for some reason we can't replace it, we'll pass it back as it was given
-    return new ObjectInputStream(ois);
-  }
-
   private static final ObjectInputFilter basicGadgetDenylistFilter =
       ObjectInputFilter.Config.createFilter(
           "!" + String.join("*;!", UnwantedTypes.dangerousClassNameTokens()));
diff --git a/src/test/java/io/github/pixee/security/ObjectInputFiltersTest.java b/src/java11Test/java/io/github/pixee/security/ObjectInputFiltersTest.java
similarity index 94%
rename from src/test/java/io/github/pixee/security/ObjectInputFiltersTest.java
rename to src/java11Test/java/io/github/pixee/security/ObjectInputFiltersTest.java
index d4c64b5..ca79d04 100644
--- a/src/test/java/io/github/pixee/security/ObjectInputFiltersTest.java
+++ b/src/java11Test/java/io/github/pixee/security/ObjectInputFiltersTest.java
@@ -41,17 +41,6 @@ void default_is_unprotected() throws Exception {
     assertThat(o, instanceOf(DiskFileItem.class));
   }
 
-  @Test
-  void validating_ois_works() throws Exception {
-    ObjectInputStream ois =
-        ObjectInputFilters.createSafeObjectInputStream(new ByteArrayInputStream(serializedGadget));
-    assertThrows(
-        InvalidClassException.class,
-        () -> {
-          ois.readObject();
-          fail("this should have been blocked");
-        });
-  }
 
   @Test
   void ois_harden_works() throws Exception {
diff --git a/src/main/java/io/github/pixee/security/HtmlEncoder.java b/src/main/java/io/github/pixee/security/HtmlEncoder.java
index 6e5e072..0a71f18 100644
--- a/src/main/java/io/github/pixee/security/HtmlEncoder.java
+++ b/src/main/java/io/github/pixee/security/HtmlEncoder.java
@@ -1,7 +1,5 @@
 package io.github.pixee.security;
 
-import com.coverity.security.Escape;
-
 /**
  * This type exposes helper methods that will help defend against XSS attacks with HTML encoding.
  *
@@ -21,4 +19,292 @@ private HtmlEncoder() {}
   public static String encode(final String s) {
     return Escape.html(s);
   }
+
+  /*
+   * This code was originally brought in as the BSD-2 licensed dependency from Coverity. However, it didn't publish any automatic module name, and we didn't really need any of the rest of it, so we just copied the code here, including the license.
+   */
+
+  /**
+   * Copyright (c) 2012-2016, Coverity, Inc. All rights reserved.
+   *
+   * <p>Redistribution and use in source and binary forms, with or without modification, are
+   * permitted provided that the following conditions are met: - Redistributions of source code must
+   * retain the above copyright notice, this list of conditions and the following disclaimer. -
+   * Redistributions in binary form must reproduce the above copyright notice, this list of
+   * conditions and the following disclaimer in the documentation and/or other materials provided
+   * with the distribution. - Neither the name of Coverity, Inc. nor the names of its contributors
+   * may be used to endorse or promote products derived from this software without specific prior
+   * written permission from Coverity, Inc.
+   *
+   * <p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
+   * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND INFRINGEMENT ARE DISCLAIMED. IN NO EVENT
+   * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+   */
+  /**
+   * Escape is a small set of methods for escaping tainted data. These escaping methods are useful
+   * in transforming user-controlled ("tainted") data into forms that are safe from being
+   * interpreted as something other than data, such as JavaScript.
+   *
+   * <p>At this time most of these escaping routines focus on cross-site scripting mitigations. Each
+   * method is good for a different HTML context. For a primer on HTML contexts, see OWASP's XSS
+   * Prevention Cheat Sheet (note however that the escaping routines are not implemented exactly
+   * according to OWASP's recommendations) or the Coverity Security Advisor documentation. Also see
+   * the Coverity Security Research Laboratory blog on how to properly use each function.
+   *
+   * <p>While Coverity's static analysis product references these escaping routines as exemplars and
+   * understands their behavior, there is no dependency on Coverity products and these routines are
+   * completely standalone. Feel free to use them! Just make sure you use them correctly.
+   *
+   * @author Romain Gaucher
+   * @author Andy Chou
+   * @author Jon Passki
+   * @author Alex Kouzemtchenko
+   */
+  private static class Escape {
+
+    /**
+     * HTML entity escaping for text content and attributes.
+     *
+     * <p>HTML entity escaping that is appropriate for the most common HTML contexts: PCDATA and
+     * "normal" attributes (non-URI, non-event, and non-CSS attributes). <br>
+     * Note that we do not recommend using non-quoted HTML attributes since the security obligations
+     * vary more between web browser. We recommend to always quote (single or double quotes) HTML
+     * attributes.<br>
+     * This method is generic to HTML entity escaping, and therefore escapes more characters than
+     * usually necessary -- mostly to handle non-quoted attribute values. If this method is somehow
+     * too slow, such as you output megabytes of text with spaces, please use the {@link
+     * #htmlText(String)} method which only escape HTML text specific characters.
+     *
+     * <p>The following characters are escaped:
+     *
+     * <ul>
+     *   <li>HTML characters: <code>' (U+0022)</code>, <code>" (U+0027)</code>, <code>\ (U+005C)
+     *       </code>, <code>/ (U+002F)</code>, <code>&lt; (U+003C)</code>, <code>&gt; (U+003E)
+     *       </code>, <code>&amp; (U+0026)</code>
+     *   <li>Control characters: <code>\t (U+0009)</code>, <code>\n (U+000A)</code>, <code>
+     *       \f (U+000C)</code>, <code>\r (U+000D)</code>, <code>SPACE (U+0020)</code>
+     *   <li>Unicode newlines: <code>LS (U+2028)</code>, <code>PS (U+2029)</code>
+     * </ul>
+     *
+     * @param input the string to be escaped
+     * @return the HTML escaped string or <code>null</code> if <code>input</code> is null
+     * @since 1.0
+     */
+    private static String html(String input) {
+      if (input == null) return null;
+
+      int length = input.length();
+      StringBuilder output = allocateStringBuilder(length);
+
+      for (int i = 0; i < length; i++) {
+        char c = input.charAt(i);
+        switch (c) {
+            // Control chars
+          case '\t':
+            output.append("&#x09;");
+            break;
+          case '\n':
+            output.append("&#x0A;");
+            break;
+          case '\f':
+            output.append("&#x0C;");
+            break;
+          case '\r':
+            output.append("&#x0D;");
+            break;
+            // Chars that have a meaning for HTML
+          case '\'':
+            output.append("&#39;");
+            break;
+          case '\\':
+            output.append("&#x5C;");
+            break;
+          case ' ':
+            output.append("&#x20;");
+            break;
+          case '/':
+            output.append("&#x2F;");
+            break;
+          case '"':
+            output.append("&quot;");
+            break;
+          case '<':
+            output.append("&lt;");
+            break;
+          case '>':
+            output.append("&gt;");
+            break;
+          case '&':
+            output.append("&amp;");
+            break;
+            // Unicode new lines
+          case '\u2028':
+            output.append("&#x2028;");
+            break;
+          case '\u2029':
+            output.append("&#x2029;");
+            break;
+
+          default:
+            output.append(c);
+            break;
+        }
+      }
+      return output.toString();
+    }
+
+    /**
+     * URI encoder.
+     *
+     * <p>URI encoding for query string values of the URI: <code>
+     * /example/?name=URI_ENCODED_VALUE_HERE</code> <br>
+     * Note that this method is not sufficient to protect for cross-site scripting in a generic URI
+     * context, but only for query string values. If you need to escape a URI in an <code>href
+     * </code> attribute (for example), ensure that:
+     *
+     * <ul>
+     *   <li>The scheme is allowed (restrict to http, https, or mailto)
+     *   <li>Use the HTML escaper {@link #html(String)} on the entire URI
+     * </ul>
+     *
+     * This URI encoder processes the following characters:
+     *
+     * <ul>
+     *   <li>URI characters: <code>' (U+0022)</code>, <code>" (U+0027)</code>, <code>\ (U+005C)
+     *       </code>, <code>/ (U+002F)</code>, <code>&lt; (U+003C)</code>, <code>&gt; (U+003E)
+     *       </code>, <code>&amp; (U+0026)</code>, <code>&lt; (U+003C)</code>, <code>&gt; (U+003E)
+     *       </code>, <code>! (U+0021)</code>, <code># (U+0023)</code>, <code>$ (U+0024)</code>,
+     *       <code>% (U+0025)</code>, <code>( (U+0028)</code>, <code>) (U+0029)</code>, <code>
+     *       * (U+002A)</code>, <code>+ (U+002B)</code>, <code>, (U+002C)</code>, <code>. (U+002E)
+     *       </code>, <code>: (U+003A)</code>, <code>; (U+003B)</code>, <code>= (U+003D)</code>,
+     *       <code>? (U+003F)</code>, <code>@ (U+0040)</code>, <code>[ (U+005B)</code>, <code>
+     *       ] (U+005D)</code>
+     *   <li>Control characters: <code>\t (U+0009)</code>, <code>\n (U+000A)</code>, <code>
+     *       \f (U+000C)</code>, <code>\r (U+000D)</code>, <code>SPACE (U+0020)</code>
+     * </ul>
+     *
+     * @param input the string to be escaped
+     * @return the URI encoded string or <code>null</code> if <code>input</code> is null
+     * @since 1.0
+     */
+    private static String uriParam(String input) {
+      if (input == null) return null;
+
+      int length = input.length();
+      StringBuilder output = allocateStringBuilder(length);
+
+      for (int i = 0; i < length; i++) {
+        char c = input.charAt(i);
+        switch (c) {
+            // Control chars
+          case '\t':
+            output.append("%09");
+            break;
+          case '\n':
+            output.append("%0A");
+            break;
+          case '\f':
+            output.append("%0C");
+            break;
+          case '\r':
+            output.append("%0D");
+            break;
+            // RFC chars to encode, plus % ' " < and >, and space
+          case ' ':
+            output.append("%20");
+            break;
+          case '!':
+            output.append("%21");
+            break;
+          case '"':
+            output.append("%22");
+            break;
+          case '#':
+            output.append("%23");
+            break;
+          case '$':
+            output.append("%24");
+            break;
+          case '%':
+            output.append("%25");
+            break;
+          case '&':
+            output.append("%26");
+            break;
+          case '\'':
+            output.append("%27");
+            break;
+          case '(':
+            output.append("%28");
+            break;
+          case ')':
+            output.append("%29");
+            break;
+          case '*':
+            output.append("%2A");
+            break;
+          case '+':
+            output.append("%2B");
+            break;
+          case ',':
+            output.append("%2C");
+            break;
+          case '.':
+            output.append("%2E");
+            break;
+          case '/':
+            output.append("%2F");
+            break;
+          case ':':
+            output.append("%3A");
+            break;
+          case ';':
+            output.append("%3B");
+            break;
+          case '<':
+            output.append("%3C");
+            break;
+          case '=':
+            output.append("%3D");
+            break;
+          case '>':
+            output.append("%3E");
+            break;
+          case '?':
+            output.append("%3F");
+            break;
+          case '@':
+            output.append("%40");
+            break;
+          case '[':
+            output.append("%5B");
+            break;
+          case ']':
+            output.append("%5D");
+            break;
+
+          default:
+            output.append(c);
+            break;
+        }
+      }
+      return output.toString();
+    }
+
+    /** Compute the allocation size of the StringBuilder based on the length. */
+    private static StringBuilder allocateStringBuilder(int length) {
+      // Allocate enough temporary buffer space to avoid reallocation in most
+      // cases. If you believe you will output large amount of data at once
+      // you might need to change the factor.
+      int buflen = length;
+      if (length * 2 > 0) buflen = length * 2;
+      return new StringBuilder(buflen);
+    }
+  }
 }
diff --git a/src/main/java/io/github/pixee/security/ObjectInputStreams.java b/src/main/java/io/github/pixee/security/ObjectInputStreams.java
new file mode 100644
index 0000000..4fc120f
--- /dev/null
+++ b/src/main/java/io/github/pixee/security/ObjectInputStreams.java
@@ -0,0 +1,40 @@
+package io.github.pixee.security;
+
+import org.apache.commons.io.serialization.ValidatingObjectInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+
+/**
+ * This type exposes helper methods that will help defend against Java deserialization attacks
+ * leveraging {@link ObjectInputStream} APIs.
+ *
+ * <p>For more information on deserialization checkout the <a
+ * href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html">OWASP
+ * Cheat Sheet</a>.
+ */
+public final class ObjectInputStreams {
+
+    /**
+     * Private no-op constructor to prevent accidental initialization of this class
+     */
+    private ObjectInputStreams() {}
+
+    /**
+     * This method returns a wrapped {@link ObjectInputStream} that protects against deserialization
+     * code execution attacks. This method can be used in Java 8 and previous.
+     *
+     * @param ois the stream to wrap and harden
+     * @return an {@link ObjectInputStream} which is safe against all publicly known gadgets
+     * @throws IOException if the underlying creation of {@link ObjectInputStream} fails
+     */
+    public static ObjectInputStream createValidatingObjectInputStream(final InputStream ois)
+            throws IOException {
+        final ValidatingObjectInputStream is = new ValidatingObjectInputStream(ois);
+        for (String gadget : UnwantedTypes.dangerousClassNameTokens()) {
+            is.reject("*" + gadget + "*");
+        }
+        return is;
+    }
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..8384d9e
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,9 @@
+open module io.github.pixee.security {
+    exports io.github.pixee.security;
+    exports io.github.pixee.security.jakarta;
+
+    requires org.apache.commons.io;
+    requires java.xml;
+    requires java.desktop;
+    requires java.base;
+}
\ No newline at end of file
diff --git a/src/test/java/io/github/pixee/security/ObjectInputStreamsTest.java b/src/test/java/io/github/pixee/security/ObjectInputStreamsTest.java
new file mode 100644
index 0000000..24a1d84
--- /dev/null
+++ b/src/test/java/io/github/pixee/security/ObjectInputStreamsTest.java
@@ -0,0 +1,54 @@
+package io.github.pixee.security;
+
+import org.apache.commons.fileupload.disk.DiskFileItem;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InvalidClassException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.file.Files;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+final class ObjectInputStreamsTest {
+
+    private static DiskFileItem gadget; // this is an evil gadget type
+    private static byte[] serializedGadget; // this the serialized bytes of that gadget
+
+    @BeforeAll
+    static void setup() throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        gadget =
+                new DiskFileItem(
+                        "fieldName",
+                        "text/html",
+                        false,
+                        "foo.html",
+                        100,
+                        Files.createTempDirectory("adi").toFile());
+        gadget.getOutputStream(); // needed to make the object serializable
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(gadget);
+        serializedGadget = baos.toByteArray();
+    }
+
+
+    @Test
+    void validating_ois_works() throws Exception {
+        ObjectInputStream ois =
+                ObjectInputStreams.createValidatingObjectInputStream(new ByteArrayInputStream(serializedGadget));
+        assertThrows(
+                InvalidClassException.class,
+                () -> {
+                    ois.readObject();
+                    fail("this should have been blocked");
+                });
+    }
+
+
+}
\ No newline at end of file
diff --git a/test-apps/hello-world-modules/build.gradle.kts b/test-apps/hello-world-modules/build.gradle.kts
new file mode 100644
index 0000000..e3993e5
--- /dev/null
+++ b/test-apps/hello-world-modules/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+    java
+    id("com.google.cloud.tools.jib") version "3.4.0"
+}
+
+repositories {
+    mavenCentral()
+}
+
+
+java {
+    modularity.inferModulePath.set(true)
+    toolchain {
+        languageVersion.set(JavaLanguageVersion.of(11))
+    }
+}
+
+jib.container {
+    entrypoint = listOf("java", "--module-path", "@/app/jib-classpath-file", "-m", "io.github.pixee.testapp/io.github.pixee.testapp.Main")
+}
+
+jib.to.image = "pixee/${project.name}"
+
+
+dependencies {
+    implementation(project.rootProject)
+}
\ No newline at end of file
diff --git a/test-apps/hello-world-modules/src/main/java/io/github/pixee/testapp/Main.java b/test-apps/hello-world-modules/src/main/java/io/github/pixee/testapp/Main.java
new file mode 100644
index 0000000..9b89062
--- /dev/null
+++ b/test-apps/hello-world-modules/src/main/java/io/github/pixee/testapp/Main.java
@@ -0,0 +1,15 @@
+package io.github.pixee.testapp;
+
+import io.github.pixee.security.HostValidator;
+
+public final class Main {
+/** A simple piece of code that references the library, so we know the module visibility is correct. */
+    public static void main(final String[] args) {
+        String message = "Hello, World!";
+        if (HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS.isAllowed(message)) {
+            System.out.println(message);
+        } else {
+            System.out.println("Access denied");
+        }
+    }
+}
diff --git a/test-apps/hello-world-modules/src/main/java/module-info.java b/test-apps/hello-world-modules/src/main/java/module-info.java
new file mode 100644
index 0000000..d69535a
--- /dev/null
+++ b/test-apps/hello-world-modules/src/main/java/module-info.java
@@ -0,0 +1,5 @@
+module io.github.pixee.testapp {
+    exports io.github.pixee.testapp;
+
+    requires io.github.pixee.security;
+}
\ No newline at end of file
diff --git a/test-apps/hello-world/build.gradle.kts b/test-apps/hello-world/build.gradle.kts
new file mode 100644
index 0000000..2e085a7
--- /dev/null
+++ b/test-apps/hello-world/build.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+    java
+    id("com.google.cloud.tools.jib") version "3.4.0"
+}
+
+repositories {
+    mavenCentral()
+}
+
+java {
+    toolchain {
+        languageVersion.set(JavaLanguageVersion.of(8))
+    }
+}
+
+jib.to.image = "pixee/${project.name}"
+
+dependencies {
+  compileOnly(project.rootProject)
+}
\ No newline at end of file
diff --git a/test-apps/hello-world/src/main/java/io/github/pixee/testapp/Main.java b/test-apps/hello-world/src/main/java/io/github/pixee/testapp/Main.java
new file mode 100644
index 0000000..15b0a4e
--- /dev/null
+++ b/test-apps/hello-world/src/main/java/io/github/pixee/testapp/Main.java
@@ -0,0 +1,15 @@
+package io.github.pixee.testapp;
+
+import io.github.pixee.security.HostValidator;
+
+public final class Main {
+
+    public static void main(final String[] args) {
+        String message = "Hello, World!";
+        if (HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS.isAllowed(message)) {
+            System.out.println(message);
+        } else {
+            System.out.println("Access denied");
+        }
+    }
+}
