diff --git a/build.gradle b/build.gradle
index 8be069aa8854291766703fe104d0fe764bf22d68..69670a47bb569251891f60c318f0d57d4481b279 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,7 +12,7 @@ buildscript {
 }
 
 plugins {
-    id "com.github.spotbugs-base" version "5.0.4" apply false
+    id "com.github.spotbugs-base" version "5.0.10" apply false
     id "com.github.hierynomus.license-report" version "0.15.0"
 }
 
@@ -73,7 +73,7 @@ allprojects {
     }
 
     spotbugs {
-        toolVersion = '3.1.12'
+        toolVersion = '4.7.1'
 
         showProgress = true
         effort = 'max'
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
index e3eac59b513ab89e5454c1f12b06534b034d24ce..d98aad57d0eee555233c83bc87fc101de8e9fe95 100644
--- a/config/checkstyle/suppressions.xml
+++ b/config/checkstyle/suppressions.xml
@@ -17,4 +17,6 @@
 
     <suppress checks="ParameterNumber" files="CPMController.java" />
 
+    <suppress checks="VisibilityModifier" files=".*Dto\.java$" />
+
 </suppressions>
diff --git a/config/spotbugs/suppressions.xml b/config/spotbugs/suppressions.xml
index 8ea457515d9e99bd7a709cf390e9fad6dbb59e08..4b41cb2f36e2f6d9ccb708eb99cc8b9337d54ea6 100644
--- a/config/spotbugs/suppressions.xml
+++ b/config/spotbugs/suppressions.xml
@@ -1,10 +1,19 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <FindBugsFilter
-        xmlns="http://findbugs.sourceforge.net/filter/3.0.0"
+        xmlns="https://github.com/spotbugs/filter/3.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://findbugs.sourceforge.net/filter/3.0.0 https://raw.githubusercontent.com/findbugsproject/findbugs/master/findbugs/etc/findbugsfilter.xsd">
+        xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
   <Match>
     <!-- Disable SpotBugs for ANTLR generated classes -->
     <Class name="~nl\.tudelft\.ewi\.auta\.checker\.grammar.*" />
   </Match>
+
+  <Match>
+    <!-- "may expose internal representation by storing an externally mutable object" is more or
+         less impossible to fix without a rewrite -->
+    <Or>
+      <Bug pattern="EI_EXPOSE_REP" />
+      <Bug pattern="EI_EXPOSE_REP2" />
+    </Or>
+  </Match>
 </FindBugsFilter>
diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java
index af4875bc0bc27a9f08d1e9e5cc2cc88922d6a39c..12ea5c735297efbaa696e803fbe614ebc5a0956c 100644
--- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java
+++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java
@@ -10,6 +10,7 @@ import java.util.Set;
 import java.util.Stack;
 import java.util.regex.Pattern;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import nl.tudelft.ewi.auta.common.annotation.Unmodifiable;
 import org.antlr.v4.runtime.CommonTokenStream;
 import org.antlr.v4.runtime.misc.Interval;
@@ -593,6 +594,7 @@ public class AttAsmReader {
          *         is a target for an unconditional jump or a return instruction
          */
         @Contract(pure = true)
+        @SuppressFBWarnings(value = {"ES_COMPARING_STRINGS_WITH_EQ"}, justification = "see comment")
         public boolean isConditional() {
             // noinspection StringEquality - identity is ensured by constructor
             return this.falsyTarget != this.truthyTarget;
diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java
index c916c1530dfa1c5d58b2a4c3f38132f6d73797a4..f187578593dffb0c7e94344e8388267e74043c44 100644
--- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java
+++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java
@@ -12,16 +12,12 @@ import nl.tudelft.ewi.auta.common.model.metric.MetricName;
 import nl.tudelft.ewi.auta.common.model.metric.PyLintResultMetric;
 import nl.tudelft.ewi.auta.worker.Job;
 import nl.tudelft.ewi.auta.worker.config.WorkerSettings;
-import nl.tudelft.ewi.auta.worker.tool.python.Python;
 import nl.tudelft.ewi.auta.worker.tool.python.PythonProcessException;
-import nl.tudelft.ewi.auta.worker.files.Unpacker;
-import org.apache.commons.compress.utils.IOUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -34,50 +30,22 @@ public class PyLint extends JobAnalyzer {
 
     private static final Logger logger = LoggerFactory.getLogger(PyLint.class);
 
-    /**
-     * The path to the radon files.
-     */
-    private final Path pyLintPath;
-
     /**
      * The worker settings.
      */
     private final WorkerSettings settings;
 
-    /**
-     * The Python instance PyLint will run in.
-     */
-    private final Python python;
-
     /**
      * Constructor for the Radon analyzer.
      *
      * @param workerSettings the worker settings
-     * @param unzipper the job unzipper
-     * @param python the python instance to run PyLint in
      *
      * @throws IOException if a file is not found
      */
     public PyLint(
-            final WorkerSettings workerSettings,
-            final Unpacker unzipper,
-            final Python python
-    ) throws IOException {
+            final WorkerSettings workerSettings
+    ) {
         this.settings = workerSettings;
-        this.python = python;
-        final var path = Files.createTempDirectory(this.settings.getTemp(), "pylint");
-        final var zipPath = Files.createTempFile(this.settings.getTemp(), "stream", ".zip");
-
-        try (var in = PyLint.class.getResourceAsStream(
-                "/nl/tudelft/ewi/auta/worker/checker/python/pylint.zip");
-             var out = Files.newOutputStream(zipPath)) {
-            IOUtils.copy(in, out);
-        }
-
-        // Unzip PyLint for usage
-        unzipper.unpack(zipPath, path);
-        this.pyLintPath = path;
-        Files.delete(zipPath);
     }
 
 
@@ -100,11 +68,11 @@ public class PyLint extends JobAnalyzer {
         final var fileNames = this.getFileNames(victim);
 
         // For every module
-        for (var i = 0; i < fileNames.size(); i++) {
+        for (final var fileName : fileNames) {
             // Prepare the command to run PyLint as tool in a separate process
-            final var process = this.python.prepare(
-                    this.pyLintPath, "pylint", "--output-format=json", fileNames.get(i)
-            );
+            final var process =
+                    new ProcessBuilder("pylint", "--output-format=json", fileName);
+            process.redirectError(ProcessBuilder.Redirect.INHERIT);
 
             // Set the right environment variables and the working directory
             final var tempOutputFile = Files.createTempFile(
@@ -121,7 +89,7 @@ public class PyLint extends JobAnalyzer {
                 logger.warn("The Python process for PyLint timed out during execution");
                 throw new PythonProcessException(
                         "The Python process for PyLint timed out during execution, "
-                         + "the module that was provided might have been to large"
+                                + "the module that was provided might have been to large"
                 );
             }
 
@@ -139,12 +107,12 @@ public class PyLint extends JobAnalyzer {
                 }
             }
 
-            var moduleName = fileNames.get(i).replace("\\", "/");
+            var moduleName = fileName.replace("\\", "/");
             moduleName = moduleName.substring(moduleName.lastIndexOf("/") + 1);
 
             // Create the module entity
             final var moduleEntity = new Entity(victim.getProject(), moduleName,
-                                                false, EntityLevel.MODULE);
+                    false, EntityLevel.MODULE);
             final var moduleEntityWithMetrics = this.createModuleEntityStructure(
                     moduleEntity, pyLintResults
             );
diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/worker/tool/python/Python.java b/worker/src/main/java/nl/tudelft/ewi/auta/worker/tool/python/Python.java
index 7a7da1be62ab656dd248d3ec5e6eb3472280e6cd..e233c369d62d7d604b2b76081345fa25d360154a 100644
--- a/worker/src/main/java/nl/tudelft/ewi/auta/worker/tool/python/Python.java
+++ b/worker/src/main/java/nl/tudelft/ewi/auta/worker/tool/python/Python.java
@@ -4,6 +4,7 @@ import org.jetbrains.annotations.Contract;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -33,27 +34,35 @@ public class Python {
     /**
      * Prepares a python process for executing.
      *
-     * @param toolDir the directory the tool is installed in
-     * @param workingDir the working directory for the tool
+     * @param toolDir the directory the tool is installed in or null to use the global site packages
+     * @param workingDir the working directory for the tool or null to inherit the current WD
      * @param cmd any command line arguments (excluding the executable)
      *
      * @return a process builder ready to execute the command
      */
-    public ProcessBuilder prepare(final Path toolDir, final Path workingDir, final String... cmd) {
+    public ProcessBuilder prepare(
+            final @Nullable Path toolDir,
+            final @Nullable Path workingDir,
+            final String... cmd
+    ) {
         final var fcmd = new ArrayList<String>();
         fcmd.add(this.info.getPath().toAbsolutePath().toString());
         fcmd.addAll(Arrays.asList(cmd));
 
         final var process = new ProcessBuilder(fcmd);
-        process.environment().put("PYTHONPATH", toolDir.toAbsolutePath().toString());
-        process.directory(workingDir.toFile());
+        if (toolDir != null) {
+            process.environment().put("PYTHONPATH", toolDir.toAbsolutePath().toString());
+        }
+        if (workingDir != null) {
+            process.directory(workingDir.toFile());
+        }
         process.redirectError(ProcessBuilder.Redirect.INHERIT);
 
         if (logger.isTraceEnabled()) {
             logger.trace("Starting `{}` as tool in {} working in or on {}",
                     String.join(" ", fcmd),
-                    toolDir.toAbsolutePath(),
-                    workingDir.toAbsolutePath()
+                    toolDir,
+                    workingDir
             );
         }
 
@@ -65,20 +74,20 @@ public class Python {
      *
      * The working directory is set to the tool directory.
      *
-     * @param toolDir the directory the tool is installed in
+     * @param toolDir the directory the tool is installed in or null to use the global site-packages
      * @param cmd any command line arguments (excluding the executable)
      *
      * @return a process builder ready to execute the command
      */
-    public ProcessBuilder prepare(final Path toolDir, final String... cmd) {
+    public ProcessBuilder prepare(final @Nullable Path toolDir, final String... cmd) {
         return this.prepare(toolDir, toolDir, cmd);
     }
 
     /**
      * Executes a python process.
      *
-     * @param toolDir the directory the tool is installed in
-     * @param workingDir the working directory for the tool
+     * @param toolDir the directory the tool is installed in or null to use the global site-packages
+     * @param workingDir the working directory for the tool or null to inherit the current WD
      * @param cmd any command line arguments (excluding the executable)
      *
      * @return the running executable
@@ -86,7 +95,7 @@ public class Python {
      * @throws IOException if an I/O error occurs while communicating with the process
      */
     public Process run(
-            final Path toolDir, final Path workingDir, final String... cmd
+            final @Nullable Path toolDir, final @Nullable Path workingDir, final String... cmd
     ) throws IOException {
         return this.prepare(toolDir, workingDir, cmd).start();
     }
@@ -96,14 +105,14 @@ public class Python {
      *
      * The working directory is set to the tool directory.
      *
-     * @param toolDir the directory the tool is installed in
+     * @param toolDir the directory the tool is installed in or null to use the global site-packages
      * @param cmd any command line arguments (excluding the executable)
      *
      * @return the running executable
      *
      * @throws IOException if an I/O error occurs while communicating with the process
      */
-    public Process run(final Path toolDir, final String... cmd) throws IOException {
+    public Process run(final @Nullable Path toolDir, final String... cmd) throws IOException {
         return this.run(toolDir, toolDir, cmd);
     }
 }
diff --git a/worker/src/main/resources/nl/tudelft/ewi/auta/worker/checker/python/pylint.zip b/worker/src/main/resources/nl/tudelft/ewi/auta/worker/checker/python/pylint.zip
index 99394b9679362512aec2fd5de436a027b6bfdf52..8a6a8cbdf9f88d5d536ca841735e60da4228e71e 100644
Binary files a/worker/src/main/resources/nl/tudelft/ewi/auta/worker/checker/python/pylint.zip and b/worker/src/main/resources/nl/tudelft/ewi/auta/worker/checker/python/pylint.zip differ
diff --git a/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java b/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java
index 561bccbe6cb759fe5153ccd92a88f7ece2f663cd..18571fcdb84300b113009c3a38a5282aa3ee7a33 100644
--- a/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java
+++ b/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java
@@ -10,8 +10,6 @@ import nl.tudelft.ewi.auta.worker.config.WorkerSettings;
 import nl.tudelft.ewi.auta.worker.files.UnicodePathEncoder;
 import nl.tudelft.ewi.auta.worker.files.Unpacker;
 import nl.tudelft.ewi.auta.worker.jobconfig.JobConfig;
-import nl.tudelft.ewi.auta.worker.tool.python.Python;
-import nl.tudelft.ewi.auta.worker.tool.python.PythonDetector;
 import org.apache.commons.compress.utils.IOUtils;
 import org.junit.jupiter.api.Test;
 
@@ -36,8 +34,6 @@ public class PyLintTest {
         final var tempDir = Paths.get(System.getProperty("java.io.tmpdir"));
         final var zipPath = Files.createTempFile(tempDir, "pyFilesForTesting", ".zip");
 
-        final var python = new Python(new PythonDetector().findPython());
-
         try (var in = PyLintTest.class
                 .getResourceAsStream("/lizard.zip");
              var out = Files.newOutputStream(zipPath)) {
@@ -64,8 +60,7 @@ public class PyLintTest {
                     "name", Collections.singletonList("Otto"),
                     "api-token", List.of("TOKEN!!!")));
 
-            final var pyLint = new PyLint(settings,
-                    new Unpacker(settings, new UnicodePathEncoder()), python);
+            final var pyLint = new PyLint(settings);
             pyLint.analyze(job, new HashMap<>());
             assertThat(job.getProject().getAllChildren()).hasSizeGreaterThan(100);
         }
@@ -101,9 +96,7 @@ public class PyLintTest {
                 "name", Collections.singletonList("Otto"),
                 "api-token", List.of("TOKEN!!!")));
 
-        final var python = new Python(new PythonDetector().findPython());
-        final var pyLint = new PyLint(settings, new Unpacker(settings, new UnicodePathEncoder()),
-                python);
+        final var pyLint = new PyLint(settings);
 
         pyLint.analyze(job, Collections.emptyMap());
         assertThat(job.getProject().getLevel()).isEqualTo(EntityLevel.PROJECT);