diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -8,12 +8,12 @@
 src="https://hadolint.github.io/hadolint/img/cat_container.png" />
 
 A smarter Dockerfile linter that helps you build [best practice][] Docker
-images. The linter is parsing the Dockerfile into an AST and performs rules on
-top of the AST. It is standing on the shoulders of [ShellCheck][] to lint
+images. The linter parses the Dockerfile into an AST and performs rules on
+top of the AST. It stands on the shoulders of [ShellCheck][] to lint
 the Bash code inside `RUN` instructions.
 
 [:globe_with_meridians: **Check the online version on
- hadolint.github.io/hadolint**](https://hadolint.github.io/hadolint)
+hadolint.github.io/hadolint**](https://hadolint.github.io/hadolint)
 [![Screenshot](screenshot.png)](https://hadolint.github.io/hadolint)
 
 ## How to use
@@ -26,55 +26,67 @@
 hadolint --trusted-registry my-company.com:500 <Dockerfile> # Warn when using untrusted FROM images
 ```
 
-Docker comes to the rescue to provide an easy way how to run `hadolint` on most
+Docker comes to the rescue, providing an easy way how to run `hadolint` on most
 platforms.
 Just pipe your `Dockerfile` to `docker run`:
 
 ```bash
-$ docker run --rm -i hadolint/hadolint < Dockerfile
+docker run --rm -i hadolint/hadolint < Dockerfile
 # OR
-$ docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile
+docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile
 ```
 
-or if you are using Windows PowerShell:
+or using [Podman](https://podman.io/):
 
+```bash
+podman run --rm -i docker.io/hadolint/hadolint < Dockerfile
+# OR
+podman run --rm -i ghcr.io/hadolint/hadolint < Dockerfile
+```
+
+or using Windows PowerShell:
+
 ```powershell
-> cat .\Dockerfile | docker run --rm -i hadolint/hadolint
+cat .\Dockerfile | docker run --rm -i hadolint/hadolint
 ```
 
 ## Install
 
 You can download prebuilt binaries for OSX, Windows and Linux from the latest
-[release page][]. However, if it doesn't work for you, please fall back to
-Docker, `brew` or source installation.
+[release page][]. However, if this does not work for you, please fall back to
+container (Docker), `brew` or source installation.
 
-If you are on OSX you can use [brew](https://brew.sh/) to install `hadolint`.
+On OSX, you can use [brew](https://brew.sh/) to install `hadolint`.
 
 ```bash
 brew install hadolint
 ```
 
-On Windows you can use [scoop](https://github.com/lukesampson/scoop) to
+On Windows, you can use [scoop](https://github.com/lukesampson/scoop) to
 install `hadolint`.
 
 ```batch
 scoop install hadolint
 ```
 
-As shown before, `hadolint` is available as a Docker container:
+On distributions that have `nix` installed, you can use the `hadolint`
+package to run ad-hoc shells or permanently install `hadolint` into
+your environment.
 
+As mentioned earlier, `hadolint` is available as a container image:
+
 ```bash
 docker pull hadolint/hadolint
 # OR
 docker pull ghcr.io/hadolint/hadolint
 ```
 
-If you need a Docker container with shell access, use the Debian or Alpine
-variants of the Docker image:
+If you need a container with shell access, use the Debian or Alpine
+variants:
 
 ```bash
 docker pull hadolint/hadolint:latest-debian
-# OR 
+# OR
 docker pull hadolint/hadolint:latest-alpine
 # OR
 docker pull ghcr.io/hadolint/hadolint:latest-debian
@@ -91,6 +103,18 @@
 && stack install
 ```
 
+If you want the
+[VS Code Hadolint](https://github.com/michaellzc/vscode-hadolint)
+extension to use Hadolint in a container, you can use the following
+[wrapper script](https://github.com/hadolint/hadolint/issues/691#issuecomment-932116329):
+
+```bash
+#!/bin/bash
+dockerfile="$1"
+shift
+docker run --rm -i hadolint/hadolint hadolint "$@" - < "$dockerfile"
+```
+
 ## CLI
 
 ```bash
@@ -100,13 +124,13 @@
 ```text
 hadolint - Dockerfile Linter written in Haskell
 
-Usage: hadolint [-v|--version] [--no-fail] [--no-color] [-c|--config FILENAME] 
-                [-V|--verbose] [-f|--format ARG] [DOCKERFILE...] 
-                [--error RULECODE] [--warning RULECODE] [--info RULECODE] 
-                [--style RULECODE] [--ignore RULECODE] 
-                [--trusted-registry REGISTRY (e.g. docker.io)] 
-                [--require-label LABELSCHEMA (e.g. maintainer:text)] 
-                [--strict-labels] [-t|--failure-theshold THRESHOLD]
+Usage: hadolint [-v|--version] [--no-fail] [--no-color] [-c|--config FILENAME]
+                [-V|--verbose] [-f|--format ARG] [DOCKERFILE...]
+                [--error RULECODE] [--warning RULECODE] [--info RULECODE]
+                [--style RULECODE] [--ignore RULECODE]
+                [--trusted-registry REGISTRY (e.g. docker.io)]
+                [--require-label LABELSCHEMA (e.g. maintainer:text)]
+                [--strict-labels] [-t|--failure-threshold THRESHOLD]
   Lint Dockerfile for errors and best practices
 
 Available options:
@@ -136,7 +160,7 @@
                            format requirement `format`
   --strict-labels          Do not permit labels other than specified in
                            `label-schema`
-  -t,--failure-theshold THRESHOLD
+  -t,--failure-threshold THRESHOLD
                            Exit with failure code only when rules with a
                            severity above THRESHOLD are violated. Accepted
                            values: [error | warning | info | style | ignore |
@@ -145,14 +169,22 @@
 
 ## Configure
 
-Configuration files can be used globally or per project. By default,
-`hadolint` will look for a configuration file in the current directory
-with the name `.hadolint.yaml` or `.hadolint.yml`
+Configuration files can be used globally or per project.
+Hadolint looks for configuration files in the following locations or their
+platform specific equivalents in this order and uses the first one exclusively:
+- `$PWD/.hadolint.yaml`
+- `$XDG_CONFIG_HOME/hadolint.yaml`
+- `$HOME/.config/hadolint.yaml`
+- `$HOME/.hadolint/hadolint.yaml or $HOME/hadolint/config.yaml`
+- `$HOME/.hadolint.yaml`
 
+In windows, the `%LOCALAPPDATA%` environment variable is used instead of 
+`XDG_CONFIG_HOME`. Config files can have either `yaml` or `yml` extensions.
+
 `hadolint` full `yaml` config file schema
 
 ```yaml
-failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none)                
+failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none)
 format: string                          # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy)
 ignored: [string]                       # list of rules
 label-schema:                           # See Linting Labels below for specific label-schema details
@@ -186,7 +218,7 @@
 
 Additionally, `hadolint` can warn you when images from untrusted
 repositories are being used in Dockerfiles, you can append the
-`trustedRegistries` keys to the configuration file as shown below:
+`trustedRegistries` keys to the configuration file, as shown below:
 
 ```yaml
 ignored:
@@ -226,18 +258,6 @@
     - DL3032
 ```
 
-The global configuration file should be placed in the folder
-specified by `XDG_CONFIG_HOME`,
-with the name `hadolint.yaml` or `hadolint.yml`. In summary, the
-following locations are valid for the configuration file, in order
-or preference:
-
-- `$PWD/.hadolint.yaml`
-- `$XDG_CONFIG_HOME/hadolint.yaml`
-- `~/.config/hadolint.yaml`
-
-In windows, the `%LOCALAPPDATA%` environment variable is used instead of `XDG_CONFIG_HOME`
-
 Additionally, you can pass a custom configuration file in the command line with
 the `--config` option
 
@@ -245,7 +265,7 @@
 hadolint --config /path/to/config.yaml Dockerfile
 ```
 
-To pass a custom configuration file (using relative or absolute path) to 
+To pass a custom configuration file (using relative or absolute path) to
 a container, use the following command:
 
 ```bash
@@ -254,11 +274,40 @@
 docker run --rm -i -v /your/path/to/hadolint.yaml:/.config/hadolint.yaml ghcr.io/hadolint/hadolint < Dockerfile
 ```
 
+In addition to config files, Hadolint can be configured with environment
+variables.
+```bash
+NO_COLOR=1                               # Truthy value e.g. 1, true or yes
+HADOLINT_NOFAIL=1                        # Truthy value e.g. 1, true or yes
+HADOLINT_VERBOSE=1                       # Truthy value e.g. 1, true or yes
+HADOLINT_FORMAT=json                     # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy | sarif )
+HADOLINT_FAILURE_THRESHOLD=info          # threshold level (error | warning | info | style | ignore | none)
+HADOLINT_OVERRIDE_ERROR=DL3010,DL3020    # comma separated list of rule codes
+HADOLINT_OVERRIDE_WARNING=DL3010,DL3020  # comma separated list of rule codes
+HADOLINT_OVERRIDE_INFO=DL3010,DL3020     # comma separated list of rule codes
+HADOLINT_OVERRIDE_STYLE=DL3010,DL3020    # comma separated list of rule codes
+HADOLINT_IGNORE=DL3010,DL3020            # comma separated list of rule codes
+HADOLINT_STRICT_LABELS=1                 # Truthy value e.g. 1, true or yes
+HADOLINT_TRUSTED_REGISTRIES              # comma separated list of registry urls
+```
+
+## Non-Posix Shells
+
+When using base images with non-posix shells as default (e.g. Windows based
+images) a special pragma `hadolint shell` can specify which shell the base image
+uses, so that Hadolint can automatically ignore all shell-specific rules.
+
+```Dockerfile
+FROM mcr.microsoft.com/windows/servercore:ltsc2022
+# hadolint shell=powershell
+RUN Get-Process notepad | Stop-Process
+```
+
 ## Inline ignores
 
-It is also possible to ignore rules by using a special comment directly
-above the Dockerfile instruction you want to make an exception for.
-Ignore rule comments look like
+It is also possible to ignore rules by adding a special comment directly
+above the Dockerfile statement for which you want to make an exception for.
+Such comments look like
 `# hadolint ignore=DL3001,SC1081`. For example:
 
 ```dockerfile
@@ -269,19 +318,19 @@
 RUN cd /tmp && echo "hello!"
 ```
 
-Inline ignores will only work if place directly above the instruction.
+The comment "inline ignores" applies only to the statement following it.
 
 ## Linting Labels
 
-Hadolint has the ability to check that specific labels be present and conform
+Hadolint is able to check if specific labels are present and conform
 to a predefined label schema.
-First a label schema must be defined either via commandline:
+First, a label schema must be defined either via the command line:
 
 ```bash
 hadolint --require-label author:text --require-label version:semver Dockerfile
 ```
 
-or via config file:
+or via the config file:
 
 ```yaml
 label-schema:
@@ -306,20 +355,20 @@
 | spdx    | An [SPDX license identifier][spdxid]               |
 | email   | An email address conforming to [RFC 5322][rfc5322] |
 
-By default, Hadolint ignores any label not specified in the label schema. To
-warn on such additional labels, turn on strict labels:
+By default, Hadolint ignores any label that is not specified in the label schema. To
+warn against such additional labels, turn on strict labels, using the command line:
 
 ```bash
 hadolint --strict-labels --require-label version:semver Dockerfile
 ```
 
-or in the config file:
+or the config file:
 
 ```yaml
 strict-labels: true
 ```
 
-When strict labels is enabled, but no label schema has been specified, Hadolint
+When strict labels is enabled, but no label schema is specified, `hadolint`
 will warn if any label is present.
 
 ### Note on dealing with variables in labels
@@ -342,8 +391,8 @@
 
 ## Integrations
 
-To get most of `hadolint` it is useful to integrate it as a check to your CI
-or to your editor, or as a pre-commit hook, to lint your `Dockerfile` as you
+To get most of `hadolint`, it is useful to integrate it as a check in your CI
+or into your editor, or as a pre-commit hook, to lint your `Dockerfile` as you
 write it. See our [Integration][] docs.
 
 - [Code Review Platform Integrations][]
@@ -356,16 +405,18 @@
 An incomplete list of implemented rules. Click on the error code to get more
 detailed information.
 
--   Rules with the prefix `DL` originate from `hadolint`. Take a look at
-`Rules.hs` to find the implementation of the rules.
+-   Rules with the prefix `DL` are from `hadolint`. Have a look at
+    `Rules.hs` to find the implementation of the rules.
 
--   Rules with the `SC` prefix originate from **ShellCheck** (Only the most
-common rules are listed, there are dozens more)
+-   Rules with the `SC` prefix are from **ShellCheck** (only the most
+    common rules are listed, there are dozens more).
 
 Please [create an issue][] if you have an idea for a good rule.
+
 <!--lint disable maximum-line-length-->
+
 | Rule                                                         | Default Severity | Description                                                                                                                                         |
-|:-------------------------------------------------------------|:-----------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|
+| :----------------------------------------------------------- | :--------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
 | [DL3000](https://github.com/hadolint/hadolint/wiki/DL3000)   | Error            | Use absolute WORKDIR.                                                                                                                               |
 | [DL3001](https://github.com/hadolint/hadolint/wiki/DL3001)   | Info             | For some bash commands it makes no sense running them in a Docker container like ssh, vim, shutdown, service, ps, free, top, kill, mount, ifconfig. |
 | [DL3002](https://github.com/hadolint/hadolint/wiki/DL3002)   | Warning          | Last user should not be root.                                                                                                                       |
@@ -399,7 +450,7 @@
 | [DL3032](https://github.com/hadolint/hadolint/wiki/DL3032)   | Warning          | `yum clean all` missing after yum command.                                                                                                          |
 | [DL3033](https://github.com/hadolint/hadolint/wiki/DL3033)   | Warning          | Specify version with `yum install -y <package>-<version>`                                                                                           |
 | [DL3034](https://github.com/hadolint/hadolint/wiki/DL3034)   | Warning          | Non-interactive switch missing from `zypper` command: `zypper install -y`                                                                           |
-| [DL3035](https://github.com/hadolint/hadolint/wiki/DL3035)   | Warning          | Do not use `zypper dist-upgrade`.                                                                                                                  	|
+| [DL3035](https://github.com/hadolint/hadolint/wiki/DL3035)   | Warning          | Do not use `zypper dist-upgrade`.                                                                                                                   |
 | [DL3036](https://github.com/hadolint/hadolint/wiki/DL3036)   | Warning          | `zypper clean` missing after zypper use.                                                                                                            |
 | [DL3037](https://github.com/hadolint/hadolint/wiki/DL3037)   | Warning          | Specify version with `zypper install -y <package>[=]<version>`.                                                                                     |
 | [DL3038](https://github.com/hadolint/hadolint/wiki/DL3038)   | Warning          | Use the `-y` switch to avoid manual input `dnf install -y <package>`                                                                                |
@@ -431,7 +482,7 @@
 | [DL4005](https://github.com/hadolint/hadolint/wiki/DL4005)   | Warning          | Use `SHELL` to change the default shell.                                                                                                            |
 | [DL4006](https://github.com/hadolint/hadolint/wiki/DL4006)   | Warning          | Set the `SHELL` option -o pipefail before `RUN` with a pipe in it                                                                                   |
 | [SC1000](https://github.com/koalaman/shellcheck/wiki/SC1000) |                  | `$` is not used specially and should therefore be escaped.                                                                                          |
-| [SC1001](https://github.com/koalaman/shellcheck/wiki/SC1001) |                  | This `\c` will be a regular `'c'`  in this context.                                                                                                 |
+| [SC1001](https://github.com/koalaman/shellcheck/wiki/SC1001) |                  | This `\c` will be a regular `'c'` in this context.                                                                                                  |
 | [SC1007](https://github.com/koalaman/shellcheck/wiki/SC1007) |                  | Remove space after `=` if trying to assign a value (or for empty string, use `var='' ...`).                                                         |
 | [SC1010](https://github.com/koalaman/shellcheck/wiki/SC1010) |                  | Use semicolon or linefeed before `done` (or quote to make it literal).                                                                              |
 | [SC1018](https://github.com/koalaman/shellcheck/wiki/SC1018) |                  | This is a unicode non-breaking space. Delete it and retype as space.                                                                                |
@@ -463,11 +514,12 @@
 | [SC2154](https://github.com/koalaman/shellcheck/wiki/SC2154) |                  | var is referenced but not assigned.                                                                                                                 |
 | [SC2155](https://github.com/koalaman/shellcheck/wiki/SC2155) |                  | Declare and assign separately to avoid masking return values.                                                                                       |
 | [SC2164](https://github.com/koalaman/shellcheck/wiki/SC2164) |                  | Use <code>cd ... &#124;&#124; exit</code> in case `cd` fails.                                                                                       |
+
 <!--lint enable maximum-line-length-->
 
 ## Develop
 
-If you are an experienced Haskeller we would be really thankful if you would
+If you are an experienced Haskeller, we would be very grateful if you would
 tear our code apart in a review.
 
 ### Setup
@@ -501,13 +553,13 @@
 
 ### Tests
 
-Run unit tests.
+Run unit tests:
 
 ```bash
 stack test
 ```
 
-Run integration tests.
+Run integration tests:
 
 ```bash
 ./integration_test.sh
@@ -521,10 +573,15 @@
 
 ## Alternatives
 
-- RedCoolBeans/[dockerlint](https://github.com/RedCoolBeans/dockerlint/)
-- projectatomic/[dockerfile_lint](https://github.com/projectatomic/dockerfile_lint/)
+-   replicatedhq/[dockerfilelint](https://github.com/replicatedhq/dockerfilelint),
+    the other linter used by the [super-linter](https://github.com/github/super-linter/blob/main/README.md#supported-linters)
 
+-   RedCoolBeans/[dockerlint](https://github.com/RedCoolBeans/dockerlint/)
+
+-   projectatomic/[dockerfile_lint](https://github.com/projectatomic/dockerfile_lint/)
+
 <!-- References -->
+
 [github-actions-img]: https://github.com/hadolint/hadolint/workflows/Haskell%20Tests/badge.svg?branch=master
 [github-actions]: https://travis-ci.org/hadolint/hadolint/actions
 [license-img]: https://img.shields.io/badge/license-GPL--3-blue.svg
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,262 +1,28 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TemplateHaskell #-}
-
 module Main where
 
-import Control.Applicative
 import Control.Monad (when)
-import qualified Data.Bifunctor as Bifunctor
+import Data.Default
+import Hadolint (OutputFormat (..), printResults, DLSeverity (..))
+import Hadolint.Config
+import Prettyprinter
 import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map as Map
-import Data.Maybe
 import qualified Data.Sequence as Seq
-import qualified Data.Set as Set
-import Data.String (IsString (fromString))
-import qualified Data.Text as Text
-import qualified Data.Version
-import qualified Development.GitRev
 import qualified Hadolint
-import qualified Hadolint.Formatter.Format as Format
 import qualified Hadolint.Rule as Rule
 import Options.Applicative
-  ( Parser,
-    ReadM,
-    action,
-    argument,
-    completeWith,
-    eitherReader,
-    execParser,
+  ( execParser,
     fullDesc,
     header,
-    help,
     helper,
     info,
-    long,
-    maybeReader,
-    metavar,
-    option,
-    progDesc,
-    short,
-    showDefaultWith,
-    str,
-    strOption,
-    switch,
-    value,
+    progDesc
   )
 -- version from hadolint.cabal file
-import qualified Paths_hadolint as Meta
-import System.Environment
 import System.Exit (exitFailure, exitSuccess)
-import System.IO (hPutStrLn, stderr)
+import System.IO (hPrint, stderr)
 
-data CommandOptions = CommandOptions
-  { showVersion :: Bool,
-    noFail :: Bool,
-    nocolor :: Bool,
-    configFile :: Maybe FilePath,
-    isVerbose :: Bool,
-    format :: Hadolint.OutputFormat,
-    dockerfiles :: [String],
-    lintingOptions :: Hadolint.LintOptions,
-    filePathInReportOption :: Maybe FilePath
-  }
 
-toOutputFormat :: String -> Maybe Hadolint.OutputFormat
-toOutputFormat "json" = Just Hadolint.Json
-toOutputFormat "sonarqube" = Just Hadolint.SonarQube
-toOutputFormat "tty" = Just Hadolint.TTY
-toOutputFormat "codeclimate" = Just Hadolint.CodeclimateJson
-toOutputFormat "gitlab_codeclimate" = Just Hadolint.GitlabCodeclimateJson
-toOutputFormat "checkstyle" = Just Hadolint.Checkstyle
-toOutputFormat "codacy" = Just Hadolint.Codacy
-toOutputFormat _ = Nothing
-
-showFormat :: Hadolint.OutputFormat -> String
-showFormat Hadolint.Json = "json"
-showFormat Hadolint.SonarQube = "sonarqube"
-showFormat Hadolint.TTY = "tty"
-showFormat Hadolint.CodeclimateJson = "codeclimate"
-showFormat Hadolint.GitlabCodeclimateJson = "gitlab_codeclimate"
-showFormat Hadolint.Checkstyle = "checkstyle"
-showFormat Hadolint.Codacy = "codacy"
-
-toNofailSeverity :: String -> Maybe Rule.DLSeverity
-toNofailSeverity "error" = Just Rule.DLErrorC
-toNofailSeverity "warning" = Just Rule.DLWarningC
-toNofailSeverity "info" = Just Rule.DLInfoC
-toNofailSeverity "style" = Just Rule.DLStyleC
-toNofailSeverity "ignore" = Just Rule.DLIgnoreC
-toNofailSeverity "none" = Just Rule.DLIgnoreC
-toNofailSeverity _ = Nothing
-
-parseOptions :: Parser CommandOptions
-parseOptions =
-  CommandOptions
-    <$> version -- CLI options parser definition
-    <*> noFail
-    <*> nocolor
-    <*> configFile
-    <*> isVerbose
-    <*> outputFormat
-    <*> files
-    <*> lintOptions
-    <*> filePathInReportOption
-  where
-    version = switch (long "version" <> short 'v' <> help "Show version")
-
-    noFail = switch (long "no-fail" <> help "Don't exit with a failure status code when any rule is violated")
-
-    noFailCutoff =
-      option
-        (maybeReader toNofailSeverity)
-        ( short 't'
-            <> long "failure-threshold"
-            <> help
-              "Exit with failure code only when rules with a severity \
-              \equal to or above THRESHOLD are violated. Accepted values: \
-              \[error | warning | info | style | ignore | none]"
-            <> value Rule.DLInfoC
-            <> metavar "THRESHOLD"
-            <> showDefaultWith (Text.unpack . Format.severityText)
-            <> completeWith ["error", "warning", "info", "style", "ignore", "none"]
-        )
-
-    nocolor = switch (long "no-color" <> help "Don't colorize output")
-
-    strictlabels =
-      switch
-        ( long "strict-labels"
-            <> help "Do not permit labels other than specified in `label-schema`"
-        )
-
-    configFile =
-      optional
-        ( strOption
-            ( long "config" <> short 'c' <> metavar "FILENAME"
-                <> help "Path to the configuration file"
-            )
-        )
-
-    isVerbose = switch (long "verbose" <> short 'V' <> help "Enables verbose logging of hadolint's output to stderr")
-
-    outputFormat =
-      option
-        (maybeReader toOutputFormat)
-        ( long "format"
-            <> short 'f' -- options for the output format
-            <> help
-              "The output format for the results [tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy | sonarqube]"
-            <> value Hadolint.TTY
-            <> showDefaultWith showFormat -- The default value
-            <> completeWith ["tty", "json", "checkstyle", "codeclimate", "gitlab_codeclimate", "codacy", "sonarqube"]
-        )
-
-    errorList =
-      many
-        ( strOption
-            ( long "error"
-                <> help "Make the rule `RULECODE` have the level `error`"
-                <> metavar "RULECODE"
-            )
-        )
-
-    warningList =
-      many
-        ( strOption
-            ( long "warning"
-                <> help "Make the rule `RULECODE` have the level `warning`"
-                <> metavar "RULECODE"
-            )
-        )
-
-    infoList =
-      many
-        ( strOption
-            ( long "info"
-                <> help "Make the rule `RULECODE` have the level `info`"
-                <> metavar "RULECODE"
-            )
-        )
-
-    styleList =
-      many
-        ( strOption
-            ( long "style"
-                <> help "Make the rule `RULECODE` have the level `style`"
-                <> metavar "RULECODE"
-            )
-        )
-
-    ignoreList =
-      many
-        ( strOption
-            ( long "ignore"
-                <> help "A rule to ignore. If present, the ignore list in the config file is ignored"
-                <> metavar "RULECODE"
-            )
-        )
-
-    files = many (argument str (metavar "DOCKERFILE..." <> action "file"))
-
-    lintOptions =
-      Hadolint.LintOptions
-        <$> errorList
-        <*> warningList
-        <*> infoList
-        <*> styleList
-        <*> ignoreList
-        <*> parseRulesConfig
-        <*> noFailCutoff
-
-    filePathInReportOption =
-      optional
-        ( strOption
-            ( long "file-path-in-report" <> metavar "FILEPATHINREPORT"
-                <> help "The file path referenced in the generated report. This only applies for the 'checkstyle' format and is useful when running Hadolint with Docker to set the correct file path."
-            )
-        )
-
-    labels =
-      Map.fromList
-        <$> many
-          ( option
-              readSingleLabelSchema
-              ( long "require-label"
-                  <> help "The option --require-label=label:format makes Hadolint check that the label `label` conforms to format requirement `format`"
-                  <> metavar "LABELSCHEMA (e.g. maintainer:text)"
-              )
-          )
-
-    parseRulesConfig =
-      Hadolint.RulesConfig
-        <$> parseAllowedRegistries
-        <*> labels
-        <*> strictlabels
-
-    parseAllowedRegistries =
-      Set.fromList . fmap fromString
-        <$> many
-          ( strOption
-              ( long "trusted-registry"
-                  <> help "A docker registry to allow to appear in FROM instructions"
-                  <> metavar "REGISTRY (e.g. docker.io)"
-              )
-          )
-
-type SingleLabelSchema = (Rule.LabelName, Rule.LabelType)
-
-readSingleLabelSchema :: ReadM SingleLabelSchema
-readSingleLabelSchema = eitherReader $ \s -> labelParser (Text.pack s)
-
-labelParser :: Text.Text -> Either String (Rule.LabelName, Rule.LabelType)
-labelParser l =
-  case Bifunctor.second (Rule.read . Text.drop 1) $ Text.breakOn ":" l of
-    (ln, Right lt) -> Right (ln, lt)
-    (_, Left e) -> Left $ Text.unpack e
-
-noFailure :: Hadolint.Result s e -> Rule.DLSeverity -> Bool
+noFailure :: Hadolint.Result s e -> DLSeverity -> Bool
 noFailure (Hadolint.Result _ Seq.Empty Seq.Empty) _ = True
 noFailure (Hadolint.Result _ Seq.Empty fails) cutoff =
   Seq.null (Seq.filter (\f -> Rule.severity f <= cutoff) fails)
@@ -264,56 +30,64 @@
 
 exitProgram ::
   Foldable f =>
-  CommandOptions ->
-  Hadolint.LintOptions ->
+  Configuration ->
   f (Hadolint.Result s e) ->
   IO ()
-exitProgram cmd conf res
-  | noFail cmd = exitSuccess
-  | Hadolint.shallSkipErrorStatus (format cmd) = exitSuccess
-  | all (`noFailure` Hadolint.failThreshold conf) res = exitSuccess
+exitProgram conf res
+  | noFail conf = exitSuccess
+  | CodeclimateJson == format conf = exitSuccess
+  | Codacy == format conf = exitSuccess
+  | all (`noFailure` failureThreshold conf) res = exitSuccess
   | otherwise = exitFailure
 
-runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO ()
-runLint cmd conf files = do
+runLint ::
+  CommandlineConfig ->
+  Configuration ->
+  IO ()
+runLint cmd conf = do
+  let files = NonEmpty.fromList $ dockerfiles cmd
+      filePathInReport = filePathInReportOption cmd
   res <- Hadolint.lintIO conf files
-  noColorEnv <- lookupEnv "NO_COLOR"
-  let noColor = nocolor cmd || isJust noColorEnv
-  let filePathInReport = filePathInReportOption cmd
-  Hadolint.printResults (format cmd) noColor filePathInReport res
-  exitProgram cmd conf res
+  printResults (format conf) (noColor conf) filePathInReport res
+  exitProgram conf res
 
+execute :: CommandlineConfig -> Configuration -> IO ()
+execute CommandlineConfig {showVersion = True} _ =
+  putStrLn Hadolint.getVersion >> exitSuccess
+execute CommandlineConfig {dockerfiles = []} _ =
+  putStrLn "Please provide a Dockerfile" >> exitFailure
+execute cmd config = runLint cmd config
+
+
 main :: IO ()
 main = do
-  cmd <- execParser opts
-  execute cmd
+  invokedWith <- execParser opts
+  fromEnvironment <- getConfigFromEnvironment
+  let fromCommandline = configuration invokedWith
+  eitherFromConfigfile <- getConfigFromFile
+    (configFile invokedWith) (Just True == partialVerbose fromCommandline)
+  fromConfigfile <- getConfigFromEither eitherFromConfigfile
+
+  let runningConfig =
+        applyPartialConfiguration
+          def
+          ( fromEnvironment <> fromConfigfile <> fromCommandline )
+
+  when (verbose runningConfig) (hPrint stderr (pretty runningConfig))
+  execute invokedWith runningConfig
   where
-    execute CommandOptions {showVersion = True} = putStrLn getVersion >> exitSuccess
-    execute CommandOptions {dockerfiles = []} =
-      putStrLn "Please provide a Dockerfile" >> exitFailure
-    execute cmd = do
-      maybeConfig <- Hadolint.getConfig (configFile cmd)
-      when (isVerbose cmd) (hPutStrLn stderr $ getFilePathDescription maybeConfig)
-      lintConfig <- Hadolint.applyConfig maybeConfig (lintingOptions cmd)
-      let files = NonEmpty.fromList (dockerfiles cmd)
-      case lintConfig of
-        Left err -> error err
-        Right conf -> runLint cmd conf files
     opts =
       info
-        (helper <*> parseOptions)
-        ( fullDesc <> progDesc "Lint Dockerfile for errors and best practices"
+        ( helper <*> parseCommandline )
+        ( fullDesc
             <> header "hadolint - Dockerfile Linter written in Haskell"
+            <> progDesc "Lint Dockerfile for errors and best practices"
         )
 
-getVersion :: String
-getVersion
-  | version == "UNKNOWN" =
-    "Haskell Dockerfile Linter " ++ Data.Version.showVersion Meta.version ++ "-no-git"
-  | otherwise = "Haskell Dockerfile Linter " ++ version
-  where
-    version = $(Development.GitRev.gitDescribe)
-
-getFilePathDescription :: Maybe FilePath -> String
-getFilePathDescription Nothing = "No configuration was specified. Using default configuration"
-getFilePathDescription (Just filepath) = "Configuration file used: " ++ filepath
+    -- Either return the config or print the error message
+    getConfigFromEither ei =
+      case ei of
+        Left err -> do
+          hPrint stderr err
+          return mempty
+        Right conf -> return conf
diff --git a/hadolint.cabal b/hadolint.cabal
--- a/hadolint.cabal
+++ b/hadolint.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           hadolint
-version:        2.7.0
+version:        2.8.0
 synopsis:       Dockerfile Linter JavaScript API
 description:    A smarter Dockerfile linter that helps you build best practice Docker images.
 category:       Development
@@ -32,15 +32,22 @@
   exposed-modules:
       Hadolint
       Hadolint.Config
+      Hadolint.Config.Commandline
+      Hadolint.Config.Configfile
+      Hadolint.Config.Configuration
+      Hadolint.Config.Environment
+      Hadolint.Formatter
       Hadolint.Formatter.Checkstyle
       Hadolint.Formatter.Codacy
       Hadolint.Formatter.Codeclimate
       Hadolint.Formatter.Format
       Hadolint.Formatter.Json
+      Hadolint.Formatter.Sarif
       Hadolint.Formatter.SonarQube
       Hadolint.Formatter.TTY
-      Hadolint.Ignore
       Hadolint.Lint
+      Hadolint.Meta
+      Hadolint.Pragma
       Hadolint.Process
       Hadolint.Rule
       Hadolint.Rule.DL3000
@@ -116,13 +123,16 @@
   hs-source-dirs:
       src
   default-extensions:
-      OverloadedStrings
-      NamedFieldPuns
-      DeriveGeneric
       DeriveAnyClass
+      DeriveGeneric
+      NamedFieldPuns
+      OverloadedStrings
+      PatternSynonyms
       RecordWildCards
-      StrictData
       ScopedTypeVariables
+      StrictData
+      TemplateHaskell
+      TupleSections
       PatternSynonyms
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal
   build-depends:
@@ -135,18 +145,22 @@
     , colourista
     , containers
     , cryptonite
+    , data-default
     , deepseq ==1.4.4.*
     , directory >=1.3.0
     , email-validate
     , filepath
     , foldl
+    , gitrev >=1.3.1
     , ilist
-    , language-docker >=10.1.0 && <11
+    , language-docker >=10.3.0 && <11
     , megaparsec >=9.0.0
     , mtl
     , network-uri
+    , optparse-applicative >=0.14.0
     , parallel
     , parsec >=3.1.14
+    , prettyprinter >=1.7.0
     , semver
     , spdx
     , split >=0.2
@@ -163,24 +177,27 @@
   hs-source-dirs:
       app
   default-extensions:
-      OverloadedStrings
-      NamedFieldPuns
-      DeriveGeneric
       DeriveAnyClass
+      DeriveGeneric
+      NamedFieldPuns
+      OverloadedStrings
+      PatternSynonyms
       RecordWildCards
-      StrictData
       ScopedTypeVariables
-      PatternSynonyms
+      StrictData
       TemplateHaskell
+      TupleSections
+      PatternSynonyms
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"
   build-depends:
       base >=4.8 && <5
     , containers
-    , gitrev >=1.3.1
+    , data-default
     , hadolint
-    , language-docker >=10.1.0 && <11
+    , language-docker >=10.3.0 && <11
     , megaparsec >=9.0.0
     , optparse-applicative >=0.14.0
+    , prettyprinter >=1.7.0
     , text
   if flag(static) && !(os(osx))
     ld-options: -static -pthread
@@ -190,85 +207,97 @@
   type: exitcode-stdio-1.0
   main-is: Spec.hs
   other-modules:
-      ConfigSpec
-      DL3000
-      DL3001
-      DL3002
-      DL3003
-      DL3004
-      DL3005
-      DL3006
-      DL3007
-      DL3008
-      DL3009
-      DL3010
-      DL3011
-      DL3012
-      DL3013
-      DL3014
-      DL3015
-      DL3016
-      DL3018
-      DL3019
-      DL3020
-      DL3021
-      DL3022
-      DL3023
-      DL3024
-      DL3025
-      DL3026
-      DL3027
-      DL3028
-      DL3029
-      DL3030
-      DL3032
-      DL3033
-      DL3034
-      DL3035
-      DL3036
-      DL3037
-      DL3038
-      DL3040
-      DL3041
-      DL3042
-      DL3043
-      DL3044
-      DL3045
-      DL3046
-      DL3047
-      DL3048
-      DL3049
-      DL3050
-      DL3051
-      DL3052
-      DL3053
-      DL3054
-      DL3055
-      DL3056
-      DL3057
-      DL3058
-      DL3059
-      DL3060
-      DL4000
-      DL4001
-      DL4003
-      DL4004
-      DL4005
-      DL4006
+      Hadolint.Config.CommandlineSpec
+      Hadolint.Config.ConfigfileSpec
+      Hadolint.Config.ConfigurationSpec
+      Hadolint.Config.EnvironmentSpec
+      Hadolint.Config.SpecHook
+      Hadolint.Formatter.ParseErrorSpec
+      Hadolint.Formatter.SarifSpec
+      Hadolint.Formatter.TTYSpec
+      Hadolint.PragmaSpec
+      Hadolint.Rule.DL3000Spec
+      Hadolint.Rule.DL3001Spec
+      Hadolint.Rule.DL3002Spec
+      Hadolint.Rule.DL3003Spec
+      Hadolint.Rule.DL3004Spec
+      Hadolint.Rule.DL3005Spec
+      Hadolint.Rule.DL3006Spec
+      Hadolint.Rule.DL3007Spec
+      Hadolint.Rule.DL3008Spec
+      Hadolint.Rule.DL3009Spec
+      Hadolint.Rule.DL3010Spec
+      Hadolint.Rule.DL3011Spec
+      Hadolint.Rule.DL3012Spec
+      Hadolint.Rule.DL3013Spec
+      Hadolint.Rule.DL3014Spec
+      Hadolint.Rule.DL3015Spec
+      Hadolint.Rule.DL3016Spec
+      Hadolint.Rule.DL3018Spec
+      Hadolint.Rule.DL3019Spec
+      Hadolint.Rule.DL3020Spec
+      Hadolint.Rule.DL3021Spec
+      Hadolint.Rule.DL3022Spec
+      Hadolint.Rule.DL3023Spec
+      Hadolint.Rule.DL3024Spec
+      Hadolint.Rule.DL3025Spec
+      Hadolint.Rule.DL3026Spec
+      Hadolint.Rule.DL3027Spec
+      Hadolint.Rule.DL3028Spec
+      Hadolint.Rule.DL3029Spec
+      Hadolint.Rule.DL3030Spec
+      Hadolint.Rule.DL3032Spec
+      Hadolint.Rule.DL3033Spec
+      Hadolint.Rule.DL3034Spec
+      Hadolint.Rule.DL3035Spec
+      Hadolint.Rule.DL3036Spec
+      Hadolint.Rule.DL3037Spec
+      Hadolint.Rule.DL3038Spec
+      Hadolint.Rule.DL3040Spec
+      Hadolint.Rule.DL3041Spec
+      Hadolint.Rule.DL3042Spec
+      Hadolint.Rule.DL3043Spec
+      Hadolint.Rule.DL3044Spec
+      Hadolint.Rule.DL3045Spec
+      Hadolint.Rule.DL3046Spec
+      Hadolint.Rule.DL3047Spec
+      Hadolint.Rule.DL3048Spec
+      Hadolint.Rule.DL3049Spec
+      Hadolint.Rule.DL3050Spec
+      Hadolint.Rule.DL3051Spec
+      Hadolint.Rule.DL3052Spec
+      Hadolint.Rule.DL3053Spec
+      Hadolint.Rule.DL3054Spec
+      Hadolint.Rule.DL3055Spec
+      Hadolint.Rule.DL3056Spec
+      Hadolint.Rule.DL3057Spec
+      Hadolint.Rule.DL3058Spec
+      Hadolint.Rule.DL3059Spec
+      Hadolint.Rule.DL3060Spec
+      Hadolint.Rule.DL4000Spec
+      Hadolint.Rule.DL4001Spec
+      Hadolint.Rule.DL4003Spec
+      Hadolint.Rule.DL4004Spec
+      Hadolint.Rule.DL4005Spec
+      Hadolint.Rule.DL4006Spec
+      Hadolint.Rule.ShellcheckSpec
+      Hadolint.ShellSpec
       Helpers
-      Shellcheck
-      ShellSpec
+      RegressionSpec
       Paths_hadolint
   hs-source-dirs:
       test
   default-extensions:
-      OverloadedStrings
-      NamedFieldPuns
-      DeriveGeneric
       DeriveAnyClass
+      DeriveGeneric
+      NamedFieldPuns
+      OverloadedStrings
+      PatternSynonyms
       RecordWildCards
-      StrictData
       ScopedTypeVariables
+      StrictData
+      TemplateHaskell
+      TupleSections
       PatternSynonyms
       ImplicitParams
       OverloadedLists
@@ -281,11 +310,14 @@
     , base >=4.8 && <5
     , bytestring >=0.10
     , containers
+    , data-default
     , foldl
     , hadolint
-    , hspec
-    , language-docker >=10.1.0 && <11
+    , hspec >=2.8.3
+    , language-docker >=10.3.0 && <11
     , megaparsec >=9.0.0
+    , optparse-applicative >=0.14.0
+    , silently
     , split >=0.2
     , text
   default-language: Haskell2010
diff --git a/src/Hadolint.hs b/src/Hadolint.hs
--- a/src/Hadolint.hs
+++ b/src/Hadolint.hs
@@ -1,47 +1,28 @@
 module Hadolint
-  ( module Hadolint.Lint,
-    module Hadolint.Process,
+  (
     module Hadolint.Config,
-    Result (..),
-    OutputFormat (..),
-    shallSkipErrorStatus,
-    printResults,
+    module Hadolint.Formatter,
+    module Hadolint.Lint,
+    module Hadolint.Meta,
+    module Hadolint.Process,
+
+    CheckFailure (..),
+    DLSeverity (..),
+    LabelSchema,
+    LabelName,
+    LabelType(..)
   )
 where
 
-import Data.Text (Text)
 import Hadolint.Config
-import qualified Hadolint.Formatter.Checkstyle
-import qualified Hadolint.Formatter.Codacy
-import qualified Hadolint.Formatter.Codeclimate
-import Hadolint.Formatter.Format (Result (..))
-import qualified Hadolint.Formatter.Json
-import qualified Hadolint.Formatter.SonarQube
-import qualified Hadolint.Formatter.TTY
+import Hadolint.Formatter
 import Hadolint.Lint
+import Hadolint.Meta
 import Hadolint.Process
-import Language.Docker.Parser (DockerfileError)
-
-data OutputFormat
-  = Json
-  | SonarQube
-  | TTY
-  | CodeclimateJson
-  | GitlabCodeclimateJson
-  | Checkstyle
-  | Codacy
-  deriving (Show, Eq)
-
-shallSkipErrorStatus :: OutputFormat -> Bool
-shallSkipErrorStatus format = format `elem` [CodeclimateJson, Codacy]
-
-printResults :: Foldable f => OutputFormat -> Bool -> Maybe FilePath -> f (Result Text DockerfileError) -> IO ()
-printResults format nocolor filePathInReport allResults =
-  case format of
-    TTY -> Hadolint.Formatter.TTY.printResults allResults nocolor
-    SonarQube -> Hadolint.Formatter.SonarQube.printResults allResults
-    Json -> Hadolint.Formatter.Json.printResults allResults
-    Checkstyle -> Hadolint.Formatter.Checkstyle.printResults allResults filePathInReport
-    CodeclimateJson -> Hadolint.Formatter.Codeclimate.printResults allResults
-    GitlabCodeclimateJson -> Hadolint.Formatter.Codeclimate.printGitlabResults allResults
-    Codacy -> Hadolint.Formatter.Codacy.printResults allResults
+import Hadolint.Rule
+  ( CheckFailure (..),
+    DLSeverity (..),
+    LabelName,
+    LabelSchema,
+    LabelType (..),
+  )
diff --git a/src/Hadolint/Config.hs b/src/Hadolint/Config.hs
--- a/src/Hadolint/Config.hs
+++ b/src/Hadolint/Config.hs
@@ -1,170 +1,14 @@
 module Hadolint.Config
-  ( applyConfig,
-    getConfig,
-    ConfigFile (..),
-    OverrideConfig (..),
-  )
-where
+  ( module Hadolint.Config.Configuration,
 
-import Control.Applicative ((<|>))
-import qualified Control.Foldl.Text as Text
-import Control.Monad (filterM)
-import qualified Data.ByteString as Bytes
-import Data.Coerce (coerce)
-import Data.Maybe (fromMaybe, listToMaybe)
-import qualified Data.Set as Set
-import Data.YAML ((.:?))
-import qualified Data.YAML as Yaml
-import GHC.Generics (Generic)
-import qualified Hadolint.Lint as Lint
-import qualified Hadolint.Process as Process
-import qualified Hadolint.Rule as Rule
-import qualified Language.Docker as Docker
-import System.Directory
-  ( XdgDirectory (..),
-    doesFileExist,
-    getCurrentDirectory,
-    getXdgDirectory,
+    CommandlineConfig (..),
+    getConfigFromEnvironment,
+    getConfigFromFile,
+    parseCommandline
   )
-import System.FilePath ((</>))
-
-data OverrideConfig = OverrideConfig
-  { overrideErrorRules :: Maybe [Lint.ErrorRule],
-    overrideWarningRules :: Maybe [Lint.WarningRule],
-    overrideInfoRules :: Maybe [Lint.InfoRule],
-    overrideStyleRules :: Maybe [Lint.StyleRule]
-  }
-  deriving (Show, Eq, Generic)
-
-instance Semigroup OverrideConfig where
-  OverrideConfig a1 a2 a3 a4 <> OverrideConfig b1 b2 b3 b4 =
-    OverrideConfig (a1 <> b1) (a2 <> b2) (a3 <> b3) (a4 <> b4)
-
-instance Monoid OverrideConfig where
-  mempty = OverrideConfig Nothing Nothing Nothing Nothing
-
-data ConfigFile = ConfigFile
-  { overrideRules :: Maybe OverrideConfig,
-    ignoredRules :: Maybe [Lint.IgnoreRule],
-    trustedRegistries :: Maybe [Lint.TrustedRegistry],
-    labelSchemaConfig :: Maybe Rule.LabelSchema,
-    strictLabelSchema :: Maybe Bool,
-    failureThreshold :: Maybe Rule.DLSeverity
-  }
-  deriving (Show, Eq, Generic)
-
-instance Yaml.FromYAML OverrideConfig where
-  parseYAML = Yaml.withMap "OverrideConfig" $ \m -> do
-    err <- m .:? "error"
-    wrn <- m .:? "warning"
-    inf <- m .:? "info"
-    sty <- m .:? "style"
-    let overrideErrorRules = coerce (err :: Maybe [Text.Text])
-        overrideWarningRules = coerce (wrn :: Maybe [Text.Text])
-        overrideInfoRules = coerce (inf :: Maybe [Text.Text])
-        overrideStyleRules = coerce (sty:: Maybe [Text.Text])
-    return OverrideConfig {..}
-
-instance Yaml.FromYAML ConfigFile where
-  parseYAML = Yaml.withMap "ConfigFile" $ \m -> do
-    overrideRules <- m .:? "override"
-    ignored <- m .:? "ignored"
-    let ignoredRules = coerce (ignored :: Maybe [Text.Text])
-    trustedRegistries <- m .:? "trustedRegistries"
-    labelSchemaConfig <- m .:? "label-schema"
-    strictLabelSchema <- m .:? "strict-labels"
-    failureThreshold <- m .:? "failure-threshold"
-    return ConfigFile {..}
-
--- | If both the ignoreRules and rulesConfig properties of Lint options are empty
--- then this function will fill them with the default found in the passed config
--- file. If there is an error parsing the default config file, this function will
--- return the error string.
-applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions)
-applyConfig maybeConfig o
-  | not (Prelude.null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)
-  | otherwise = do
-    case maybeConfig of
-      Nothing -> return (Right o)
-      Just config -> parseAndApply config
-  where
-    parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)
-    parseAndApply configFile = do
-      contents <- Bytes.readFile configFile
-      return $ case Yaml.decode1Strict contents of
-        Left (_, err) -> Left (formatError err configFile)
-        Right config -> Right $ fromMaybe o (applyOverride config)
-
-    applyOverride ConfigFile {..} =
-      -- Maybe.do
-      do
-        OverrideConfig {..} <- overrideRules <|> Just mempty
-        overrideError <- overrideErrorRules <|> Just mempty
-        overrideWarning <- overrideWarningRules <|> Just mempty
-        overrideInfo <- overrideInfoRules <|> Just mempty
-        overrideStyle <- overrideStyleRules <|> Just mempty
-        overrideIgnored <- ignoredRules <|> Just mempty
-        overrideThreshold <- failureThreshold <|> Just mempty
-
-        trusted <- Set.fromList . coerce <$> (trustedRegistries <|> Just mempty)
-        schema <- labelSchemaConfig <|> Just mempty
-        strictLabels <- strictLabelSchema <|> Just False
-
-        let rulesConfig = Lint.rulesConfig o
-
-        return $
-          Lint.LintOptions
-            { Lint.errorRules = Lint.errorRules o <|> overrideError,
-              Lint.warningRules = Lint.warningRules o <|> overrideWarning,
-              Lint.infoRules = Lint.infoRules o <|> overrideInfo,
-              Lint.styleRules = Lint.styleRules o <|> overrideStyle,
-              Lint.ignoreRules = Lint.ignoreRules o <|> overrideIgnored,
-              Lint.rulesConfig =
-                Process.RulesConfig
-                  { Process.allowedRegistries = Process.allowedRegistries rulesConfig `ifNull` trusted,
-                    Process.labelSchema = Process.labelSchema rulesConfig `ifNull` schema,
-                    Process.strictLabels = Process.strictLabels rulesConfig || strictLabels
-                  },
-            Lint.failThreshold = Lint.failThreshold o <> overrideThreshold
-            }
-
-    ifNull value override = if null value then override else value
-
-    formatError err config =
-      Prelude.unlines
-        [ "Error parsing your config file in  '" ++ config ++ "':",
-          "It should contain one of the keys 'override', 'ignored'",
-          "or 'trustedRegistries'. For example:\n",
-          "ignored:",
-          "\t- DL3000",
-          "\t- SC1099\n\n",
-          "The key 'override' should contain only lists with names 'error',",
-          "'warning', 'info' or 'style', which each name rules to override the",
-          "severity on. For example:\n",
-          "override:",
-          "\terror:",
-          "\t\t- DL3008\n\n",
-          "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n",
-          "trustedRegistries:",
-          "\t- docker.io",
-          "\t- my-company.com",
-          "",
-          err
-        ]
+where
 
--- | Gets the configuration file which Hadolint uses
-getConfig :: Maybe FilePath -> IO (Maybe FilePath)
-getConfig maybeConfig =
-  case maybeConfig of
-    Nothing -> findConfig
-    _ -> return maybeConfig
-  where
-    findConfig :: IO (Maybe FilePath)
-    findConfig = do
-      localConfigFiles <- traverse
-                            (\filePath -> (</> filePath) <$> getCurrentDirectory)
-                            (fmap ("."++) acceptedConfigs)
-      configFiles <- traverse (getXdgDirectory XdgConfig) acceptedConfigs
-      listToMaybe <$> filterM doesFileExist (localConfigFiles ++ configFiles)
-      where
-        acceptedConfigs = ["hadolint.yaml", "hadolint.yml"]
+import Hadolint.Config.Commandline
+import Hadolint.Config.Configuration
+import Hadolint.Config.Environment
+import Hadolint.Config.Configfile
diff --git a/src/Hadolint/Config/Commandline.hs b/src/Hadolint/Config/Commandline.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Config/Commandline.hs
@@ -0,0 +1,263 @@
+module Hadolint.Config.Commandline
+  ( CommandlineConfig (..),
+    parseCommandline
+  )
+where
+
+import Control.Applicative
+import Data.Bifunctor (second)
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import Data.String (IsString (fromString))
+import Data.Text (Text, pack, unpack, breakOn, drop)
+import Hadolint.Formatter.Format (readMaybeOutputFormat)
+import Hadolint.Config.Configuration
+import Hadolint.Rule
+  ( LabelName,
+    LabelType,
+    readEitherLabelType,
+    readEitherSeverity
+  )
+import Options.Applicative
+  ( Parser,
+    ReadM,
+    action,
+    argument,
+    completeWith,
+    eitherReader,
+    flag',
+    help,
+    long,
+    maybeReader,
+    metavar,
+    option,
+    short,
+    str,
+    strOption,
+    switch,
+  )
+
+
+data CommandlineConfig =
+  CommandlineConfig
+    { showVersion :: Bool,
+      configFile :: Maybe FilePath,
+      dockerfiles :: [String],
+      filePathInReportOption :: Maybe FilePath,
+      configuration :: PartialConfiguration
+    }
+  deriving (Eq, Show)
+
+parseCommandline :: Parser CommandlineConfig
+parseCommandline =
+  CommandlineConfig
+    <$> parseVersion
+    <*> parseConfigFile
+    <*> parseFiles
+    <*> parseFilePathInReportOption
+    <*> parseConfiguration
+  where
+    parseVersion = switch (long "version" <> short 'v' <> help "Show version")
+
+    parseConfigFile =
+      optional
+        ( strOption
+            ( long "config" <> short 'c' <> metavar "FILENAME"
+                <> help "Path to the configuration file"
+            )
+        )
+
+    parseFiles = many (argument str (metavar "DOCKERFILE..." <> action "file"))
+
+    parseFilePathInReportOption =
+      optional
+        ( strOption
+            ( long "file-path-in-report"
+                <> metavar "FILEPATHINREPORT"
+                <> help "The file path referenced in the generated report.\
+                        \ This only applies for the 'checkstyle' format and is\
+                        \ useful when running Hadolint with Docker to set the\
+                        \ correct file path."
+            )
+        )
+
+    parseConfiguration =
+      PartialConfiguration
+        <$> parseNoFail
+        <*> parseNoColor
+        <*> parseVerbose
+        <*> parseOutputFormat
+        <*> parseErrorList
+        <*> parseWarningList
+        <*> parseInfoList
+        <*> parseStyleList
+        <*> parseIgnoreList
+        <*> parseAllowedRegistries
+        <*> parseLabelSchema
+        <*> parseStrictlabels
+        <*> parseFailureThreshold
+
+    -- All optional flags with boolean value must not have a default value. The
+    -- optional parser then converts it to Nothing.
+    -- This is to ensure if they are not set, this is correctly mirrored in the
+    -- parsed Configuration and then their behaviour with respect to
+    -- configurations from environment variables or config files is correct.
+    parseNoFail =
+      optional
+        ( flag' True
+            ( long "no-fail"
+                <> help "Don't exit with a failure status code when any rule is\
+                        \ violated"
+            )
+        )
+
+    parseNoColor =
+      optional
+        ( flag' True
+            ( long "no-color"
+              <> help "Don't colorize output"
+            )
+        )
+
+    parseVerbose =
+      optional
+        ( flag' True
+            ( long "verbose"
+                <> short 'V'
+                <> help "Enables verbose logging of hadolint's output to stderr"
+            )
+        )
+
+    parseOutputFormat =
+      optional $
+        option
+          ( maybeReader (readMaybeOutputFormat . pack) )
+          ( long "format"
+              <> short 'f' -- options for the output format
+              <> help
+                "The output format for the results [tty | json | checkstyle |\
+                \ codeclimate | gitlab_codeclimate | codacy | sonarqube |\
+                \ sarif] (default: tty)"
+              <> completeWith
+                  [ "tty",
+                    "json",
+                    "checkstyle",
+                    "codeclimate",
+                    "gitlab_codeclimate",
+                    "codacy",
+                    "sonarqube",
+                    "sarif"
+                  ]
+          )
+
+    parseErrorList =
+      many
+        ( strOption
+            ( long "error"
+                <> help "Make the rule `RULECODE` have the level `error`"
+                <> metavar "RULECODE"
+            )
+        )
+
+    parseWarningList =
+      many
+        ( strOption
+            ( long "warning"
+                <> help "Make the rule `RULECODE` have the level `warning`"
+                <> metavar "RULECODE"
+            )
+        )
+
+    parseInfoList =
+      many
+        ( strOption
+            ( long "info"
+                <> help "Make the rule `RULECODE` have the level `info`"
+                <> metavar "RULECODE"
+            )
+        )
+
+    parseStyleList =
+      many
+        ( strOption
+            ( long "style"
+                <> help "Make the rule `RULECODE` have the level `style`"
+                <> metavar "RULECODE"
+            )
+        )
+
+    parseIgnoreList =
+      many
+        ( strOption
+            ( long "ignore"
+                <> metavar "RULECODE"
+                <> help "A rule to ignore. If present, the ignore list in the\
+                        \ config file is ignored"
+            )
+        )
+
+    parseAllowedRegistries =
+      Set.fromList . fmap fromString
+        <$> many
+          ( strOption
+              ( long "trusted-registry"
+                  <> metavar "REGISTRY (e.g. docker.io)"
+                  <> help "A docker registry to allow to appear in FROM \
+                          \instructions"
+              )
+          )
+
+    parseLabelSchema =
+      Map.fromList
+        <$> many
+          ( option
+              readSingleLabelSchema
+              ( long "require-label"
+                  <> metavar "LABELSCHEMA (e.g. maintainer:text)"
+                  <> help "The option --require-label=label:format makes\
+                          \ Hadolint check that the label `label` conforms to\
+                          \ format requirement `format`"
+              )
+          )
+
+    parseStrictlabels =
+      optional
+        ( flag' True
+            ( long "strict-labels"
+                <> help "Do not permit labels other than specified in\
+                        \ `label-schema`"
+            )
+        )
+
+    parseFailureThreshold =
+      optional $
+        option
+          ( eitherReader (readEitherSeverity . pack) )
+          ( short 't'
+              <> long "failure-threshold"
+              <> help
+                "Exit with failure code only when rules with a severity equal\
+                \ to or above THRESHOLD are violated. Accepted values: [error\
+                \ | warning | info | style | ignore | none] (default: info)"
+              <> metavar "THRESHOLD"
+              <> completeWith
+                  [ "error",
+                    "warning",
+                    "info",
+                    "style",
+                    "ignore",
+                    "none"
+                  ]
+          )
+
+
+type SingleLabelSchema = (LabelName, LabelType)
+
+readSingleLabelSchema :: ReadM SingleLabelSchema
+readSingleLabelSchema = eitherReader $ \s -> labelParser (pack s)
+
+labelParser :: Text -> Either String (LabelName, LabelType)
+labelParser l =
+  case second (readEitherLabelType . Data.Text.drop 1) $ breakOn ":" l of
+    (ln, Right lt) -> Right (ln, lt)
+    (_, Left e) -> Left (unpack e)
diff --git a/src/Hadolint/Config/Configfile.hs b/src/Hadolint/Config/Configfile.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Config/Configfile.hs
@@ -0,0 +1,101 @@
+module Hadolint.Config.Configfile
+  ( getConfigFromFile
+  )
+where
+
+import Control.Monad (when, filterM)
+import Data.Maybe (listToMaybe)
+import Data.YAML as Yaml
+import qualified Data.ByteString as Bytes
+import Hadolint.Config.Configuration (PartialConfiguration (..))
+import System.Directory
+  ( XdgDirectory (..),
+    doesFileExist,
+    getCurrentDirectory,
+    getAppUserDataDirectory,
+    getUserDocumentsDirectory,
+    getXdgDirectory,
+  )
+import System.FilePath ((</>))
+import System.IO (hPrint, stderr)
+
+
+getConfigFromFile ::
+  Maybe FilePath -> Bool -> IO (Either String PartialConfiguration)
+getConfigFromFile maybeExplicitPath verbose = do
+  maybePath <- getConfig maybeExplicitPath
+  when verbose $ hPrint stderr $ getFilePathDescription maybePath
+  case maybePath of
+    Nothing -> return $ Right mempty
+    Just path -> readConfig path
+
+readConfig :: FilePath -> IO (Either String PartialConfiguration)
+readConfig path = do
+  contents <- Bytes.readFile path
+  return $ case Yaml.decode1Strict contents of
+    Left (_, err) -> Left (formatError err path)
+    Right config -> Right config
+
+getFilePathDescription :: Maybe FilePath -> String
+getFilePathDescription Nothing =
+  "No configuration was specified. Using default configuration"
+getFilePathDescription (Just filepath) = "Configuration file used: " ++ filepath
+
+formatError :: String -> String -> String
+formatError err config =
+  Prelude.unlines
+    [ "Error parsing your config file in  '" ++ config ++ "':",
+      "It should contain one of the keys 'override', 'ignored'",
+      "or 'trustedRegistries'. For example:\n",
+      "ignored:",
+      "\t- DL3000",
+      "\t- SC1099\n\n",
+      "The key 'override' should contain only lists with names 'error',",
+      "'warning', 'info' or 'style', which each name rules to override the",
+      "severity on. For example:\n",
+      "override:",
+      "\terror:",
+      "\t\t- DL3008\n\n",
+      "The key 'trustedRegistries' should contain the names of the allowed",
+      "docker registries:\n",
+      "trustedRegistries:",
+      "\t- docker.io",
+      "\t- my-company.com",
+      "",
+      err
+    ]
+
+-- | Gets the configuration file which Hadolint uses
+getConfig :: Maybe FilePath -> IO (Maybe FilePath)
+getConfig maybeConfig =
+  case maybeConfig of
+    Nothing -> findConfig
+    _ -> return maybeConfig
+
+-- | If no configuration file path was given on the command line, Hadolint
+-- searches these locations or their equivalents on MacOS/Windows:
+--  - $(pwd)/.hadolint.{yaml|yml}
+--  - $HOME/.config/hadolint.{yaml|yml}
+--  - $HOME/.hadolint/{hadolint|config}.{yaml|yml}
+--  - $HOME/.hadolint.{yaml|yml}
+-- The first file found is used, all other are ignored.
+findConfig :: IO (Maybe FilePath)
+findConfig = do
+  filesInCWD <- traverse
+                  (\filePath -> (</> filePath) <$> getCurrentDirectory)
+                  hiddenConfigs
+  filesInXdgConfig <- traverse (getXdgDirectory XdgConfig) visibleConfigs
+  filesInAppData <- traverse
+                      (\fp -> (</> fp) <$> getAppUserDataDirectory "hadolint")
+                      (visibleConfigs <> moreConfigs)
+  filesInHome <- traverse
+                   (\fp -> (</> fp) <$> getUserDocumentsDirectory)
+                   hiddenConfigs
+  listToMaybe
+    <$> filterM
+          doesFileExist
+          (filesInCWD <> filesInXdgConfig <> filesInAppData <> filesInHome)
+  where
+    hiddenConfigs = [".hadolint.yaml", ".hadolint.yml"]
+    visibleConfigs = ["hadolint.yaml", "hadolint.yml"]
+    moreConfigs = ["config.yaml", "config.yml"]
diff --git a/src/Hadolint/Config/Configuration.hs b/src/Hadolint/Config/Configuration.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Config/Configuration.hs
@@ -0,0 +1,229 @@
+module Hadolint.Config.Configuration
+  ( Configuration (..),
+    PartialConfiguration (..),
+
+    applyPartialConfiguration,
+  )
+where
+
+import Data.Maybe (fromMaybe)
+import Control.Applicative
+import Data.Coerce (coerce)
+import Data.Default
+import Data.Text (Text)
+import Data.YAML ((.:?), (.!=))
+import GHC.Generics (Generic)
+import Hadolint.Formatter.Format (OutputFormat (..))
+import Hadolint.Rule (RuleCode (..), DLSeverity (..), LabelSchema)
+import Language.Docker
+import Prettyprinter
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified Data.YAML as Yaml
+
+
+data Configuration =
+  Configuration
+    { noFail :: Bool,
+      noColor :: Bool,
+      verbose :: Bool,
+      format :: OutputFormat,
+      errorRules :: [RuleCode],
+      warningRules :: [RuleCode],
+      infoRules :: [RuleCode],
+      styleRules :: [RuleCode],
+      ignoreRules :: [RuleCode],
+      allowedRegistries :: Set.Set Registry,
+      labelSchema :: LabelSchema,
+      strictLabels :: Bool,
+      failureThreshold :: DLSeverity
+    }
+  deriving (Eq, Show)
+
+instance Default Configuration where
+  def =
+    Configuration
+      False
+      False
+      False
+      def
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      False
+      def
+
+applyPartialConfiguration ::
+  Configuration -> PartialConfiguration -> Configuration
+applyPartialConfiguration config partial =
+  Configuration
+    (fromMaybe (noFail config) (partialNoFail partial))
+    (fromMaybe (noColor config) (partialNoColor partial))
+    (fromMaybe (verbose config) (partialVerbose partial))
+    (fromMaybe (format config) (partialFormat partial))
+    (errorRules config <> partialErrorRules partial)
+    (warningRules config <> partialWarningRules partial)
+    (infoRules config <> partialInfoRules partial)
+    (styleRules config <> partialStyleRules partial)
+    (ignoreRules config <> partialIgnoreRules partial)
+    (allowedRegistries config <> partialAllowedRegistries partial)
+    (labelSchema config <> partialLabelSchema partial)
+    (fromMaybe (strictLabels config) (partialStrictLabels partial))
+    (fromMaybe (failureThreshold config) (partialFailureThreshold partial))
+
+instance Pretty Configuration where
+  pretty c =
+    nest 2
+      ( vsep
+          [ "Configuration:",
+            "no fail:" <+> pretty (noFail c),
+            "no color:" <+> pretty (noColor c),
+            "output format:" <+> pretty (format c),
+            "failure threshold:" <+> pretty (failureThreshold c),
+            prettyPrintRulelist "error" (errorRules c),
+            prettyPrintRulelist "warning" (warningRules c),
+            prettyPrintRulelist "info" (infoRules c),
+            prettyPrintRulelist "style" (styleRules c),
+            prettyPrintRulelist "ignore" (ignoreRules c),
+            "strict labels:" <+> pretty (strictLabels c),
+            prettyPrintLabelSchema (labelSchema c),
+            prettyPrintRegistries (allowedRegistries c)
+          ]
+      )
+
+prettyPrintRulelist :: String -> [RuleCode] -> Doc ann
+prettyPrintRulelist name lst =
+  nest 2 (("override" <+> pretty name <> ":\n") <> prettyPrintList pretty lst)
+
+-- | This function needs to convert the set to a list because Doc ann is not
+-- ordered.
+prettyPrintRegistries :: Set.Set Registry -> Doc ann
+prettyPrintRegistries regs =
+  nest 2 ( "allowed registries:\n"
+             <> prettyPrintList
+                 (\r -> "-" <+> pretty (unRegistry r))
+                 (Set.toList regs)
+         )
+
+prettyPrintLabelSchema :: LabelSchema -> Doc ann
+prettyPrintLabelSchema ls =
+  nest 2 ( "label schema:\n"
+             <> prettyPrintList
+                  (\(n, t) -> pretty n <> ":" <+> pretty t)
+                  (Map.toList ls)
+         )
+
+-- | pretty print a list with a custom pretty printing function for each element
+prettyPrintList :: (a -> Doc ann) -> [a] -> Doc ann
+prettyPrintList _ [] = "none"
+prettyPrintList prnt lst = vsep (fmap (\i -> "-" <+> prnt i) lst)
+
+
+data PartialConfiguration =
+  PartialConfiguration
+    { partialNoFail :: Maybe Bool,
+      partialNoColor :: Maybe Bool,
+      partialVerbose :: Maybe Bool,
+      partialFormat :: Maybe OutputFormat,
+      partialErrorRules :: [RuleCode],
+      partialWarningRules :: [RuleCode],
+      partialInfoRules :: [RuleCode],
+      partialStyleRules :: [RuleCode],
+      partialIgnoreRules :: [RuleCode],
+      partialAllowedRegistries :: Set.Set Registry,
+      partialLabelSchema :: LabelSchema,
+      partialStrictLabels :: Maybe Bool,
+      partialFailureThreshold :: Maybe DLSeverity
+    }
+  deriving (Eq, Show)
+
+
+instance Semigroup PartialConfiguration where
+  PartialConfiguration a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13
+    <> PartialConfiguration b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 =
+      PartialConfiguration
+        (b1 <|> a1)
+        (b2 <|> a2)
+        (b3 <|> a3)
+        (a4 <> b4)
+        (a5 <> b5)
+        (a6 <> b6)
+        (a7 <> b7)
+        (a8 <> b8)
+        (a9 <> b9)
+        (a10 <> b10)
+        (a11 <> b11)
+        (b12 <|> a12)
+        (a13 <> b13)
+
+instance Monoid PartialConfiguration where
+  mempty =
+    PartialConfiguration
+      Nothing
+      Nothing
+      Nothing
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      mempty
+      Nothing
+      mempty
+
+instance Default PartialConfiguration where
+  def = mempty
+
+instance Yaml.FromYAML PartialConfiguration where
+  parseYAML = Yaml.withMap "Configuration" $ \m -> do
+    partialNoFail <- m .:? "no-fail" .!= Nothing
+    partialNoColor <- m .:? "no-color" .!= Nothing
+    partialVerbose <- m .:? "verbose" .!= Nothing
+    partialFormat <- m .:? "output-format"
+    override <- m .:? "override" .!= mempty
+    ignored <- m .:? "ignored" .!= mempty
+    trusted <- m .:? "trusted-registries" .!= mempty
+    partialLabelSchema <- m .:? "label-schema" .!= mempty
+    partialStrictLabels <- m .:? "strict-labels" .!= Nothing
+    let partialIgnoreRules = coerce (ignored :: [Text])
+        partialErrorRules = overrideErrorRules override
+        partialWarningRules = overrideWarningRules override
+        partialInfoRules = overrideInfoRules override
+        partialStyleRules = overrideStyleRules override
+        partialAllowedRegistries = Set.fromList (coerce (trusted :: [Text]))
+    partialFailureThreshold <- m .:? "failure-threshold"
+    return PartialConfiguration {..}
+
+
+data OverrideConfig = OverrideConfig
+  { overrideErrorRules :: [RuleCode],
+    overrideWarningRules :: [RuleCode],
+    overrideInfoRules :: [RuleCode],
+    overrideStyleRules :: [RuleCode]
+  }
+  deriving (Show, Eq, Generic)
+
+instance Semigroup OverrideConfig where
+  OverrideConfig a1 a2 a3 a4 <> OverrideConfig b1 b2 b3 b4 =
+    OverrideConfig (a1 <> b1) (a2 <> b2) (a3 <> b3) (a4 <> b4)
+
+instance Monoid OverrideConfig where
+  mempty = OverrideConfig mempty mempty mempty mempty
+
+instance Yaml.FromYAML OverrideConfig where
+  parseYAML = Yaml.withMap "OverrideConfig" $ \m -> do
+    err <- m .:? "error" .!= mempty
+    wrn <- m .:? "warning" .!= mempty
+    inf <- m .:? "info" .!= mempty
+    sty <- m .:? "style" .!= mempty
+    let overrideErrorRules = coerce (err :: [Text])
+        overrideWarningRules = coerce (wrn :: [Text])
+        overrideInfoRules = coerce (inf :: [Text])
+        overrideStyleRules = coerce (sty:: [Text])
+    return OverrideConfig {..}
diff --git a/src/Hadolint/Config/Environment.hs b/src/Hadolint/Config/Environment.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Config/Environment.hs
@@ -0,0 +1,103 @@
+module Hadolint.Config.Environment
+  ( getConfigFromEnvironment
+  )
+where
+
+import Data.Char (toLower)
+import Data.Coerce (coerce)
+import Data.Map (empty, fromList)
+import Data.Set (Set, empty, fromList)
+import Data.Text (Text, pack, unpack, drop, splitOn, breakOn)
+import Hadolint.Formatter.Format (OutputFormat (..), readMaybeOutputFormat)
+import Hadolint.Config.Configuration
+import Hadolint.Rule
+import Language.Docker.Syntax
+import System.Environment
+
+
+getConfigFromEnvironment :: IO PartialConfiguration
+getConfigFromEnvironment =
+  PartialConfiguration
+    <$> maybeTruthy "HADOLINT_NOFAIL"
+    <*> maybeTruthy "NO_COLOR"
+    <*> maybeTruthy "HADOLINT_VERBOSE"
+    <*> getFormat
+    <*> getOverrideList "HADOLINT_OVERRIDE_ERROR"
+    <*> getOverrideList "HADOLINT_OVERRIDE_WARNING"
+    <*> getOverrideList "HADOLINT_OVERRIDE_INFO"
+    <*> getOverrideList "HADOLINT_OVERRIDE_STYLE"
+    <*> getOverrideList "HADOLINT_IGNORE"
+    <*> getAllowedSet "HADOLINT_ALLOWED_REGISTRIES"
+    <*> getLabelSchema "HADOLINT_REQUIRE_LABELS"
+    <*> maybeTruthy "HADOLINT_STRICT_LABELS"
+    <*> getFailureThreshold
+
+
+maybeTruthy :: String -> IO (Maybe Bool)
+maybeTruthy name = do
+  e <- lookupEnv name
+  case e of
+    Just v ->
+      if truthy v
+      then return $ Just True
+      else return $ Just False
+    Nothing -> return Nothing
+
+truthy :: String -> Bool
+truthy s = map toLower s `elem` ["1", "y", "on", "true", "yes"]
+
+getFormat :: IO (Maybe OutputFormat)
+getFormat = do
+  fmt <- lookupEnv "HADOLINT_FORMAT"
+  return $ (readMaybeOutputFormat . pack) =<< fmt
+
+getOverrideList :: String -> IO [RuleCode]
+getOverrideList env = do
+  maybeString <- lookupEnv env
+  case maybeString of
+    Just s -> return $ getRulecodes (pack s)
+    Nothing -> return []
+
+getRulecodes :: Text -> [RuleCode]
+getRulecodes s = do
+  list <- splitOn "," s
+  let rules = coerce (list :: Text)
+  return rules
+
+getAllowedSet :: String -> IO (Set Registry)
+getAllowedSet env = do
+  maybeString <- lookupEnv env
+  case maybeString of
+    Just s -> return $ Data.Set.fromList (getAllowed (pack s))
+    Nothing -> return Data.Set.empty
+
+getAllowed :: Text -> [Registry]
+getAllowed s = do
+  list <- splitOn "," s
+  let regs = coerce (list :: Text)
+  return regs
+
+getLabelSchema :: String -> IO LabelSchema
+getLabelSchema env = do
+  maybeString <- lookupEnv env
+  case maybeString of
+    Just s -> return $ Data.Map.fromList (labelSchemaFromText (pack s))
+    Nothing -> return Data.Map.empty
+
+labelSchemaFromText :: Text -> [(LabelName, LabelType)]
+labelSchemaFromText txt =
+  [ (ln, lt) | Right (ln, lt) <- map convertToLabelSchema (convertToPairs txt) ]
+
+convertToPairs :: Text -> [(Text, Text)]
+convertToPairs txt = map (breakOn ":") (splitOn "," txt)
+
+convertToLabelSchema :: (Text, Text) -> Either String (LabelName, LabelType)
+convertToLabelSchema (tln, tlt) =
+  case (readEitherLabelType . Data.Text.drop 1) tlt of
+    Right lt -> Right (coerce tln :: Text, lt)
+    Left e -> Left (unpack e)
+
+getFailureThreshold :: IO (Maybe DLSeverity)
+getFailureThreshold = do
+  ft <- lookupEnv "HADOLINT_FAILURE_THRESHOLD"
+  return $ (readMaybeSeverity . pack) =<< ft
diff --git a/src/Hadolint/Formatter.hs b/src/Hadolint/Formatter.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Formatter.hs
@@ -0,0 +1,37 @@
+module Hadolint.Formatter
+  ( OutputFormat (..),
+    Result (..),
+    printResults,
+    readMaybeOutputFormat,
+  )
+where
+
+import Data.Text (Text)
+import Hadolint.Formatter.Format
+import Language.Docker.Parser (DockerfileError)
+import qualified Hadolint.Formatter.Checkstyle as FormatCheckstyle
+import qualified Hadolint.Formatter.Codacy as FormatCodacy
+import qualified Hadolint.Formatter.Codeclimate as FormatCodeclimate
+import qualified Hadolint.Formatter.Json as FormatJson
+import qualified Hadolint.Formatter.Sarif as FormatSarif
+import qualified Hadolint.Formatter.SonarQube as FormatSonarQube
+import qualified Hadolint.Formatter.TTY as FormatTTY
+
+
+printResults ::
+  Foldable f =>
+  OutputFormat ->
+  Bool ->
+  Maybe FilePath ->
+  f (Result Text DockerfileError) ->
+  IO ()
+printResults format nocolor filePathInReport allResults =
+  case format of
+    Checkstyle -> FormatCheckstyle.printResults allResults filePathInReport
+    Codacy -> FormatCodacy.printResults allResults
+    CodeclimateJson -> FormatCodeclimate.printResults allResults
+    GitlabCodeclimateJson -> FormatCodeclimate.printGitlabResults allResults
+    Json -> FormatJson.printResults allResults
+    Sarif -> FormatSarif.printResults allResults
+    SonarQube -> FormatSonarQube.printResults allResults
+    TTY -> FormatTTY.printResults allResults nocolor
diff --git a/src/Hadolint/Formatter/Format.hs b/src/Hadolint/Formatter/Format.hs
--- a/src/Hadolint/Formatter/Format.hs
+++ b/src/Hadolint/Formatter/Format.hs
@@ -1,25 +1,86 @@
 module Hadolint.Formatter.Format
-  ( severityText,
-    stripNewlines,
+  ( OutputFormat (..),
+    Result (..),
+    Text.Megaparsec.Error.errorBundlePretty,
     errorMessage,
     errorMessageLine,
     errorPosition,
     errorPositionPretty,
-    Text.Megaparsec.Error.errorBundlePretty,
-    Result (..),
+    severityText,
+    stripNewlines,
+    readMaybeOutputFormat,
     toResult,
   )
 where
 
-import qualified Data.List.NonEmpty as NE
+import Data.Default
 import Data.Sequence (Seq)
-import qualified Data.Sequence as Seq
-import qualified Data.Text as Text
-import qualified Hadolint.Rule
+import Data.Text (Text)
+import Data.Text.Prettyprint.Doc (Pretty, pretty)
+import Data.YAML
 import Text.Megaparsec (TraversableStream (..), pstateSourcePos)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (SourcePos, sourcePosPretty)
 import Text.Megaparsec.Stream (VisualStream)
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
+import qualified Hadolint.Rule
+
+data OutputFormat
+  = Json
+  | SonarQube
+  | TTY
+  | CodeclimateJson
+  | GitlabCodeclimateJson
+  | Checkstyle
+  | Codacy
+  | Sarif
+  deriving (Eq, Show)
+
+instance Pretty OutputFormat where
+  pretty Json = "json"
+  pretty SonarQube = "sonarqube"
+  pretty TTY = "tty"
+  pretty CodeclimateJson = "codeclimate"
+  pretty GitlabCodeclimateJson = "gitlab_codeclimate"
+  pretty Checkstyle = "checkstyle"
+  pretty Codacy = "codacy"
+  pretty Sarif = "sarif"
+
+instance Semigroup OutputFormat where
+  _ <> f = f
+
+instance Monoid OutputFormat where
+  mempty = TTY
+
+instance FromYAML OutputFormat where
+  parseYAML = withOutputFormat pure
+
+withOutputFormat ::
+  (OutputFormat -> Parser a) ->
+  Node Pos ->
+  Parser a
+withOutputFormat f v@(Scalar _ (SStr b)) =
+  case readMaybeOutputFormat b of
+    Just out -> f out
+    Nothing -> typeMismatch "output format" v
+withOutputFormat _ v = typeMismatch "output format" v
+
+instance Default OutputFormat where
+  def = TTY
+
+readMaybeOutputFormat :: Text -> Maybe OutputFormat
+readMaybeOutputFormat "json" = Just Json
+readMaybeOutputFormat "sonarqube" = Just SonarQube
+readMaybeOutputFormat "tty" = Just TTY
+readMaybeOutputFormat "codeclimate" = Just CodeclimateJson
+readMaybeOutputFormat "gitlab_codeclimate" = Just GitlabCodeclimateJson
+readMaybeOutputFormat "checkstyle" = Just Checkstyle
+readMaybeOutputFormat "codacy" = Just Codacy
+readMaybeOutputFormat "sarif" = Just Sarif
+readMaybeOutputFormat _ = Nothing
+
 
 data Result s e = Result
   { fileName :: Text.Text,
diff --git a/src/Hadolint/Formatter/Sarif.hs b/src/Hadolint/Formatter/Sarif.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Formatter/Sarif.hs
@@ -0,0 +1,165 @@
+module Hadolint.Formatter.Sarif
+  ( printResults,
+    formatResult,
+  )
+where
+
+import qualified Control.Foldl as Foldl
+import Data.Aeson hiding (Result)
+import qualified Data.ByteString.Lazy.Char8 as B
+import Data.Sequence as Seq
+import qualified Data.Text as Text
+import Hadolint.Formatter.Format
+  ( Result (..),
+    errorMessage,
+    errorPosition,
+  )
+import Hadolint.Meta
+  ( getShortVersion,
+  )
+import Hadolint.Rule
+  ( CheckFailure (..),
+    DLSeverity (..),
+    unRuleCode,
+  )
+import Text.Megaparsec (TraversableStream)
+import Text.Megaparsec.Error
+import Text.Megaparsec.Pos
+  ( sourceColumn,
+    sourceLine,
+    sourceName,
+    unPos,
+  )
+import Text.Megaparsec.Stream (VisualStream)
+
+data SarifFormat s e
+  = SarifCheck Text.Text CheckFailure
+  | SarifError (ParseErrorBundle s e)
+
+instance
+  ( VisualStream s,
+    TraversableStream s,
+    ShowErrorComponent e
+  ) =>
+  ToJSON (SarifFormat s e)
+  where
+  toJSON (SarifCheck filename CheckFailure {..}) =
+    object
+      [ "ruleId" .= unRuleCode code,
+        "level" .= toSeverity severity,
+        "message"
+          .= object
+            [ "text" .= message
+            ],
+        "locations"
+          .= [ object
+                 [ "physicalLocation"
+                     .= object
+                       [ "artifactLocation"
+                           .= object
+                             [ "uri" .= filename
+                             ],
+                         "region"
+                           .= object
+                             [ "startLine" .= line,
+                               "endLine" .= line,
+                               "startColumn" .= (1 :: Int),
+                               "endColumn" .= (1 :: Int),
+                               "sourceLanguage" .= Text.pack language
+                             ]
+                       ]
+                 ]
+             ]
+      ]
+    where
+      language = if "DL" `Text.isPrefixOf` unRuleCode code
+                    then "dockerfile"
+                    else "sh"
+  toJSON (SarifError err) =
+    object
+      [ "ruleId" .= Text.pack "DL1000",
+        "level" .= Text.pack "error",
+        "message"
+          .= object
+            [ "text" .= errorMessage err
+            ],
+        "locations"
+          .= [ object
+                 [ "physicalLocation"
+                     .= object
+                       [ "artifactLocation"
+                           .= object
+                             [ "uri" .= Text.pack (sourceName pos)
+                             ],
+                         "region"
+                           .= object
+                             [ "startLine" .= linenumber,
+                               "endLine" .= linenumber,
+                               "startColumn" .= column,
+                               "endColumn" .= column,
+                               "sourceLanguage" .= Text.pack "dockerfile"
+                             ]
+                       ]
+                 ]
+             ]
+      ]
+    where
+      pos = errorPosition err
+      linenumber = unPos $ sourceLine pos
+      column = unPos $ sourceColumn pos
+
+formatResult :: Result s e -> Seq (SarifFormat s e)
+formatResult (Result filename errors checks) = allMessages
+  where
+    allMessages = errorMessages <> checkMessages
+    checkMessages = fmap (SarifCheck filename) checks
+    errorMessages = fmap SarifError errors
+
+printResults ::
+  ( VisualStream s,
+    TraversableStream s,
+    ShowErrorComponent e,
+    Foldable f
+  ) =>
+  f (Result s e) ->
+  IO ()
+printResults results =
+  B.putStr . encode $
+    object
+      [ ("version", "2.1.0"),
+        "$schema"
+          .= Text.pack "http://json.schemastore.org/sarif-2.1.0",
+        "runs"
+          .= [ object
+                 [ "tool"
+                     .= object
+                       [ "driver"
+                           .= object
+                             [ ("name", "Hadolint"),
+                               ("fullName", "Haskell Dockerfile Linter"),
+                               ("downloadUri",
+                                  "https://github.com/hadolint/hadolint"),
+                               "version"
+                                 .= Text.pack Hadolint.Meta.getShortVersion,
+                               "shortDescription"
+                                 .= object
+                                   [ ("text",
+  "Dockerfile linter, validate inline bash, written in Haskell")
+                                   ]
+                             ]
+                       ],
+                   "results" .= flattened,
+                   "defaultSourceLanguage" .= Text.pack "dockerfile"
+                 ]
+             ]
+      ]
+  where
+    flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results
+
+-- | SARIF only specifies three severities "error", "warning" and "note"
+-- We pack our "info" and "style" severities together into the "note" severity
+-- here.
+toSeverity :: DLSeverity -> Text.Text
+toSeverity DLErrorC = "error"
+toSeverity DLWarningC = "warning"
+toSeverity _ = "note"
diff --git a/src/Hadolint/Ignore.hs b/src/Hadolint/Ignore.hs
deleted file mode 100644
--- a/src/Hadolint/Ignore.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-module Hadolint.Ignore (ignored) where
-
-import qualified Control.Foldl as Foldl
-import qualified Data.IntMap.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-import Data.Void (Void)
-import Hadolint.Rule (RuleCode (RuleCode))
-import Language.Docker.Syntax
-import qualified Text.Megaparsec as Megaparsec
-import qualified Text.Megaparsec.Char as Megaparsec
-
-ignored :: Foldl.Fold (InstructionPos Text.Text) (Map.IntMap (Set.Set RuleCode))
-ignored = Foldl.Fold parse mempty id
-  where
-    parse acc InstructionPos {instruction = Comment comment, lineNumber = line} =
-      case parseComment comment of
-        Just ignores@(_ : _) -> Map.insert (line + 1) (Set.fromList . fmap RuleCode $ ignores) acc
-        _ -> acc
-    parse acc _ = acc
-
-    parseComment :: Text.Text -> Maybe [Text.Text]
-    parseComment =
-      Megaparsec.parseMaybe commentParser
-
-    commentParser :: Megaparsec.Parsec Void Text.Text [Text.Text]
-    commentParser =
-      do
-        spaces
-        >> string "hadolint"
-        >> spaces1
-        >> string "ignore="
-        >> spaces
-        >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)
-
-    ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` Set.fromList "DLSC0123456789")
-    string = Megaparsec.string
-    spaces = Megaparsec.takeWhileP Nothing space
-    spaces1 = Megaparsec.takeWhile1P Nothing space
-    space c = c == ' ' || c == '\t'
diff --git a/src/Hadolint/Lint.hs b/src/Hadolint/Lint.hs
--- a/src/Hadolint/Lint.hs
+++ b/src/Hadolint/Lint.hs
@@ -2,71 +2,37 @@
   ( lintIO,
     lint,
     analyze,
-    LintOptions (..),
-    ErrorRule,
-    WarningRule,
-    InfoRule,
-    StyleRule,
-    IgnoreRule,
     TrustedRegistry,
   )
 where
 
+import Data.Sequence (Seq)
+import Data.Text (Text)
+import Hadolint.Config.Configuration (Configuration (..))
+import Hadolint.Rule (RuleCode, DLSeverity (..), CheckFailure (..))
+import Language.Docker.Parser (DockerfileError, Error)
+import Language.Docker.Syntax (Dockerfile)
 import qualified Control.Parallel.Strategies as Parallel
 import qualified Data.List.NonEmpty as NonEmpty
 import qualified Data.Sequence as Seq
-import Data.Text (Text)
 import qualified Data.Text as Text
 import qualified Hadolint.Formatter.Format as Format
 import qualified Hadolint.Process
-import qualified Hadolint.Rule
 import qualified Language.Docker as Docker
-import Language.Docker.Parser (DockerfileError, Error)
-import Language.Docker.Syntax (Dockerfile)
 
-type ErrorRule = Hadolint.Rule.RuleCode
 
-type WarningRule = Hadolint.Rule.RuleCode
-
-type InfoRule = Hadolint.Rule.RuleCode
-
-type StyleRule = Hadolint.Rule.RuleCode
-
-type IgnoreRule = Hadolint.Rule.RuleCode
-
 type TrustedRegistry = Text
 
-data LintOptions = LintOptions
-  { errorRules :: [ErrorRule],
-    warningRules :: [WarningRule],
-    infoRules :: [InfoRule],
-    styleRules :: [StyleRule],
-    ignoreRules :: [IgnoreRule],
-    rulesConfig :: Hadolint.Process.RulesConfig,
-    failThreshold :: Hadolint.Rule.DLSeverity
-  }
-  deriving (Show)
 
-instance Semigroup LintOptions where
-  LintOptions a1 a2 a3 a4 a5 a6 a7 <> LintOptions b1 b2 b3 b4 b5 b6 b7 =
-    LintOptions
-      (a1 <> b1)
-      (a2 <> b2)
-      (a3 <> b3)
-      (a4 <> b4)
-      (a5 <> b5)
-      (a6 <> b6)
-      (a7 <> b7)
-
-instance Monoid LintOptions where
-  mempty = LintOptions mempty mempty mempty mempty mempty mempty mempty
-
--- | Performs the process of parsing the dockerfile and analyzing it with all the applicable
--- rules, depending on the list of ignored rules.
-lintIO :: LintOptions -> NonEmpty.NonEmpty FilePath -> IO (NonEmpty.NonEmpty (Format.Result Text DockerfileError))
-lintIO options dFiles = do
+-- | Performs the process of parsing the dockerfile and analyzing it with all
+-- the applicable rules, depending on the list of ignored rules.
+lintIO ::
+  Configuration ->
+  NonEmpty.NonEmpty FilePath ->
+  IO (NonEmpty.NonEmpty (Format.Result Text DockerfileError))
+lintIO config dFiles = do
   parsedFiles <- mapM parseFile (NonEmpty.toList dFiles)
-  return $ NonEmpty.fromList (lint options parsedFiles)
+  return $ NonEmpty.fromList (lint config parsedFiles)
   where
     parseFile :: String -> IO (Text, Either Error Dockerfile)
     parseFile "-" = do
@@ -77,42 +43,46 @@
       return (Text.pack s, res)
 
 lint ::
-  LintOptions ->
+  Configuration ->
   [(Text, Either Error Dockerfile)] ->
   [Format.Result Text DockerfileError]
-lint options parsedFiles = gather results `Parallel.using` parallelRun
+lint config parsedFiles = gather results `Parallel.using` parallelRun
   where
     gather = fmap (uncurry Format.toResult)
     results =
       [ ( name,
-          fmap (analyze options) parseResult
+          fmap (analyze config) parseResult
         )
         | (name, parseResult) <- parsedFiles
       ]
     parallelRun = Parallel.parList Parallel.rseq
 
-analyze :: LintOptions -> Dockerfile -> Seq.Seq Hadolint.Rule.CheckFailure
-analyze options dockerfile = fixer process
+analyze :: Configuration -> Dockerfile -> Seq Hadolint.Rule.CheckFailure
+analyze config dockerfile = fixer process
   where
-    fixer = fixSeverity options
-    process = Hadolint.Process.run (rulesConfig options) dockerfile
+    fixer = fixSeverity config
+    process = Hadolint.Process.run config dockerfile
 
-fixSeverity :: LintOptions -> Seq.Seq Hadolint.Rule.CheckFailure -> Seq.Seq Hadolint.Rule.CheckFailure
-fixSeverity LintOptions {..} = Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)
+fixSeverity ::
+  Configuration ->
+  Seq CheckFailure ->
+  Seq CheckFailure
+fixSeverity Configuration {..} =
+  Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)
   where
     correctSeverity =
-      makeSeverity Hadolint.Rule.DLErrorC errorRules
-        . makeSeverity Hadolint.Rule.DLWarningC warningRules
-        . makeSeverity Hadolint.Rule.DLInfoC infoRules
-        . makeSeverity Hadolint.Rule.DLStyleC styleRules
+      makeSeverity DLErrorC errorRules
+        . makeSeverity DLWarningC warningRules
+        . makeSeverity DLInfoC infoRules
+        . makeSeverity DLStyleC styleRules
 
     ignoredRules = ignoreFilter ignoreRules
 
-    makeSeverity s rules rule@Hadolint.Rule.CheckFailure {code} =
+    makeSeverity s rules rule@CheckFailure {code} =
       if code `elem` rules
-        then rule {Hadolint.Rule.severity = s}
+        then rule {severity = s}
         else rule
 
-    ignoreFilter :: [IgnoreRule] -> Hadolint.Rule.CheckFailure -> Bool
-    ignoreFilter ignored Hadolint.Rule.CheckFailure {code, severity} =
-      code `notElem` ignored && severity /= Hadolint.Rule.DLIgnoreC
+    ignoreFilter :: [RuleCode] -> CheckFailure -> Bool
+    ignoreFilter ignored CheckFailure {code, severity} =
+      code `notElem` ignored && severity /= DLIgnoreC
diff --git a/src/Hadolint/Meta.hs b/src/Hadolint/Meta.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Meta.hs
@@ -0,0 +1,21 @@
+module Hadolint.Meta
+  ( getVersion,
+    getShortVersion,
+  )
+where
+
+import qualified Development.GitRev
+
+
+getVersion :: String
+getVersion = "Haskell Dockerfile Linter " ++ getShortVersion
+
+getShortVersion :: String
+getShortVersion = v ++ d
+  where
+    version = $(Development.GitRev.gitDescribe)
+    dirty = $(Development.GitRev.gitDirty)
+    v = case version of
+      "UNKONWN" -> "-no-git"
+      _ -> version
+    d = if dirty then "-dirty" else ""
diff --git a/src/Hadolint/Pragma.hs b/src/Hadolint/Pragma.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Pragma.hs
@@ -0,0 +1,76 @@
+module Hadolint.Pragma
+  ( ignored,
+    parseShell
+  )
+  where
+
+import Data.Functor.Identity (Identity)
+import Data.Text (Text)
+import Data.Void (Void)
+import Hadolint.Rule (RuleCode (RuleCode))
+import Language.Docker.Syntax
+import qualified Control.Foldl as Foldl
+import qualified Data.IntMap.Strict as Map
+import qualified Data.Set as Set
+import qualified Text.Megaparsec as Megaparsec
+import qualified Text.Megaparsec.Char as Megaparsec
+
+
+ignored :: Foldl.Fold (InstructionPos Text) (Map.IntMap (Set.Set RuleCode))
+ignored = Foldl.Fold parse mempty id
+  where
+    parse acc InstructionPos {instruction = Comment comment, lineNumber = line} =
+      case parseComment comment of
+        Just ignores@(_ : _) -> Map.insert (line + 1) (Set.fromList . fmap RuleCode $ ignores) acc
+        _ -> acc
+    parse acc _ = acc
+
+parseComment :: Text -> Maybe [Text]
+parseComment =
+  Megaparsec.parseMaybe commentParser
+
+commentParser :: Megaparsec.Parsec Void Text [Text]
+commentParser =
+  do
+    spaces
+    >> string "hadolint"
+    >> spaces1
+    >> string "ignore="
+    >> spaces
+    >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)
+
+ruleName :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)
+ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` Set.fromList "DLSC0123456789")
+
+
+parseShell :: Text -> Maybe Text
+parseShell = Megaparsec.parseMaybe shellParser
+
+shellParser :: Megaparsec.Parsec Void Text Text
+shellParser =
+  do
+    spaces
+    >> string "hadolint"
+    >> spaces1
+    >> string "shell"
+    >> spaces
+    >> string "="
+    >> spaces
+    >> shellName
+
+shellName :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)
+shellName = Megaparsec.takeWhile1P Nothing (/= '\n')
+
+
+string :: Megaparsec.Tokens Text
+  -> Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)
+string = Megaparsec.string
+
+spaces :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)
+spaces = Megaparsec.takeWhileP Nothing space
+
+spaces1 :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)
+spaces1 = Megaparsec.takeWhile1P Nothing space
+
+space :: Char -> Bool
+space c = c == ' ' || c == '\t'
diff --git a/src/Hadolint/Process.hs b/src/Hadolint/Process.hs
--- a/src/Hadolint/Process.hs
+++ b/src/Hadolint/Process.hs
@@ -1,13 +1,14 @@
-module Hadolint.Process (run, RulesConfig (..)) where
+module Hadolint.Process (run) where
 
+import Hadolint.Config.Configuration (Configuration (..))
+import Hadolint.Rule (CheckFailure (..), Failures, Rule, RuleCode)
+import Language.Docker.Syntax
 import qualified Control.Foldl as Foldl
 import qualified Data.IntMap.Strict as SMap
 import qualified Data.Sequence as Seq
 import qualified Data.Set as Set
 import qualified Data.Text as Text
-import qualified Hadolint.Ignore
-import Hadolint.Rule (CheckFailure (..), Failures, Rule, RuleCode)
-import qualified Hadolint.Rule as Rule
+import qualified Hadolint.Pragma
 import qualified Hadolint.Rule.DL3000
 import qualified Hadolint.Rule.DL3001
 import qualified Hadolint.Rule.DL3002
@@ -74,28 +75,8 @@
 import qualified Hadolint.Rule.DL4006
 import qualified Hadolint.Rule.Shellcheck
 import qualified Hadolint.Shell as Shell
-import Language.Docker.Syntax
 
 
--- | Contains the required parameters for optional rules
-data RulesConfig = RulesConfig
-  { -- | The docker registries that are allowed in FROM
-    allowedRegistries :: Set.Set Registry,
-    labelSchema :: Rule.LabelSchema,
-    strictLabels :: Bool
-  }
-  deriving (Show, Eq)
-
-instance Semigroup RulesConfig where
-  RulesConfig a1 a2 a3 <> RulesConfig b1 b2 b3 =
-    RulesConfig
-      (a1 <> b1)
-      (a2 <> b2)
-      (a3 || b3)
-
-instance Monoid RulesConfig where
-  mempty = RulesConfig mempty mempty False
-
 data AnalisisResult = AnalisisResult
   { -- | The set of ignored rules per line
     ignored :: SMap.IntMap (Set.Set RuleCode),
@@ -103,7 +84,7 @@
     failed :: Failures
   }
 
-run :: RulesConfig -> [InstructionPos Text.Text] -> Failures
+run :: Configuration -> [InstructionPos Text.Text] -> Failures
 run config dockerfile = Seq.filter shouldKeep failed
   where
     AnalisisResult {..} = Foldl.fold (analyze config) dockerfile
@@ -113,16 +94,18 @@
         ignoreList <- SMap.lookup line ignored
         return $ code `Set.member` ignoreList
 
-analyze :: RulesConfig -> Foldl.Fold (InstructionPos Text.Text) AnalisisResult
+analyze ::
+  Configuration ->
+  Foldl.Fold (InstructionPos Text.Text) AnalisisResult
 analyze config =
   AnalisisResult
-    <$> Hadolint.Ignore.ignored
+    <$> Hadolint.Pragma.ignored
     <*> Foldl.premap parseShell (failures config <> onBuildFailures config)
 
 parseShell :: InstructionPos Text.Text -> InstructionPos Shell.ParsedShell
 parseShell = fmap Shell.parseShell
 
-onBuildFailures :: RulesConfig -> Rule Shell.ParsedShell
+onBuildFailures :: Configuration -> Rule Shell.ParsedShell
 onBuildFailures config =
   Foldl.prefilter
     isOnBuild
@@ -134,8 +117,8 @@
     unwrapOnbuild inst@InstructionPos {instruction = OnBuild i} = inst {instruction = i}
     unwrapOnbuild inst = inst
 
-failures :: RulesConfig -> Rule Shell.ParsedShell
-failures RulesConfig {allowedRegistries, labelSchema, strictLabels} =
+failures :: Configuration -> Rule Shell.ParsedShell
+failures Configuration {allowedRegistries, labelSchema, strictLabels} =
   Hadolint.Rule.DL3000.rule
     <> Hadolint.Rule.DL3001.rule
     <> Hadolint.Rule.DL3002.rule
diff --git a/src/Hadolint/Rule.hs b/src/Hadolint/Rule.hs
--- a/src/Hadolint/Rule.hs
+++ b/src/Hadolint/Rule.hs
@@ -1,14 +1,17 @@
 module Hadolint.Rule where
 
 import Control.DeepSeq (NFData)
+import Data.Default
+import Data.String (IsString (..))
+import Data.Text (Text, unpack)
+import GHC.Generics (Generic)
+import Language.Docker.Syntax
+import Prettyprinter (Pretty, pretty)
 import qualified Control.Foldl as Foldl
 import qualified Data.Map as Map
 import qualified Data.Sequence as Seq
-import Data.String (IsString (..))
 import qualified Data.Text as Text
 import qualified Data.YAML as Yaml
-import GHC.Generics (Generic)
-import Language.Docker.Syntax
 
 infixl 0 |>
 
@@ -21,37 +24,67 @@
   | DLInfoC
   | DLStyleC
   | DLIgnoreC
-  deriving (Show, Read, Eq, Ord, Generic, NFData)
+  deriving (Eq, Ord, Show, Generic, NFData)
 
 instance Yaml.FromYAML DLSeverity where
   parseYAML = withSeverity pure
 
-withSeverity :: (DLSeverity -> Yaml.Parser a) -> Yaml.Node Yaml.Pos -> Yaml.Parser a
+withSeverity ::
+  (DLSeverity -> Yaml.Parser a) ->
+  Yaml.Node Yaml.Pos ->
+  Yaml.Parser a
 withSeverity f v@(Yaml.Scalar _ (Yaml.SStr b)) =
-  case Hadolint.Rule.readSeverity b of
+  case readEitherSeverity b of
     Right s -> f s
     Left _ -> Yaml.typeMismatch "severity" v
 withSeverity _ v = Yaml.typeMismatch "severity" v
 
-readSeverity :: Text.Text -> Either Text.Text DLSeverity
-readSeverity "error" = Right DLErrorC
-readSeverity "warning" = Right DLWarningC
-readSeverity "info" = Right DLInfoC
-readSeverity "style" = Right DLStyleC
-readSeverity "ignore" = Right DLIgnoreC
-readSeverity "none" = Right DLIgnoreC
-readSeverity t = Left ("Invalid severity: " <> t)
+readEitherSeverity :: Text -> Either String DLSeverity
+readEitherSeverity "error" = Right DLErrorC
+readEitherSeverity "warning" = Right DLWarningC
+readEitherSeverity "info" = Right DLInfoC
+readEitherSeverity "style" = Right DLStyleC
+readEitherSeverity "ignore" = Right DLIgnoreC
+readEitherSeverity "none" = Right DLIgnoreC
+readEitherSeverity t = Left ("Invalid severity: " ++ unpack t)
 
-instance Semigroup DLSeverity where s1 <> s2 = min s1 s2
+readMaybeSeverity :: Text -> Maybe DLSeverity
+readMaybeSeverity "error" = Just DLErrorC
+readMaybeSeverity "warning" = Just DLWarningC
+readMaybeSeverity "info" = Just DLInfoC
+readMaybeSeverity "style" = Just DLStyleC
+readMaybeSeverity "ignore" = Just DLIgnoreC
+readMaybeSeverity "none" = Just DLIgnoreC
+readMaybeSeverity _ = Nothing
 
+instance Semigroup DLSeverity where _ <> s2 = s2
+
 instance Monoid DLSeverity where mempty = DLIgnoreC
 
-newtype RuleCode = RuleCode {unRuleCode :: Text.Text}
-  deriving (Show, Eq, Ord)
+instance Default DLSeverity where
+  def = DLInfoC
 
+instance Pretty DLSeverity where
+  pretty DLErrorC = "error"
+  pretty DLWarningC = "warning"
+  pretty DLInfoC = "info"
+  pretty DLStyleC = "style"
+  pretty DLIgnoreC = "ignore"
+
+
+newtype RuleCode = RuleCode {unRuleCode :: Text}
+  deriving (Eq, Ord)
+
+instance Show RuleCode where
+  show rc = show (unRuleCode rc)
+
 instance IsString RuleCode where
   fromString = RuleCode . Text.pack
 
+instance Pretty RuleCode where
+  pretty rc = pretty $ show rc
+
+
 data CheckFailure = CheckFailure
   { code :: RuleCode,
     severity :: DLSeverity,
@@ -63,47 +96,59 @@
 instance Ord CheckFailure where
   a `compare` b = line a `compare` line b
 
+
 type Failures = Seq.Seq CheckFailure
 
+
 data State a = State
   { failures :: Failures,
     state :: a
   }
   deriving (Show)
 
+
 type LabelName = Text.Text
 
 data LabelType
-  = RawText
-  | Url
-  | Spdx
+  = Email
   | GitHash
+  | RawText
   | Rfc3339
   | SemVer
-  | Email
-  deriving (Eq, Read, Show)
+  | Spdx
+  | Url
+  deriving (Eq, Show)
 
-read :: Text.Text -> Either Text.Text LabelType
-read "url"     = Right Url
-read "spdx"    = Right Spdx
-read "hash"    = Right GitHash
-read "rfc3339" = Right Rfc3339
-read "semver"  = Right SemVer
-read "email"   = Right Email
-read "text"    = Right RawText
-read ""        = Right RawText
-read t         = Left ("Invalid label type: " <> t)
+readEitherLabelType :: Text -> Either Text LabelType
+readEitherLabelType "email" = Right Email
+readEitherLabelType "hash" = Right GitHash
+readEitherLabelType "text" = Right RawText
+readEitherLabelType "rfc3339" = Right Rfc3339
+readEitherLabelType "semver" = Right SemVer
+readEitherLabelType "spdx" = Right Spdx
+readEitherLabelType "url" = Right Url
+readEitherLabelType "" = Right RawText
+readEitherLabelType t = Left ("invalid label type: " <> t)
 
 instance Yaml.FromYAML LabelType where
   parseYAML = withLabelType pure
 
 withLabelType :: (LabelType -> Yaml.Parser a) -> Yaml.Node Yaml.Pos -> Yaml.Parser a
 withLabelType f v@(Yaml.Scalar _ (Yaml.SStr b)) =
-    case Hadolint.Rule.read b of
+    case readEitherLabelType b of
       Right lt -> f lt
       Left _ -> Yaml.typeMismatch "labeltype" v
 withLabelType _ v = Yaml.typeMismatch "labeltype" v
 
+instance Pretty LabelType where
+  pretty RawText = "text"
+  pretty Url = "url"
+  pretty Spdx = "spdx"
+  pretty GitHash = "hash"
+  pretty Rfc3339 = "rfc3339"
+  pretty SemVer = "semver"
+  pretty Email = "email"
+
 type LabelSchema = Map.Map LabelName LabelType
 
 
@@ -214,3 +259,10 @@
     ".txz",
     ".xz"
   ]
+
+dropQuotes :: Text -> Text
+dropQuotes = Text.dropAround quotes
+  where
+    quotes '\"' = True
+    quotes '\'' = True
+    quotes _ = False
diff --git a/src/Hadolint/Rule/DL3000.hs b/src/Hadolint/Rule/DL3000.hs
--- a/src/Hadolint/Rule/DL3000.hs
+++ b/src/Hadolint/Rule/DL3000.hs
@@ -12,18 +12,12 @@
     severity = DLErrorC
     message = "Use absolute WORKDIR"
     check (Workdir loc)
-      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
-      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
-      | isWindowsAbsolute (Text.dropAround dropQuotes loc) = True
+      | "$" `Text.isPrefixOf` dropQuotes loc = True
+      | "/" `Text.isPrefixOf` dropQuotes loc = True
+      | isWindowsAbsolute (dropQuotes loc) = True
       | otherwise = False
     check _ = True
 {-# INLINEABLE rule #-}
-
-dropQuotes :: Char -> Bool
-dropQuotes chr
-  | chr == '\"' = True
-  | chr == '\'' = True
-  | otherwise = False
 
 isWindowsAbsolute :: Text.Text -> Bool
 isWindowsAbsolute path
diff --git a/src/Hadolint/Rule/DL3009.hs b/src/Hadolint/Rule/DL3009.hs
--- a/src/Hadolint/Rule/DL3009.hs
+++ b/src/Hadolint/Rule/DL3009.hs
@@ -1,20 +1,24 @@
 module Hadolint.Rule.DL3009 (rule) where
 
+import Hadolint.Rule
+import Language.Docker.Syntax
 import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
 import qualified Data.Text as Text
-import Hadolint.Rule
 import qualified Hadolint.Shell as Shell
-import Language.Docker.Syntax
 
+
 data Acc
   = Acc
       { lastFrom :: BaseImage,
+        dockerClean :: Bool,
         stages :: Map.Map Text.Text Linenumber,
         forgets :: Map.Map Linenumber BaseImage
       }
   | Empty
-  deriving (Show)
+  deriving (Eq, Show)
 
+
 rule :: Rule Shell.ParsedShell
 rule = veryCustomRule check (emptyState Empty) markFailures
   where
@@ -24,9 +28,14 @@
 
     check line st (From from) = st |> modify (rememberStage line from)
     check line st (Run (RunArgs args flags))
-      | hasNoCacheMount flags && foldArguments forgotToCleanup args =
-          st |> modify (rememberLine line)
-      | otherwise = st
+      | not (foldArguments forgotToCleanup args) =
+          if foldArguments disabledDockerClean args
+          then st |> modify rememberDockerClean
+          else st
+      | hasCacheDirectory "/var/lib/apt/lists" flags = st
+      | hasCacheDirectory "/var/lib/apt" flags
+        && hasCacheDirectory "/var/cache/apt" flags = st
+      | otherwise = st |> modify (rememberLine line)
     check _ st _ = st
 
     -- Convert the final state into failures
@@ -43,29 +52,53 @@
 {-# INLINEABLE rule #-}
 
 rememberStage :: Linenumber -> BaseImage -> Acc -> Acc
-rememberStage line from@BaseImage {image = Image _ als} (Acc _ stages o) = Acc from (Map.insert als line stages) o
-rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from (Map.singleton als line) Map.empty
+rememberStage line from@BaseImage {image = Image _ als} (Acc _ _ stages o) = Acc from True (Map.insert als line stages) o
+rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from True (Map.singleton als line) Map.empty
 
 rememberLine :: Linenumber -> Acc -> Acc
-rememberLine line (Acc from stages o) = Acc from stages (Map.insert line from o)
-rememberLine line Empty = Acc emptyImage mempty (Map.singleton line emptyImage)
+rememberLine line (Acc from clean stages o) = Acc from clean stages (Map.insert line from o)
+rememberLine line Empty = Acc emptyImage True mempty (Map.singleton line emptyImage)
 
+rememberDockerClean :: Acc -> Acc
+rememberDockerClean Empty = Acc emptyImage False mempty mempty
+rememberDockerClean (Acc from _ stages forget) = Acc from False stages forget
+
 forgotToCleanup :: Shell.ParsedShell -> Bool
 forgotToCleanup args
-  | hasUpdate && not hasCleanup = True
+  | hasUpdate args && not hasCleanup = True
   | otherwise = False
   where
     hasCleanup =
       any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)
 
-    hasUpdate = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)
+hasUpdate :: Shell.ParsedShell -> Bool
+hasUpdate args =
+  any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)
 
-hasNoCacheMount :: RunFlags -> Bool
-hasNoCacheMount RunFlags
-  { mount =
-      Just (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = p}})
-  } = Text.dropWhileEnd (=='/') p /= "/var/lib/apt/lists"
-hasNoCacheMount RunFlags {} = True
+disabledDockerClean :: Shell.ParsedShell -> Bool
+disabledDockerClean args
+  | removesScript || keepsPackages = True
+  | otherwise = False
+  where
+    removesScript =
+      any
+        (Shell.cmdHasArgs "rm" ["/etc/apt/apt.conf.d/docker-clean"])
+        (Shell.presentCommands args)
+    keepsPackages =
+      any
+        ( Shell.cmdHasArgs
+            "echo"
+            ["\'Binary::apt::APT::Keep-Downloaded-Packages \"true\";\'"]
+        )
+        (Shell.presentCommands args)
+
+hasCacheDirectory :: Text.Text -> RunFlags -> Bool
+hasCacheDirectory dir RunFlags { mount } =
+  not ( null $ Set.filter (isCacheMount dir) mount)
+
+isCacheMount :: Text.Text -> RunMount -> Bool
+isCacheMount dir (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = t}}) = dir `Text.isPrefixOf` t
+isCacheMount _ _ = False
 
 -- | Even though dockerfiles without a FROM are not valid, we still want to provide some feedback for this rule
 -- so we pretend there is a base image at the start of the file if there is none
diff --git a/src/Hadolint/Rule/DL3010.hs b/src/Hadolint/Rule/DL3010.hs
--- a/src/Hadolint/Rule/DL3010.hs
+++ b/src/Hadolint/Rule/DL3010.hs
@@ -1,21 +1,138 @@
 module Hadolint.Rule.DL3010 (rule) where
 
 import Data.Foldable (toList)
+import Data.Function ((&))
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.Set as Set
+import qualified Data.Sequence as Seq
 import qualified Data.Text as Text
 import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
 import Language.Docker.Syntax
 
-rule :: Rule args
-rule = simpleRule code severity message check
+
+data Acc
+  = Acc
+      { archives :: Set.Set (Linenumber, Text.Text),
+        extracted :: Set.Set (Linenumber, Text.Text)
+      }
+  | Empty
+
+rule :: Rule Shell.ParsedShell
+rule = veryCustomRule check (emptyState Empty) markFailures
   where
     code = "DL3010"
     severity = DLInfoC
-    message = "Use ADD for extracting archives into an image"
-    check (Copy (CopyArgs srcs _ _ _ NoSource)) =
-      and
-        [ not (format `Text.isSuffixOf` src)
-          | SourcePath src <- toList srcs,
-            format <- archiveFileFormatExtensions
-        ]
-    check _ = True
+    message = "Use `ADD` for extracting archives into an image"
+
+    check _ _ (From _) = emptyState Empty
+    check line st (Copy (CopyArgs srcs tgt _ _ NoSource)) =
+      st |> modify (rememberArchives line srcs tgt)
+    check _ st (Run (RunArgs args _))
+      | Acc archives _ <- state st,
+        ex <- foldArguments (getExtractedArchives archives) args =
+          st |> modify (markExtracted ex)
+      | otherwise = st
+    check _ st _ = st
+
+    markFailures :: State Acc -> Failures
+    markFailures (State fails (Acc _ e)) =
+      Set.foldl' (Seq.|>) fails (Set.map makeFail e)
+    markFailures st = failures st
+
+    makeFail :: (Linenumber, Text.Text) -> CheckFailure
+    makeFail (line, _) = CheckFailure {..}
 {-# INLINEABLE rule #-}
+
+
+extractsThisArchive :: (Linenumber, Text.Text) -> Shell.Command -> Bool
+extractsThisArchive (_, archive) cmd =
+  (isTarExtractCommand cmd || isUnzipCommand cmd) && archive `elem` arguments
+  where
+    arguments = map basename $ Shell.getArgsNoFlags cmd
+
+getExtractedArchives ::
+  Set.Set (Linenumber, Text.Text) ->
+  Shell.ParsedShell ->
+  Set.Set (Linenumber, Text.Text)
+getExtractedArchives archives shell =
+  Set.filter
+    (\a -> any (extractsThisArchive a) cmds)
+    archives
+  where
+    cmds = Shell.presentCommands shell
+
+isTarExtractCommand :: Shell.Command -> Bool
+isTarExtractCommand cmd@(Shell.Command name _ _) =
+  name == "tar" && (any longExtractFlags args || any shortExtractFlags args)
+  where
+    longExtractFlags f = f `elem` ["--extract", "--get"]
+    shortExtractFlags f = "-" `Text.isPrefixOf` f && "x" `Text.isInfixOf` f
+    args = Shell.getArgs cmd
+
+isUnzipCommand :: Shell.Command -> Bool
+isUnzipCommand (Shell.Command name _ _) =
+  name `elem`
+    [ "unzip",
+      "gunzip",
+      "bunzip2",
+      "unlzma",
+      "unxz",
+      "zgz",
+      "uncompress",
+      "zcat",
+      "gzcat"
+    ]
+
+markExtracted :: Set.Set (Linenumber, Text.Text) -> Acc -> Acc
+markExtracted _ Empty = Empty
+markExtracted exarcv Acc {archives, extracted} =
+  Acc { archives, extracted = Set.union exarcv extracted }
+
+rememberArchives ::
+  Linenumber ->
+  NonEmpty SourcePath ->
+  TargetPath ->
+  Acc ->
+  Acc
+rememberArchives line paths target Empty =
+  if isArchive $ unTargetPath target
+    then Acc
+          { archives = Set.singleton (line, basename $ unTargetPath target),
+            extracted = Set.empty
+          }
+    else Acc
+          { archives =
+              paths
+                & toList
+                & map (basename . unSourcePath)
+                & Set.fromList
+                & Set.filter isArchive
+                & Set.map (line,),
+            extracted = Set.empty
+          }
+rememberArchives line paths target Acc {archives, extracted} =
+  if isArchive $ unTargetPath target
+    then Acc
+          { archives =
+              Set.insert (line, basename $ unTargetPath target) archives,
+            extracted
+          }
+    else Acc
+          { archives =
+              paths
+                & toList
+                & map (basename . unSourcePath)
+                & Set.fromList
+                & Set.filter isArchive
+                & Set.map (line,)
+                & Set.union archives,
+            extracted
+          }
+
+basename :: Text.Text -> Text.Text
+basename = Text.takeWhileEnd (\c -> c /= '/' && c /= '\\') . dropQuotes
+
+isArchive :: Text.Text -> Bool
+isArchive src =
+  any (`Text.isSuffixOf` dropQuotes src) archiveFileFormatExtensions
diff --git a/src/Hadolint/Rule/DL3019.hs b/src/Hadolint/Rule/DL3019.hs
--- a/src/Hadolint/Rule/DL3019.hs
+++ b/src/Hadolint/Rule/DL3019.hs
@@ -1,10 +1,11 @@
 module Hadolint.Rule.DL3019 (rule) where
 
-import qualified Data.Text as Text
 import Hadolint.Rule
 import Hadolint.Shell (ParsedShell)
-import qualified Hadolint.Shell as Shell
 import Language.Docker.Syntax
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import qualified Hadolint.Shell as Shell
 
 rule :: Rule ParsedShell
 rule = simpleRule code severity message check
@@ -20,11 +21,16 @@
 {-# INLINEABLE rule #-}
 
 hasCacheMount :: RunFlags -> Bool
-hasCacheMount RunFlags
-  { mount =
-      Just (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = p}})
-  } = Text.dropWhileEnd (=='/') p == "/var/cache/apk"
-hasCacheMount RunFlags {} = False
+hasCacheMount RunFlags { mount } =
+  not (null $ Set.filter isCacheMount mount)
+
+isCacheMount :: RunMount -> Bool
+isCacheMount (CacheMount CacheOpts { cTarget = t })= isVarCacheApk t
+isCacheMount _ = False
+
+isVarCacheApk :: TargetPath -> Bool
+isVarCacheApk TargetPath { unTargetPath = p }
+  = Text.dropWhileEnd (=='/') p == "/var/cache/apk"
 
 forgotCacheOption :: Shell.Command -> Bool
 forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd
diff --git a/src/Hadolint/Rule/DL3020.hs b/src/Hadolint/Rule/DL3020.hs
--- a/src/Hadolint/Rule/DL3020.hs
+++ b/src/Hadolint/Rule/DL3020.hs
@@ -20,10 +20,13 @@
 isArchive :: Text.Text -> Bool
 isArchive path =
   or
-    ( [ ftype `Text.isSuffixOf` path
+    ( [ ftype `Text.isSuffixOf` dropQuotes path
         | ftype <- archiveFileFormatExtensions
       ]
     )
 
 isUrl :: Text.Text -> Bool
-isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])
+isUrl path = or
+  [ proto `Text.isPrefixOf` dropQuotes path
+    | proto <- ["https://", "http://"]
+  ]
diff --git a/src/Hadolint/Rule/DL3033.hs b/src/Hadolint/Rule/DL3033.hs
--- a/src/Hadolint/Rule/DL3033.hs
+++ b/src/Hadolint/Rule/DL3033.hs
@@ -12,15 +12,40 @@
     severity = DLWarningC
     message = "Specify version with `yum install -y <package>-<version>`."
 
-    check (Run (RunArgs args _)) = foldArguments (all versionFixed . yumPackages) args
+    check (Run (RunArgs args _)) =
+      foldArguments (all packageVersionFixed . yumPackages) args
+        && foldArguments (all moduleVersionFixed . yumModules) args
     check _ = True
-
-    versionFixed package =
-      "-" `Text.isInfixOf` package
-        || ".rpm" `Text.isSuffixOf` package
 {-# INLINEABLE rule #-}
 
 yumPackages :: Shell.ParsedShell -> [Text.Text]
 yumPackages args =
-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      not (Shell.cmdHasArgs "yum" ["module"] cmd),
+      arg <- installFilter cmd
+  ]
+
+packageVersionFixed :: Text.Text -> Bool
+packageVersionFixed package =
+  "-" `Text.isInfixOf` package || ".rpm" `Text.isSuffixOf` package
+
+yumModules :: Shell.ParsedShell -> [Text.Text]
+yumModules args =
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdHasArgs "yum" ["module"] cmd,
+      arg <- installFilter cmd
+  ]
+
+moduleVersionFixed :: Text.Text -> Bool
+moduleVersionFixed = Text.isInfixOf ":"
+
+installFilter :: Shell.Command -> [Text.Text]
+installFilter cmd =
+  [ arg
+    | Shell.cmdHasArgs "yum" ["install"] cmd,
+      arg <- Shell.getArgsNoFlags cmd,
+      arg /= "install",
+      arg /= "module"
   ]
diff --git a/src/Hadolint/Rule/DL3034.hs b/src/Hadolint/Rule/DL3034.hs
--- a/src/Hadolint/Rule/DL3034.hs
+++ b/src/Hadolint/Rule/DL3034.hs
@@ -26,5 +26,5 @@
           "si",
           "patch"
         ]
-    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]
+    hasYesOption = Shell.hasAnyFlag ["non-interactive", "n", "no-confirm", "y"]
 {-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3041.hs b/src/Hadolint/Rule/DL3041.hs
--- a/src/Hadolint/Rule/DL3041.hs
+++ b/src/Hadolint/Rule/DL3041.hs
@@ -12,15 +12,40 @@
     severity = DLWarningC
     message = "Specify version with `dnf install -y <package>-<version>`."
 
-    check (Run (RunArgs args _)) = foldArguments (all versionFixed . dnfPackages) args
+    check (Run (RunArgs args _)) =
+      foldArguments (all packageVersionFixed . dnfPackages) args
+        && foldArguments (all moduleVersionFixed . dnfModules) args
     check _ = True
-
-    versionFixed package =
-      "-" `Text.isInfixOf` package
-        || ".rpm" `Text.isSuffixOf` package
 {-# INLINEABLE rule #-}
 
 dnfPackages :: Shell.ParsedShell -> [Text.Text]
 dnfPackages args =
-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "dnf" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
+    [ arg
+      | cmd <- Shell.presentCommands args,
+        not (Shell.cmdHasArgs "dnf" ["module"] cmd),
+        arg <- installFilter cmd
+    ]
+
+packageVersionFixed :: Text.Text -> Bool
+packageVersionFixed package =
+  "-" `Text.isInfixOf` package || ".rpm" `Text.isSuffixOf` package
+
+dnfModules :: Shell.ParsedShell -> [Text.Text]
+dnfModules args =
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdHasArgs "dnf" ["module"] cmd,
+      arg <- installFilter cmd
+  ]
+
+moduleVersionFixed :: Text.Text -> Bool
+moduleVersionFixed = Text.isInfixOf ":"
+
+installFilter :: Shell.Command -> [Text.Text]
+installFilter cmd =
+  [ arg
+    | Shell.cmdHasArgs "dnf" ["install"] cmd,
+      arg <- Shell.getArgsNoFlags cmd,
+      arg /= "install",
+      arg /= "module"
   ]
diff --git a/src/Hadolint/Rule/DL3059.hs b/src/Hadolint/Rule/DL3059.hs
--- a/src/Hadolint/Rule/DL3059.hs
+++ b/src/Hadolint/Rule/DL3059.hs
@@ -1,29 +1,44 @@
 module Hadolint.Rule.DL3059 (rule) where
 
 import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
 import Language.Docker.Syntax
 
 
 data Acc
-  = Acc RunFlags
+  = Acc { flags :: RunFlags, count :: Int }
   | Empty
-  deriving (Eq, Show)
+  deriving (Eq)
 
-rule :: Rule args
+-- | This Rule catches multiple consecutive `RUN` instructions.
+-- It ignores the case where multiple commands are chained together (e.g. with
+-- `&&`) because in that case the programmer most likely has deliberately
+-- chosen to use multiuple `RUN` instructions. Cases where --mount=xxx flags
+-- differ are excluded as well.
+rule :: Rule Shell.ParsedShell
 rule = customRule check (emptyState Empty)
   where
     code = "DL3059"
     severity = DLInfoC
     message = "Multiple consecutive `RUN` instructions. Consider consolidation."
 
-    check line st (Run (RunArgs _ flags))
-      | state st == Acc flags = st |> addFail CheckFailure {..}
-      | otherwise = st |> modify (remember flags)
+    check line st (Run (RunArgs ar fl))
+      | state st == Empty =
+          st |> modify (remember fl (foldArguments countCommands ar))
+      | flags (state st) /= fl =
+          st |> modify (remember fl (foldArguments countCommands ar))
+      | foldArguments countCommands ar > 2 || count (state st) > 2 =
+          st |> modify (remember fl (foldArguments countCommands ar))
+      | otherwise = st |> addFail CheckFailure {..}
+    check _ st (Comment _) = st
     check _ st _ = st |> modify reset
 {-# INLINEABLE rule #-}
 
-remember :: RunFlags -> Acc -> Acc
-remember flags _ = Acc flags
+remember :: RunFlags -> Int -> Acc -> Acc
+remember fl cn _ = Acc { flags = fl, count = cn }
 
 reset :: Acc -> Acc
 reset _ = Empty
+
+countCommands :: Shell.ParsedShell -> Int
+countCommands script = length $ Shell.presentCommands script
diff --git a/src/Hadolint/Rule/Shellcheck.hs b/src/Hadolint/Rule/Shellcheck.hs
--- a/src/Hadolint/Rule/Shellcheck.hs
+++ b/src/Hadolint/Rule/Shellcheck.hs
@@ -3,27 +3,125 @@
 import qualified Data.Set as Set
 import qualified Data.Text as Text
 import Hadolint.Rule
-import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell
+import Hadolint.Pragma (parseShell)
 import qualified Hadolint.Shell as Shell
 import Language.Docker.Syntax
 import qualified ShellCheck.Interface
 
-rule :: Rule ParsedShell
-rule = customRule check (emptyState Shell.defaultShellOpts)
+
+data Acc
+  = Acc
+      { opts :: Shell.ShellOpts,
+        defaultOpts :: Shell.ShellOpts
+      }
+  | Empty
+
+
+rule :: Rule Shell.ParsedShell
+rule = customRule check (emptyState Empty)
   where
-    check _ st (From _) = st |> replaceWith Shell.defaultShellOpts -- Reset the state
-    check _ st (Arg name _) = st |> modify (Shell.addVars [name])
-    check _ st (Env pairs) = st |> modify (Shell.addVars (map fst pairs))
-    check _ st (Shell args) = st |> modify (Shell.setShell (foldArguments Shell.original args))
-    check line st (Run (RunArgs args _)) = getFailures |> foldr addFail st
+    check _ st (From _) = st |> modify newStage
+    check _ st (Arg name _) = st |> modify (addVars [name])
+    check _ st (Env pairs) = st |> modify (addVars (map fst pairs))
+    check _ st (Shell args) =
+      st |> modify (setShell (foldArguments Shell.original args))
+    check _ st (Comment com) =
+      case Hadolint.Pragma.parseShell com of
+        Just sh -> st |> modify (shellPragma sh)
+        _ -> st
+    check line st (Run (RunArgs args _)) = getFailures (state st) |> foldr addFail st
       where
-        getFailures = foldArguments (runShellCheck (state st)) args
-        runShellCheck opts script = Set.fromList [toFailure line c | c <- Shell.shellcheck opts script]
+        getFailures Empty = foldArguments (runShellCheck Shell.defaultShellOpts) args
+        getFailures s = foldArguments (runShellCheck (opts s)) args
+        runShellCheck options script =
+          Set.fromList
+            [ toFailure line c
+              | c <- Shell.shellcheck options script
+            ]
     check _ st _ = st
 {-# INLINEABLE rule #-}
 
+newStage :: Acc -> Acc
+newStage Empty =
+  Acc
+    { opts = Shell.defaultShellOpts,
+      defaultOpts = Shell.defaultShellOpts
+    }
+newStage Acc {..} =
+  Acc
+    { opts = defaultOpts,
+      defaultOpts
+    }
+
+addVars :: [Text.Text] -> Acc -> Acc
+addVars vars Empty =
+  Acc
+    { opts =
+        Shell.ShellOpts
+          { shellName = Shell.shellName Shell.defaultShellOpts,
+            envVars = Shell.envVars Shell.defaultShellOpts <> Set.fromList vars
+          },
+      defaultOpts = Shell.defaultShellOpts
+    }
+addVars vars Acc {..} =
+  Acc
+    { opts =
+        Shell.ShellOpts
+          { shellName = Shell.shellName opts,
+            envVars = Shell.envVars opts <> Set.fromList vars
+          },
+      defaultOpts
+    }
+
+setShell :: Text.Text -> Acc -> Acc
+setShell sh Empty =
+  Acc
+    { opts =
+        Shell.ShellOpts
+          { shellName = sh,
+            envVars = Shell.envVars Shell.defaultShellOpts
+          },
+      defaultOpts = Shell.defaultShellOpts
+    }
+setShell sh Acc {..} =
+  Acc
+    { opts =
+        Shell.ShellOpts
+          { shellName = sh,
+            envVars = Shell.envVars opts
+          },
+      defaultOpts
+    }
+
+shellPragma :: Text.Text -> Acc -> Acc
+shellPragma sh Empty =
+  Acc
+    { opts =
+        Shell.ShellOpts
+          { shellName = sh,
+            envVars = Shell.envVars Shell.defaultShellOpts
+          },
+      defaultOpts =
+        Shell.ShellOpts
+          { shellName = sh,
+            envVars = Shell.envVars Shell.defaultShellOpts
+          }
+    }
+shellPragma sh Acc {..} =
+  Acc
+    { opts =
+        Shell.ShellOpts
+          { shellName = sh,
+            envVars = Shell.envVars opts
+          },
+      defaultOpts
+    }
+
 -- | Converts ShellCheck errors into our own errors type
-toFailure :: Linenumber -> ShellCheck.Interface.PositionedComment -> CheckFailure
+toFailure :: Linenumber ->
+  ShellCheck.Interface.PositionedComment ->
+  CheckFailure
 toFailure line c =
   CheckFailure
     { code = RuleCode $ Text.pack ("SC" ++ show (code c)),
@@ -32,13 +130,16 @@
       line = line
     }
   where
-    getDLSeverity :: ShellCheck.Interface.Severity -> DLSeverity
-    getDLSeverity s =
-      case s of
-        ShellCheck.Interface.WarningC -> DLWarningC
-        ShellCheck.Interface.InfoC -> DLInfoC
-        ShellCheck.Interface.StyleC -> DLStyleC
-        _ -> DLErrorC
-    severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc
+    severity pc =
+      ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc
     code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc
-    message pc = ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc
+    message pc =
+      ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc
+
+getDLSeverity :: ShellCheck.Interface.Severity -> DLSeverity
+getDLSeverity s =
+  case s of
+    ShellCheck.Interface.WarningC -> DLWarningC
+    ShellCheck.Interface.InfoC -> DLInfoC
+    ShellCheck.Interface.StyleC -> DLStyleC
+    _ -> DLErrorC
diff --git a/src/Hadolint/Shell.hs b/src/Hadolint/Shell.hs
--- a/src/Hadolint/Shell.hs
+++ b/src/Hadolint/Shell.hs
@@ -61,7 +61,7 @@
 
 shellcheck :: ShellOpts -> ParsedShell -> [PositionedComment]
 shellcheck (ShellOpts sh env) (ParsedShell txt _ _) =
-  if any (`Text.isPrefixOf` sh) nonPosixShells || hasUnsupportedShebang txt
+  if any (`Text.isInfixOf` sh) nonPosixShells || hasUnsupportedShebang txt
     then [] -- Do no run for non-posix shells i.e. powershell, cmd.exe
     else runShellCheck
   where
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
deleted file mode 100644
--- a/test/ConfigSpec.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-module ConfigSpec where
-
-import Control.Monad (unless)
-import qualified Data.ByteString.Char8 as Bytes
-import Data.Map
-import qualified Data.YAML as Yaml
-import Hadolint.Config
-import Hadolint.Rule as Rule
-import Test.HUnit
-import Test.Hspec
-
-tests :: SpecWith ()
-tests =
-  describe "Config" $ do
-    it "Parses config with only error severities" $
-      let configFile =
-            [ "override:",
-              "  error:",
-              "    - DL3000",
-              "    - SC1010"
-            ]
-          override = Just (OverrideConfig (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing)
-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only warning severities" $
-      let configFile =
-            [ "override:",
-              "  warning:",
-              "    - DL3000",
-              "    - SC1010"
-            ]
-          override = Just (OverrideConfig Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing)
-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only info severities" $
-      let configFile =
-            [ "override:",
-              "  info:",
-              "    - DL3000",
-              "    - SC1010"
-            ]
-          override = Just (OverrideConfig Nothing Nothing (Just ["DL3000", "SC1010"]) Nothing)
-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only style severities" $
-      let configFile =
-            [ "override:",
-              "  style:",
-              "    - DL3000",
-              "    - SC1010"
-            ]
-          override = Just (OverrideConfig Nothing Nothing Nothing (Just ["DL3000", "SC1010"]))
-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only ignores" $
-      let configFile =
-            [ "ignored:",
-              "- DL3000",
-              "- SC1010"
-            ]
-          expected = ConfigFile Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only trustedRegistries" $
-      let configFile =
-            [ "trustedRegistries:",
-              "- hub.docker.com",
-              "- my.shady.xyz"
-            ]
-          expected = ConfigFile Nothing Nothing (Just ["hub.docker.com", "my.shady.xyz"]) Nothing Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only label-schema" $
-      let configFile =
-            [ "label-schema:",
-              "  author: text",
-              "  url: url"
-            ]
-          expected = ConfigFile Nothing Nothing Nothing (Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])) Nothing Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with only label-schema" $
-      let configFile = [ "strict-labels: true" ]
-          expected = ConfigFile Nothing Nothing Nothing Nothing (Just True) Nothing
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses config with failure-threshold" $
-      let configFile = [ "failure-threshold: error" ]
-          expected = ConfigFile Nothing Nothing Nothing Nothing Nothing (Just Rule.DLErrorC)
-       in assertConfig expected (Bytes.unlines configFile)
-
-    it "Parses full file" $
-      let configFile =
-            [ "override:",
-              "  info:",
-              "    - DL3002",
-              "  style:",
-              "    - DL3004",
-              "  warning:",
-              "    - DL3003",
-              "  error:",
-              "    - DL3001",
-              "",
-              "trustedRegistries:",
-              "- hub.docker.com",
-              "",
-              "ignored:",
-              "- DL3000",
-              "",
-              "strict-labels: false",
-              "label-schema:",
-              "  author: text",
-              "  url: url",
-              "",
-              "failure-threshold: style"
-            ]
-          override = Just (OverrideConfig (Just ["DL3001"]) (Just ["DL3003"]) (Just ["DL3002"]) (Just ["DL3004"]))
-          labelschema = Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])
-          expected = ConfigFile override (Just ["DL3000"]) (Just ["hub.docker.com"]) labelschema (Just False) (Just Rule.DLStyleC)
-       in assertConfig expected (Bytes.unlines configFile)
-
-assertConfig :: HasCallStack => ConfigFile -> Bytes.ByteString -> Assertion
-assertConfig config s =
-  case Yaml.decode1Strict s of
-    Left (_, err) ->
-      assertFailure err
-    Right result ->
-      checkResult result
-  where
-    checkResult result =
-      unless (result == config) $
-        assertFailure ("Config \n\n" ++ show config ++ "\n\n is not \n\n" ++ show result)
diff --git a/test/DL3000.hs b/test/DL3000.hs
deleted file mode 100644
--- a/test/DL3000.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module DL3000 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3000 - Use absolute WORKDIR." $ do
-    it "workdir relative" $ ruleCatches "DL3000" "WORKDIR relative/dir"
-    it "workdir absolute" $ ruleCatchesNot "DL3000" "WORKDIR /usr/local"
-    it "workdir variable" $ ruleCatchesNot "DL3000" "WORKDIR ${work}"
-    it "workdir relative single quotes" $ ruleCatches "DL3000" "WORKDIR \'relative/dir\'"
-    it "workdir absolute single quotes" $ ruleCatchesNot "DL3000" "WORKDIR \'/usr/local\'"
-    -- no test for variable/single quotes since the variable would not expand.
-    it "workdir relative double quotes" $ ruleCatches "DL3000" "WORKDIR \"relative/dir\""
-    it "workdir absolute double quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"/usr/local\""
-    it "workdir variable double quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"${dir}\""
-    --
-    it "workdir absolute windows" $ ruleCatchesNot "DL3000" "WORKDIR \'C:\\\'"
-    it "workdir absolute windows quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"C:\\\""
-    it "workdir absolute windows alternative" $
-      ruleCatchesNot "DL3000" "WORKDIR C:/"
-    it "workdir absolute windows quotes alternative" $
-      ruleCatchesNot "DL3000" "WORKDIR \"C:/\""
diff --git a/test/DL3001.hs b/test/DL3001.hs
deleted file mode 100644
--- a/test/DL3001.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-module DL3001 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3001 - invalid CMD rules" $ do
-    it "invalid cmd" $ do
-      ruleCatches "DL3001" "RUN top"
-      onBuildRuleCatches "DL3001" "RUN top"
-    it "install ssh" $ do
-      ruleCatchesNot "DL3001" "RUN apt-get install ssh"
-      onBuildRuleCatchesNot "DL3001" "RUN apt-get install ssh"
diff --git a/test/DL3002.hs b/test/DL3002.hs
deleted file mode 100644
--- a/test/DL3002.hs
+++ /dev/null
@@ -1,86 +0,0 @@
-module DL3002 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3002 - Last user should not be root." $ do
-    it "last user should not be root" $
-      let dockerFile =
-            [ "FROM scratch",
-              "USER root"
-            ]
-       in ruleCatches "DL3002" $ Text.unlines dockerFile
-
-    it "no root" $
-      let dockerFile =
-            [ "FROM scratch",
-              "USER foo"
-            ]
-       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
-
-    it "no root UID" $
-      let dockerFile =
-            [ "FROM scratch",
-              "USER 0"
-            ]
-       in ruleCatches "DL3002" $ Text.unlines dockerFile
-
-    it "no root:root" $
-      let dockerFile =
-            [ "FROM scratch",
-              "USER root:root"
-            ]
-       in ruleCatches "DL3002" $ Text.unlines dockerFile
-
-    it "no UID:GID" $
-      let dockerFile =
-            [ "FROM scratch",
-              "USER 0:0"
-            ]
-       in ruleCatches "DL3002" $ Text.unlines dockerFile
-
-    it "can switch back to non root" $
-      let dockerFile =
-            [ "FROM scratch",
-              "USER root",
-              "RUN something",
-              "USER foo"
-            ]
-       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
-
-    it "warns on transitive root user" $
-      let dockerFile =
-            [ "FROM debian as base",
-              "USER root",
-              "RUN something",
-              "FROM base",
-              "RUN something else"
-            ]
-       in ruleCatches "DL3002" $ Text.unlines dockerFile
-
-    it "warns on multiple stages" $
-      let dockerFile =
-            [ "FROM debian as base",
-              "USER root",
-              "RUN something",
-              "FROM scratch",
-              "USER foo",
-              "RUN something else"
-            ]
-       in ruleCatches "DL3002" $ Text.unlines dockerFile
-
-    it "does not warn when switching in multiple stages" $
-      let dockerFile =
-            [ "FROM debian as base",
-              "USER root",
-              "RUN something",
-              "USER foo",
-              "FROM scratch",
-              "RUN something else"
-            ]
-       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
diff --git a/test/DL3003.hs b/test/DL3003.hs
deleted file mode 100644
--- a/test/DL3003.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module DL3003 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3003 - Use WORKDIR to switch to a directory." $ do
-    it "ok using WORKDIR" $ ruleCatchesNot "DL3003" "WORKDIR /opt"
-    it "not ok using cd" $ ruleCatches "DL3003" "RUN cd /opt"
diff --git a/test/DL3004.hs b/test/DL3004.hs
deleted file mode 100644
--- a/test/DL3004.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module DL3004 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3004 - Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce root." $ do
-    it "sudo" $ do
-      ruleCatches "DL3004" "RUN sudo apt-get update"
-      onBuildRuleCatches "DL3004" "RUN sudo apt-get update"
-    it "install sudo" $ do
-      ruleCatchesNot "DL3004" "RUN apt-get install sudo"
-      onBuildRuleCatchesNot "DL3004" "RUN apt-get install sudo"
-    it "sudo chained programs" $ do
-      ruleCatches "DL3004" "RUN apt-get update && sudo apt-get install"
-      onBuildRuleCatches "DL3004" "RUN apt-get update && sudo apt-get install"
diff --git a/test/DL3005.hs b/test/DL3005.hs
deleted file mode 100644
--- a/test/DL3005.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module DL3005 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3005" $ do
-    it "apt-get dist-upgrade" $ do
-      ruleCatches "DL3005" "RUN apt-get update && apt-get dist-upgrade"
-      onBuildRuleCatches "DL3005" "RUN apt-get update && apt-get dist-upgrade"
diff --git a/test/DL3006.hs b/test/DL3006.hs
deleted file mode 100644
--- a/test/DL3006.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module DL3006 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3006 - Always tag the version of an image explicitly." $ do
-    it "no untagged" $ ruleCatches "DL3006" "FROM debian"
-    it "no untagged with name" $ ruleCatches "DL3006" "FROM debian AS builder"
-    it "untagged digest is not an error" $
-      ruleCatchesNot "DL3006" "FROM ruby@sha256:f1dbca0f5dbc9"
-    it "untagged digest is not an error" $
-      ruleCatchesNot "DL3006" "FROM ruby:2"
-    it "local aliases are OK to be untagged" $
-      let dockerFile =
-            [ "FROM golang:1.9.3-alpine3.7 AS build",
-              "RUN foo",
-              "FROM build as unit-test",
-              "RUN bar",
-              "FROM alpine:3.7",
-              "RUN baz"
-            ]
-       in do
-            ruleCatchesNot "DL3006" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3006" $ Text.unlines dockerFile
-    it "other untagged cases are not ok" $
-      let dockerFile =
-            [ "FROM golang:1.9.3-alpine3.7 AS build",
-              "RUN foo",
-              "FROM node as unit-test",
-              "RUN bar",
-              "FROM alpine:3.7",
-              "RUN baz"
-            ]
-       in do
-            ruleCatches "DL3006" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3006" $ Text.unlines dockerFile
-    it "scratch" $ ruleCatchesNot "DL3006" "FROM scratch"
diff --git a/test/DL3007.hs b/test/DL3007.hs
deleted file mode 100644
--- a/test/DL3007.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-module DL3007 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3007 - Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag." $ do
-    it "explicit latest" $ ruleCatches "DL3007" "FROM debian:latest"
-    it "explicit latest with name" $ ruleCatches "DL3007" "FROM debian:latest AS builder"
-    it "explicit tagged" $ ruleCatchesNot "DL3007" "FROM debian:jessie"
-    it "explicit SHA" $
-      ruleCatchesNot
-        "DL3007"
-        "FROM hub.docker.io/debian@sha256:\
-        \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"
-    it "explicit tagged with name" $
-      ruleCatchesNot "DL3007" "FROM debian:jessie AS builder"
diff --git a/test/DL3008.hs b/test/DL3008.hs
deleted file mode 100644
--- a/test/DL3008.hs
+++ /dev/null
@@ -1,61 +0,0 @@
-module DL3008 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3008 - Pin versions in apt-get install." $ do
-    it "apt-get version pinning" $ do
-      ruleCatches "DL3008" "RUN apt-get update && apt-get install python"
-      onBuildRuleCatches "DL3008" "RUN apt-get update && apt-get install python"
-    it "apt-get pinned chained" $
-      let dockerFile =
-            [ "RUN apt-get update \\",
-              " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\",
-              " && rm -rf /var/lib/apt/lists/*"
-            ]
-       in do
-            ruleCatchesNot "DL3008" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile
-
-    it "apt-get pinned regression" $
-      let dockerFile =
-            [ "RUN apt-get update && apt-get install --no-install-recommends -y \\",
-              "python-demjson=2.2.2* \\",
-              "wget=1.16.1* \\",
-              "git=1:2.5.0* \\",
-              "ruby=1:2.1.*"
-            ]
-       in do
-            ruleCatchesNot "DL3008" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile
-    it "apt-get version" $ do
-      ruleCatchesNot "DL3008" "RUN apt-get install -y python=1.2.2"
-      onBuildRuleCatchesNot "DL3008" "RUN apt-get install -y python=1.2.2"
-    it "apt-get version" $ do
-      ruleCatchesNot "DL3008" "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
-      onBuildRuleCatchesNot "DL3008" "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
-    it "apt-get pinned" $ do
-      ruleCatchesNot
-        "DL3008"
-        "RUN apt-get -y --no-install-recommends install nodejs=0.10"
-      onBuildRuleCatchesNot
-        "DL3008"
-        "RUN apt-get -y --no-install-recommends install nodejs=0.10"
-    it "apt-get tolerate target-release" $
-      let dockerFile =
-            [ "RUN set -e &&\\",
-              " echo \"deb http://http.debian.net/debian jessie-backports main\" \
-              \> /etc/apt/sources.list.d/jessie-backports.list &&\\",
-              " apt-get update &&\\",
-              " apt-get install -y --no-install-recommends -t jessie-backports \
-              \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\",
-              " rm -rf /var/lib/apt/lists/*"
-            ]
-       in do
-            ruleCatchesNot "DL3008" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile
diff --git a/test/DL3009.hs b/test/DL3009.hs
deleted file mode 100644
--- a/test/DL3009.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-module DL3009 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3009 - Delete the apt-get lists after installing something." $ do
-    it "apt-get no cleanup" $
-      let dockerFile =
-            [ "FROM scratch",
-              "RUN apt-get update && apt-get install python"
-            ]
-       in do
-            ruleCatches "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
-    it "apt-get cleanup in stage image" $
-      let dockerFile =
-            [ "FROM ubuntu as foo",
-              "RUN apt-get update && apt-get install python",
-              "FROM scratch",
-              "RUN echo hey!"
-            ]
-       in do
-            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
-    it "apt-get no cleanup in last stage" $
-      let dockerFile =
-            [ "FROM ubuntu as foo",
-              "RUN hey!",
-              "FROM scratch",
-              "RUN apt-get update && apt-get install python"
-            ]
-       in do
-            ruleCatches "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
-    it "apt-get no cleanup in intermediate stage" $
-      let dockerFile =
-            [ "FROM ubuntu as foo",
-              "RUN apt-get update && apt-get install python",
-              "FROM foo",
-              "RUN hey!"
-            ]
-       in do
-            ruleCatches "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
-    it "no warn apt-get cleanup in intermediate stage that cleans lists" $
-      let dockerFile =
-            [ "FROM ubuntu as foo",
-              "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*",
-              "FROM foo",
-              "RUN hey!"
-            ]
-       in do
-            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3009" $ Text.unlines dockerFile
-    it "no warn apt-get cleanup in intermediate stage when stage not used later" $
-      let dockerFile =
-            [ "FROM ubuntu as foo",
-              "RUN apt-get update && apt-get install python",
-              "FROM scratch",
-              "RUN hey!"
-            ]
-       in do
-            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
-    it "apt-get cleanup" $
-      let dockerFile =
-            [ "FROM scratch",
-              "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"
-            ]
-       in do
-            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3009" $ Text.unlines dockerFile
-
-    it "don't warn: BuildKit cache mount to apt lists directory" $ do
-      ruleCatchesNot "DL3009" "RUN --mount=type=cache,target=/var/lib/apt/lists apt-get update && apt-get install python"
-      onBuildRuleCatchesNot "DL3009" "RUN --mount=type=cache,target=/var/lib/apt/lists apt-get update && apt-get install python"
diff --git a/test/DL3010.hs b/test/DL3010.hs
deleted file mode 100644
--- a/test/DL3010.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module DL3010 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "COPY rules" $ do
-    it "use add" $ ruleCatches "DL3010" "COPY packaged-app.tar /usr/src/app"
-    it "use not add" $ ruleCatchesNot "DL3010" "COPY package.json /usr/src/app"
-    it "ignore: copy from previous stage" $ ruleCatchesNot "DL3010" "COPY --from=builder /usr/local/share/some.tar /opt/some.tar"
diff --git a/test/DL3011.hs b/test/DL3011.hs
deleted file mode 100644
--- a/test/DL3011.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module DL3011 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "EXPOSE rules" $ do
-    it "invalid port" $ ruleCatches "DL3011" "EXPOSE 80000"
-    it "valid port" $ ruleCatchesNot "DL3011" "EXPOSE 60000"
diff --git a/test/DL3012.hs b/test/DL3012.hs
deleted file mode 100644
--- a/test/DL3012.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3012 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3012 - Multiple `HEALTHCHECK` instructions" $ do
-    it "ok with no HEALTHCHECK instruction" $
-      ruleCatchesNot "DL3012" "FROM scratch"
-    it "ok with one HEALTHCHECK instruction" $
-      ruleCatchesNot "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla"
-    it "ok with two HEALTHCHECK instructions in two stages" $
-      ruleCatchesNot "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla1\nFROM scratch\nHEALTHCHECK CMD /bin/bla2"
-    it "warn with two HEALTHCHECK instructions" $
-      ruleCatches "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla1\nHEALTHCHECK CMD /bin/bla2"
diff --git a/test/DL3013.hs b/test/DL3013.hs
deleted file mode 100644
--- a/test/DL3013.hs
+++ /dev/null
@@ -1,139 +0,0 @@
-module DL3013 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3013 - Pin versions in pip." $ do
-    it "pip2 version not pinned" $ do
-      ruleCatches "DL3013" "RUN pip2 install MySQL_python"
-      onBuildRuleCatches "DL3013" "RUN pip2 install MySQL_python"
-    it "pip3 version not pinned" $ do
-      ruleCatches "DL3013" "RUN pip3 install MySQL_python"
-      onBuildRuleCatches "DL3013" "RUN pip2 install MySQL_python"
-    it "pip3 version pinned" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install MySQL_python==1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install MySQL_python==1.2.2"
-    it "pip3 install from local package" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install mypkg.whl"
-      ruleCatchesNot "DL3013" "RUN pip3 install mypkg.tar.gz"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install mypkg.whl"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install mypkg.tar.gz"
-    it "pip install requirements" $ do
-      ruleCatchesNot "DL3013" "RUN pip install -r requirements.txt"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install -r requirements.txt"
-    it "pip install requirements with long flag" $ do
-      ruleCatchesNot "DL3013" "RUN pip install --requirement requirements.txt"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install --requirement requirements.txt"
-    it "pip install use setup.py" $ do
-      ruleCatchesNot "DL3013" "RUN pip install ."
-      onBuildRuleCatchesNot "DL3013" "RUN pip install ."
-    it "pip version not pinned" $ do
-      ruleCatches "DL3013" "RUN pip install MySQL_python"
-      onBuildRuleCatches "DL3013" "RUN pip install MySQL_python"
-    it "pip version pinned" $ do
-      ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2"
-    it "pip version pinned with ~= operator" $ do
-      ruleCatchesNot "DL3013" "RUN pip install MySQL_python~=1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python~=1.2.2"
-    it "pip version pinned with === operator" $ do
-      ruleCatchesNot "DL3013" "RUN pip install MySQL_python===1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python===1.2.2"
-    it "pip version pinned with flag --ignore-installed" $ do
-      ruleCatchesNot "DL3013" "RUN pip install --ignore-installed MySQL_python==1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install --ignore-installed MySQL_python==1.2.2"
-    it "pip version pinned with flag --build" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
-    it "pip version pinned with flag --prefix" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
-    it "pip version pinned with flag --root" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
-    it "pip version pinned with flag --target" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
-    it "pip version pinned with flag --trusted-host" $ do
-      ruleCatchesNot "DL3013" "RUN pip3 install --trusted-host host example==1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --trusted-host host example==1.2.2"
-    it "pip version pinned with python -m" $ do
-      ruleCatchesNot "DL3013" "RUN python -m pip install example==1.2.2"
-      onBuildRuleCatchesNot "DL3013" "RUN python -m pip install example==1.2.2"
-    it "pip version not pinned with python -m" $ do
-      ruleCatches "DL3013" "RUN python -m pip install example"
-      onBuildRuleCatches "DL3013" "RUN python -m pip install --index-url url example"
-    it "pip install git" $ do
-      ruleCatchesNot
-        "DL3013"
-        "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
-      onBuildRuleCatchesNot
-        "DL3013"
-        "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
-    it "pip install unversioned git" $ do
-      ruleCatches
-        "DL3013"
-        "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
-      onBuildRuleCatches
-        "DL3013"
-        "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
-    it "pip install upper bound" $ do
-      ruleCatchesNot "DL3013" "RUN pip install 'alabaster>=0.7'"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster>=0.7'"
-    it "pip install lower bound" $ do
-      ruleCatchesNot "DL3013" "RUN pip install 'alabaster<0.7'"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster<0.7'"
-    it "pip install excluded version" $ do
-      ruleCatchesNot "DL3013" "RUN pip install 'alabaster!=0.7'"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster!=0.7'"
-    it "pip install user directory" $ do
-      ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --user"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --user"
-    it "pip install no pip version check" $ do
-      ruleCatchesNot
-        "DL3013"
-        "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
-      onBuildRuleCatchesNot
-        "DL3013"
-        "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
-    it "pip install --index-url" $ do
-      ruleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-      onBuildRuleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-    it "pip install index-url with -i flag" $ do
-      ruleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-      onBuildRuleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-    it "pip install --index-url with --extra-index-url" $ do
-      ruleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
-      onBuildRuleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
-    it "pip install no cache dir" $ do
-      ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
-    it "pip install constraints file - long version argument" $ do
-      ruleCatchesNot "DL3013" "RUN pip install pykafka --constraint http://foo.bar.baz"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install pykafka --constraint http://foo.bar.baz"
-    it "pip install constraints file - short version argument" $ do
-      ruleCatchesNot "DL3013" "RUN pip install pykafka -c http://foo.bar.baz"
-      onBuildRuleCatchesNot "DL3013" "RUN pip install pykafka -c http://foo.bar.baz"
-    it "pip install --index-url with --extra-index-url with basic auth" $ do
-      ruleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
-      onBuildRuleCatchesNot
-        "DL3013"
-        "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
diff --git a/test/DL3014.hs b/test/DL3014.hs
deleted file mode 100644
--- a/test/DL3014.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module DL3014 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3014 - Use the -y switch." $ do
-    it "apt-get auto yes" $ do
-      ruleCatches "DL3014" "RUN apt-get install python"
-      onBuildRuleCatches "DL3014" "RUN apt-get install python"
-    it "apt-get yes shortflag" $ do
-      ruleCatchesNot "DL3014" "RUN apt-get install -yq python"
-      onBuildRuleCatchesNot "DL3014" "RUN apt-get install -yq python"
-    it "apt-get yes quiet level 2 implies -y" $ do
-      ruleCatchesNot "DL3014" "RUN apt-get install -qq python"
-      onBuildRuleCatchesNot "DL3014" "RUN apt-get install -qq python"
-    it "apt-get yes different pos" $ do
-      ruleCatchesNot "DL3014" "RUN apt-get install -y python"
-      onBuildRuleCatchesNot "DL3014" "RUN apt-get install -y python"
-    it "apt-get with auto yes" $ do
-      ruleCatchesNot "DL3014" "RUN apt-get -y install python"
-      onBuildRuleCatchesNot "DL3014" "RUN apt-get -y install python"
-    it "apt-get with auto expanded yes" $ do
-      ruleCatchesNot "DL3014" "RUN apt-get --yes install python"
-      onBuildRuleCatchesNot "DL3014" "RUN apt-get --yes install python"
-    it "apt-get with assume-yes" $ do
-      ruleCatchesNot "DL3014" "RUN apt-get --assume-yes install python"
-      onBuildRuleCatchesNot "DL3014" "RUN apt-get --assume-yes install python"
diff --git a/test/DL3015.hs b/test/DL3015.hs
deleted file mode 100644
--- a/test/DL3015.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module DL3015 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3015 - Avoid additional packages by specifying `--no-install-recommends`." $ do
-    it "apt-get install recommends" $ do
-      ruleCatchesNot
-        "DL3015"
-        "RUN apt-get install --no-install-recommends python"
-      onBuildRuleCatchesNot
-        "DL3015"
-        "RUN apt-get install --no-install-recommends python"
-    it "apt-get no install recommends" $ do
-      ruleCatches "DL3015" "RUN apt-get install python"
-      onBuildRuleCatches "DL3015" "RUN apt-get install python"
-    it "apt-get no install recommends" $ do
-      ruleCatches "DL3015" "RUN apt-get -y install python"
-      onBuildRuleCatches "DL3015" "RUN apt-get -y install python"
-    it "apt-get no install recommends via option" $ do
-      ruleCatchesNot "DL3015" "RUN apt-get -o APT::Install-Recommends=false install python"
-      onBuildRuleCatchesNot "DL3015" "RUN apt-get -o APT::Install-Recommends=false install python"
diff --git a/test/DL3016.hs b/test/DL3016.hs
deleted file mode 100644
--- a/test/DL3016.hs
+++ /dev/null
@@ -1,123 +0,0 @@
-module DL3016 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3016 - Pin versions in npm." $ do
-    it "version pinned in package.json" $ do
-      ruleCatchesNot "DL3016" "RUN npm install"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install"
-    it "version pinned in package.json with arguments" $ do
-      ruleCatchesNot "DL3016" "RUN npm install --progress=false"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install --progress=false"
-    it "version pinned" $ do
-      ruleCatchesNot "DL3016" "RUN npm install express@4.1.1"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install express@4.1.1"
-    it "version pinned with scope" $ do
-      ruleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0\""
-      onBuildRuleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0\""
-    it "version pinned multiple packages" $ do
-      ruleCatchesNot "DL3016" "RUN npm install express@\"4.1.1\" sax@0.1.1"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install express@\"4.1.1\" sax@0.1.1"
-    it "version pinned with --global" $ do
-      ruleCatchesNot "DL3016" "RUN npm install --global express@\"4.1.1\""
-      onBuildRuleCatchesNot "DL3016" "RUN npm install --global express@\"4.1.1\""
-    it "version pinned with -g" $ do
-      ruleCatchesNot "DL3016" "RUN npm install -g express@\"4.1.1\""
-      onBuildRuleCatchesNot "DL3016" "RUN npm install -g express@\"4.1.1\""
-    it "version does not have to be pinned for tarball suffix .tar" $ do
-      ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar"
-    it "version does not have to be pinned for tarball suffix .tar.gz" $ do
-      ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar.gz"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar.gz"
-    it "version does not have to be pinned for tarball suffix .tgz" $ do
-      ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tgz"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tgz"
-    it "version does not have to be pinned for folder - absolute path" $ do
-      ruleCatchesNot "DL3016" "RUN npm install /folder"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install /folder"
-    it "version does not have to be pinned for folder - relative path from current folder" $ do
-      ruleCatchesNot "DL3016" "RUN npm install ./folder"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install ./folder"
-    it "version does not have to be pinned for folder - relative path to parent folder" $ do
-      ruleCatchesNot "DL3016" "RUN npm install ../folder"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install ../folder"
-    it "version does not have to be pinned for folder - relative path from home" $ do
-      ruleCatchesNot "DL3016" "RUN npm install ~/folder"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install ~/folder"
-    it "commit pinned for git+ssh" $ do
-      ruleCatchesNot
-        "DL3016"
-        "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
-      onBuildRuleCatchesNot
-        "DL3016"
-        "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
-    it "commit pinned for git+http" $ do
-      ruleCatchesNot
-        "DL3016"
-        "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
-      onBuildRuleCatchesNot
-        "DL3016"
-        "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
-    it "commit pinned for git+https" $ do
-      ruleCatchesNot
-        "DL3016"
-        "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
-      onBuildRuleCatchesNot
-        "DL3016"
-        "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
-    it "commit pinned for git" $ do
-      ruleCatchesNot
-        "DL3016"
-        "RUN npm install git://github.com/npm/npm.git#v1.0.27"
-      onBuildRuleCatchesNot
-        "DL3016"
-        "RUN npm install git://github.com/npm/npm.git#v1.0.27"
-    it "npm run install is fine" $ do
-      ruleCatchesNot
-        "DL3016"
-        "RUN npm run --crazy install"
-      onBuildRuleCatchesNot
-        "DL3016"
-        "RUN npm run --crazy install"
-
-    --version range is not supported
-    it "version pinned with scope" $ do
-      ruleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
-      onBuildRuleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
-    it "version not pinned" $ do
-      ruleCatches "DL3016" "RUN npm install express"
-      onBuildRuleCatches "DL3016" "RUN npm install express"
-    it "version not pinned with scope" $ do
-      ruleCatches "DL3016" "RUN npm install @myorg/privatepackage"
-      onBuildRuleCatches "DL3016" "RUN npm install @myorg/privatepackage"
-    it "version not pinned multiple packages" $ do
-      ruleCatches "DL3016" "RUN npm install express sax@0.1.1"
-      onBuildRuleCatches "DL3016" "RUN npm install express sax@0.1.1"
-    it "version not pinned with --global" $ do
-      ruleCatches "DL3016" "RUN npm install --global express"
-      onBuildRuleCatches "DL3016" "RUN npm install --global express"
-    it "commit not pinned for git+ssh" $ do
-      ruleCatches "DL3016" "RUN npm install git+ssh://git@github.com:npm/npm.git"
-      onBuildRuleCatches "DL3016" "RUN npm install git+ssh://git@github.com:npm/npm.git"
-    it "commit not pinned for git+http" $ do
-      ruleCatches "DL3016" "RUN npm install git+http://isaacs@github.com/npm/npm"
-      onBuildRuleCatches "DL3016" "RUN npm install git+http://isaacs@github.com/npm/npm"
-    it "commit not pinned for git+https" $ do
-      ruleCatches
-        "DL3016"
-        "RUN npm install git+https://isaacs@github.com/npm/npm.git"
-      onBuildRuleCatches
-        "DL3016"
-        "RUN npm install git+https://isaacs@github.com/npm/npm.git"
-    it "commit not pinned for git" $ do
-      ruleCatches "DL3016" "RUN npm install git://github.com/npm/npm.git"
-      onBuildRuleCatches "DL3016" "RUN npm install git://github.com/npm/npm.git"
-    it "don't fire on loglevel flag" $ do
-      ruleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
-      onBuildRuleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
diff --git a/test/DL3018.hs b/test/DL3018.hs
deleted file mode 100644
--- a/test/DL3018.hs
+++ /dev/null
@@ -1,90 +0,0 @@
-module DL3018 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3018 - Pin versions in `apk add`." $ do
-    it "apk add version pinning single" $ do
-      ruleCatches "DL3018" "RUN apk add flex"
-      onBuildRuleCatches "DL3018" "RUN apk add flex"
-    it "apk add no version pinning single" $ do
-      ruleCatchesNot "DL3018" "RUN apk add flex=2.6.4-r1"
-      onBuildRuleCatchesNot "DL3018" "RUN apk add flex=2.6.4-r1"
-    it "apk add version pinned chained" $
-      let dockerFile =
-            [ "RUN apk add --no-cache flex=2.6.4-r1 \\",
-              " && pip install -r requirements.txt"
-            ]
-       in do
-            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
-    it "apk add version pinned regression" $
-      let dockerFile =
-            [ "RUN apk add --no-cache \\",
-              "flex=2.6.4-r1 \\",
-              "libffi=3.2.1-r3 \\",
-              "python2=2.7.13-r1 \\",
-              "libbz2=1.0.6-r5"
-            ]
-       in do
-            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
-    it "apk add version pinned regression - one missed" $
-      let dockerFile =
-            [ "RUN apk add --no-cache \\",
-              "flex=2.6.4-r1 \\",
-              "libffi \\",
-              "python2=2.7.13-r1 \\",
-              "libbz2=1.0.6-r5"
-            ]
-       in do
-            ruleCatches "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3018" $ Text.unlines dockerFile
-    it "apk add virtual package" $
-      let dockerFile =
-            [ "RUN apk add \\",
-              "--virtual build-dependencies \\",
-              "python-dev=1.1.1 build-base=2.2.2 wget=3.3.3 \\",
-              "&& pip install -r requirements.txt \\",
-              "&& python setup.py install \\",
-              "&& apk del build-dependencies"
-            ]
-       in do
-            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
-    it "apk add with repository without equal sign" $
-      let dockerFile =
-            [ "RUN apk add --no-cache \\",
-              "--repository https://nl.alpinelinux.org/alpine/edge/testing \\",
-              "flow=0.78.0-r0"
-            ]
-       in do
-            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
-    it "apk add with repository with equal sign" $
-      let dockerFile =
-            [ "RUN apk add --no-cache \\",
-              "--repository=https://nl.alpinelinux.org/alpine/edge/testing \\",
-              "flow=0.78.0-r0"
-            ]
-       in do
-            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
-    it "apk add with repository (-X) without equal sign" $
-      let dockerFile =
-            [ "RUN apk add --no-cache \\",
-              "-X https://nl.alpinelinux.org/alpine/edge/testing \\",
-              "flow=0.78.0-r0"
-            ]
-       in do
-            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
-            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
-
-    it "don't trigger when installing from .apk file" $ do
-      ruleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"
-      onBuildRuleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"
diff --git a/test/DL3019.hs b/test/DL3019.hs
deleted file mode 100644
--- a/test/DL3019.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module DL3019 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3019 - Use the --no-cache switch." $ do
-    it "apk add with --no-cache" $ do
-      ruleCatches "DL3019" "RUN apk add flex=2.6.4-r1"
-      onBuildRuleCatches "DL3019" "RUN apk add flex=2.6.4-r1"
-    it "apk add without --no-cache" $ do
-      ruleCatchesNot "DL3019" "RUN apk add --no-cache flex=2.6.4-r1"
-      onBuildRuleCatchesNot "DL3019" "RUN apk add --no-cache flex=2.6.4-r1"
-    it "don't warn: apk add with BuildKit cache mount" $ do
-      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"
-      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"
-    it "don't warn: apk add with BuildKit cache mount in wrong dir and --no-cache" $ do
-      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"
-      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"
-    it "warn: apk add with BuildKit cache mount to wrong dir" $ do
-      ruleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"
-      onBuildRuleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"
diff --git a/test/DL3020.hs b/test/DL3020.hs
deleted file mode 100644
--- a/test/DL3020.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3020 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3020 - Use `COPY` instead of `ADD` for files and folders." $ do
-    it "add for tar" $ ruleCatchesNot "DL3020" "ADD file.tar /usr/src/app/"
-    it "add for gzip" $ ruleCatchesNot "DL3020" "ADD file.gz /usr/src/app/"
-    it "add for bz2" $ ruleCatchesNot "DL3020" "ADD file.bz2 /usr/src/app/"
-    it "add for xz" $ ruleCatchesNot "DL3020" "ADD file.xz /usr/src/app/"
-    it "add for tgz" $ ruleCatchesNot "DL3020" "ADD file.tgz /usr/src/app/"
-    it "add for url" $ ruleCatchesNot "DL3020" "ADD http://file.com /usr/src/app/"
-    it "using add" $ ruleCatches "DL3020" "ADD file /usr/src/app/"
-    it "warn for zip" $ ruleCatches "DL3020" "ADD file.zip /usr/src/app/"
diff --git a/test/DL3021.hs b/test/DL3021.hs
deleted file mode 100644
--- a/test/DL3021.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module DL3021 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3021 - `COPY` with more than 2 arguments requires the last argument to end with `/`" $ do
-    it "no warn on 2 args" $ ruleCatchesNot "DL3021" "COPY foo bar"
-    it "warn on 3 args" $ ruleCatches "DL3021" "COPY foo bar baz"
-    it "no warn on 3 args" $ ruleCatchesNot "DL3021" "COPY foo bar baz/"
diff --git a/test/DL3022.hs b/test/DL3022.hs
deleted file mode 100644
--- a/test/DL3022.hs
+++ /dev/null
@@ -1,29 +0,0 @@
-module DL3022 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3022 - `COPY --from` should reference a previously defined `FROM` alias" $ do
-    it "warn on missing alias" $ ruleCatches "DL3022" "COPY --from=foo bar ."
-    it "warn on alias defined after" $
-      let dockerFile =
-            [ "FROM scratch",
-              "COPY --from=build foo .",
-              "FROM node as build",
-              "RUN baz"
-            ]
-       in ruleCatches "DL3022" $ Text.unlines dockerFile
-    it "don't warn on correctly defined aliases" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "RUN foo",
-              "FROM node",
-              "COPY --from=build foo .",
-              "RUN baz"
-            ]
-       in ruleCatchesNot "DL3022" $ Text.unlines dockerFile
diff --git a/test/DL3023.hs b/test/DL3023.hs
deleted file mode 100644
--- a/test/DL3023.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module DL3023 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3023 - `COPY --from` cannot reference its own `FROM` alias" $ do
-    it "warn on copying from your the same FROM" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "COPY --from=foo bar ."
-            ]
-       in ruleCatches "DL3023" $ Text.unlines dockerFile
-    it "don't warn on copying from other sources" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "RUN foo",
-              "FROM node as run",
-              "COPY --from=build foo .",
-              "RUN baz"
-            ]
-       in ruleCatchesNot "DL3023" $ Text.unlines dockerFile
diff --git a/test/DL3024.hs b/test/DL3024.hs
deleted file mode 100644
--- a/test/DL3024.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module DL3024 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3024 - Duplicate aliases" $ do
-    it "warn on duplicate aliases" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN something",
-              "FROM scratch as foo",
-              "RUN something"
-            ]
-       in ruleCatches "DL3024" $ Text.unlines dockerFile
-    it "don't warn on unique aliases" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "RUN foo",
-              "FROM node as run",
-              "RUN baz"
-            ]
-       in ruleCatchesNot "DL3024" $ Text.unlines dockerFile
diff --git a/test/DL3025.hs b/test/DL3025.hs
deleted file mode 100644
--- a/test/DL3025.hs
+++ /dev/null
@@ -1,36 +0,0 @@
-module DL3025 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3025 - Use arguments JSON notation for `CMD` and `ENTRYPOINT` arguments" $ do
-    it "warn on ENTRYPOINT" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "ENTRYPOINT something"
-            ]
-       in ruleCatches "DL3025" $ Text.unlines dockerFile
-    it "don't warn on ENTRYPOINT json notation" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "ENTRYPOINT [\"foo\", \"bar\"]"
-            ]
-       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
-    it "warn on CMD" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "CMD something"
-            ]
-       in ruleCatches "DL3025" $ Text.unlines dockerFile
-    it "don't warn on CMD json notation" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "CMD [\"foo\", \"bar\"]",
-              "CMD [ \"foo\", \"bar\" ]"
-            ]
-       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
diff --git a/test/DL3026.hs b/test/DL3026.hs
deleted file mode 100644
--- a/test/DL3026.hs
+++ /dev/null
@@ -1,56 +0,0 @@
-module DL3026 (tests) where
-
-import Data.Map as Map
-import Data.Text as Text
-import Hadolint.Process
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL2036 - Use only an allowed registry in the FROM image" $ do
-    it "does not warn on empty allowed registries" $ do
-      let dockerFile =
-            [ "FROM random.com/debian"
-            ]
-      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
-
-    it "warn on non-allowed registry" $ do
-      let dockerFile =
-            [ "FROM random.com/debian"
-            ]
-      let ?rulesConfig = Hadolint.Process.RulesConfig ["docker.io"] Map.empty False
-      ruleCatches "DL3026" $ Text.unlines dockerFile
-
-    it "does not warn on allowed registries" $ do
-      let dockerFile =
-            [ "FROM random.com/debian"
-            ]
-      let ?rulesConfig = Hadolint.Process.RulesConfig ["x.com", "random.com"] Map.empty False
-      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
-
-    it "doesn't warn on scratch image" $ do
-      let dockerFile =
-            [ "FROM scratch"
-            ]
-      let ?rulesConfig = Hadolint.Process.RulesConfig ["x.com", "random.com"] Map.empty False
-      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
-
-    it "allows boths all forms of docker.io" $ do
-      let dockerFile =
-            [ "FROM ubuntu:18.04 AS builder1",
-              "FROM zemanlx/ubuntu:18.04 AS builder2",
-              "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
-            ]
-      let ?rulesConfig = Hadolint.Process.RulesConfig ["docker.io"] Map.empty False
-      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
-
-    it "allows using previous stages" $ do
-      let dockerFile =
-            [ "FROM random.com/foo AS builder1",
-              "FROM builder1 AS builder2"
-            ]
-      let ?rulesConfig = Hadolint.Process.RulesConfig ["random.com"] Map.empty False
-      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
diff --git a/test/DL3027.hs b/test/DL3027.hs
deleted file mode 100644
--- a/test/DL3027.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module DL3027 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3027 - Do not use `apt` as it is meant to be a end-user tool, use `apt-get` or `apt-cache` instead" $ do
-    it "apt" $
-      let dockerFile =
-            [ "FROM ubuntu",
-              "RUN apt install python"
-            ]
-       in do
-            ruleCatches "DL3027" $ Text.unlines dockerFile
-            onBuildRuleCatches "DL3027" $ Text.unlines dockerFile
diff --git a/test/DL3028.hs b/test/DL3028.hs
deleted file mode 100644
--- a/test/DL3028.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module DL3028 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3028 - Pin versions in gem install." $
-    describe "version pinning" $ do
-      describe "i" $ do
-        it "unpinned" $ do
-          ruleCatches "DL3028" "RUN gem i bundler"
-          onBuildRuleCatches "DL3028" "RUN gem i bundler"
-        it "pinned" $ do
-          ruleCatchesNot "DL3028" "RUN gem i bundler:1"
-          onBuildRuleCatchesNot "DL3028" "RUN gem i bundler:1"
-        it "multi" $ do
-          ruleCatches "DL3028" "RUN gem i bunlder:1 nokogiri"
-          onBuildRuleCatches "DL3028" "RUN gem i bunlder:1 nokogiri"
-          ruleCatchesNot "DL3028" "RUN gem i bunlder:1 nokogirii:1"
-          onBuildRuleCatchesNot "DL3028" "RUN gem i bunlder:1 nokogiri:1"
-      describe "install" $ do
-        it "unpinned" $ do
-          ruleCatches "DL3028" "RUN gem install bundler"
-          onBuildRuleCatches "DL3028" "RUN gem install bundler"
-        it "pinned" $ do
-          ruleCatchesNot "DL3028" "RUN gem install bundler:1"
-          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler:1"
-        it "does not warn on -v" $ do
-          ruleCatchesNot "DL3028" "RUN gem install bundler -v '2.0.1'"
-          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler -v '2.0.1'"
-        it "does not warn on --version without =" $ do
-          ruleCatchesNot "DL3028" "RUN gem install bundler --version '2.0.1'"
-          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler --version '2.0.1'"
-        it "does not warn on --version with =" $ do
-          ruleCatchesNot "DL3028" "RUN gem install bundler --version='2.0.1'"
-          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler --version='2.0.1'"
-        it "does not warn on extra flags" $ do
-          ruleCatchesNot "DL3028" "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
-          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
diff --git a/test/DL3029.hs b/test/DL3029.hs
deleted file mode 100644
--- a/test/DL3029.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-module DL3029 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3029 - Do not use --platform flag with FROM." $ do
-    it "explicit platform flag" $ ruleCatches "DL3029" "FROM --platform=linux debian:jessie"
-    it "no platform flag" $ ruleCatchesNot "DL3029" "FROM debian:jessie"
diff --git a/test/DL3030.hs b/test/DL3030.hs
deleted file mode 100644
--- a/test/DL3030.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3030 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3030 - Use the `-y` switch to avoid manual input `yum install -y <package>`" $ do
-    it "not ok when not using `-y` switch" $ do
-      ruleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"
-      onBuildRuleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"
-    it "ok when using `-y` switch" $ do
-      ruleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"
-      ruleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
-      onBuildRuleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"
-      onBuildRuleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
diff --git a/test/DL3032.hs b/test/DL3032.hs
deleted file mode 100644
--- a/test/DL3032.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3032 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3032 - `yum clean all` missing after yum command." $ do
-    it "not ok with no clean all" $ do
-      ruleCatches "DL3032" "RUN yum install -y mariadb-10.4"
-      onBuildRuleCatches "DL3032" "RUN yum install -y mariadb-10.4"
-    it "ok with yum clean all " $ do
-      ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
-      ruleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
-      onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
-      onBuildRuleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
diff --git a/test/DL3033.hs b/test/DL3033.hs
deleted file mode 100644
--- a/test/DL3033.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3033 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3033 - Specify version with `yum install -y <package>-<version>`" $ do
-    it "not ok wihout yum version pinning" $ do
-      ruleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"
-      onBuildRuleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"
-    it "ok with yum version pinning" $ do
-      ruleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"
-      ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
-      onBuildRuleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"
-      onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
diff --git a/test/DL3034.hs b/test/DL3034.hs
deleted file mode 100644
--- a/test/DL3034.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3034 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3034 - Non-interactive switch missing from zypper command: `zypper install -y`" $ do
-    it "not ok without non-interactive switch" $ do
-      ruleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"
-      onBuildRuleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"
-    it "ok with non-interactive switch present" $ do
-      ruleCatchesNot "DL3034" "RUN zypper install -y httpd=2.4.24 && zypper clean"
-      ruleCatchesNot "DL3034" "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"
-      onBuildRuleCatchesNot "DL3034" "RUN zypper install -y httpd=2.4.24 && zypper clean"
-      onBuildRuleCatchesNot "DL3034" "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"
diff --git a/test/DL3035.hs b/test/DL3035.hs
deleted file mode 100644
--- a/test/DL3035.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module DL3035 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3035" $ do
-    it "not ok: zypper dist-upgrade" $ do
-      ruleCatches "DL3035" "RUN zypper dist-upgrade"
-      ruleCatches "DL3035" "RUN zypper dup"
-      onBuildRuleCatches "DL3035" "RUN zypper dist-upgrade"
-      onBuildRuleCatches "DL3035" "RUN zypper dup"
diff --git a/test/DL3036.hs b/test/DL3036.hs
deleted file mode 100644
--- a/test/DL3036.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3036 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3036 - `zypper clean` missing after zypper use." $ do
-    it "not ok without zypper clean" $ do
-      ruleCatches "DL3036" "RUN zypper install -y mariadb=10.4"
-      onBuildRuleCatches "DL3036" "RUN zypper install -y mariadb=10.4"
-    it "ok with zypper clean" $ do
-      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
-      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
-      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
-      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
diff --git a/test/DL3037.hs b/test/DL3037.hs
deleted file mode 100644
--- a/test/DL3037.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-module DL3037 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3037 - Specify version with `zypper install -y <package>[=]<version>`." $ do
-    -- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'
-    -- and '<' have to be escaped. Hence the double escaping.
-    it "not ok without zypper version pinning" $ do
-      ruleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"
-      onBuildRuleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"
-    it "ok with different variants of zypper version pinning" $ do
-      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat=9.0.39 && zypper clean"
-      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>=9.0 && zypper clean"
-      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>9.0 && zypper clean"
-      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<=9.0 && zypper clean"
-      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<9.0 && zypper clean"
-      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"
-      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat=9.0.39 && zypper clean"
-      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>=9.0 && zypper clean"
-      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>9.0 && zypper clean"
-      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<=9.0 && zypper clean"
-      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<9.0 && zypper clean"
-      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"
diff --git a/test/DL3038.hs b/test/DL3038.hs
deleted file mode 100644
--- a/test/DL3038.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3038 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3038 - Use the `-y` switch to avoid manual input `dnf install -y <package>`" $ do
-    it "not ok without dnf non-interactive flag" $ do
-      ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
-      onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
-    it "ok with dnf non-interactive flag" $ do
-      ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-      ruleCatchesNot "DL3038" "RUN notdnf install httpd"
-      onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-      onBuildRuleCatchesNot "DL3038" "RUN notdnf install httpd"
diff --git a/test/DL3040.hs b/test/DL3040.hs
deleted file mode 100644
--- a/test/DL3040.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3040 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3040 - `dnf clean all` missing after dnf command." $ do
-    it "no ok without dnf clean all" $ do
-      ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
-      onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
-    it "ok with dnf clean all" $ do
-      ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
-      ruleCatchesNot "DL3040" "RUN notdnf install mariadb"
-      onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
-      onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"
diff --git a/test/DL3041.hs b/test/DL3041.hs
deleted file mode 100644
--- a/test/DL3041.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3041 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3041 - Specify version with `dnf install -y <package>-<version>`" $ do
-    it "not ok without dnf version pinning" $ do
-      ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
-      onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
-    it "ok with dnf version pinning" $ do
-      ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-      ruleCatchesNot "DL3041" "RUN notdnf install tomcat"
-      onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-      onBuildRuleCatchesNot "DL3041" "RUN notdnf install tomcat"
diff --git a/test/DL3042.hs b/test/DL3042.hs
deleted file mode 100644
--- a/test/DL3042.hs
+++ /dev/null
@@ -1,86 +0,0 @@
-module DL3042 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3042 - Avoid cache directory with `pip install --no-cache-dir <package>`." $ do
-    it "pip2 --no-cache-dir not used" $ do
-      ruleCatches "DL3042" "RUN pip2 install MySQL_python"
-      onBuildRuleCatches "DL3042" "RUN pip2 install MySQL_python"
-    it "pip3 --no-cache-dir not used" $ do
-      ruleCatches "DL3042" "RUN pip3 install MySQL_python"
-      onBuildRuleCatches "DL3042" "RUN pip3 install MySQL_python"
-    it "pip --no-cache-dir not used" $ do
-      ruleCatches "DL3042" "RUN pip install MySQL_python"
-      onBuildRuleCatches "DL3042" "RUN pip install MySQL_python"
-    it "pip2 --no-cache-dir used" $ do
-      ruleCatchesNot "DL3042" "RUN pip2 install MySQL_python --no-cache-dir"
-      onBuildRuleCatchesNot "DL3042" "RUN pip2 install MySQL_python --no-cache-dir"
-    it "pip3 --no-cache-dir used" $ do
-      ruleCatchesNot "DL3042" "RUN pip3 install --no-cache-dir MySQL_python"
-      onBuildRuleCatchesNot "DL3042" "RUN pip3 install --no-cache-dir MySQL_python"
-    it "pip --no-cache-dir used" $ do
-      ruleCatchesNot "DL3042" "RUN pip install MySQL_python --no-cache-dir"
-      onBuildRuleCatchesNot "DL3042" "RUN pip install MySQL_python --no-cache-dir"
-    it "don't match on pipx" $ do
-      ruleCatchesNot "DL3042" "RUN pipx install software"
-      onBuildRuleCatchesNot "DL3042" "Run pipx install software"
-    it "don't match on pipenv" $ do
-      ruleCatchesNot "DL3042" "RUN pipenv install library"
-      onBuildRuleCatchesNot "DL3042" "RUN pipenv install library"
-
-    -- ENV won't work with the onBuildRuleCatches[Not] wrapper:
-    it "respect ENV PIP_NO_CACHE_DIR with truthy values" $ do
-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=1\nRUN pip install MySQL_python"
-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=on\nRUN pip install MySQL_python"
-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=yes\nRUN pip install MySQL_python"
-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=true\nRUN pip install MySQL_python"
-    it "respect RUN PIP_NO_CACHE_DIR=... with truthy values" $ do
-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=1 pip install MySQL_python"
-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=on pip install MySQL_python"
-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=yes pip install MySQL_python"
-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=true pip install MySQL_python"
-    it "respect RUN export PIP_NO_CACHE_DIR=... with truthy values" $ do
-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=1 && pip install MySQL_python"
-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=on && pip install MySQL_python"
-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=yes && pip install MySQL_python"
-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=true && pip install MySQL_python"
-    it "respect ENV PIP_NO_CACHE_DIR with falsy values" $ do
-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=0\nRUN pip install MySQL_python"
-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=off\nRUN pip install MySQL_python"
-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=no\nRUN pip install MySQL_python"
-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=false\nRUN pip install MySQL_python"
-    it "respect RUN PIP_NO_CACHE_DIR=... with falsy values" $ do
-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=0 pip install MySQL_python"
-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=off pip install MySQL_python"
-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=no pip install MySQL_python"
-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=false pip install MySQL_python"
-    it "respect RUN export PIP_NO_CACHE_DIR=... with falsy values" $ do
-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=0 && pip install MySQL_python"
-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=off && pip install MySQL_python"
-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=no && pip install MySQL_python"
-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=false && pip install MySQL_python"
-
-    it "don't trigger if PIP_NO_CACHE_DIR is inherited" $
-      let dockerFile = Text.unlines
-            [ "FROM debian:buster as base",
-              "ENV PIP_NO_CACHE_DIR=1",
-              "FROM base",
-              "RUN pip install six"
-            ]
-       in do
-        ruleCatchesNot "DL3042" dockerFile
-    it "trigger if PIP_NO_CACHE_DIR is not inherited" $
-      let dockerFile = Text.unlines
-            [ "FROM debian:buster as base",
-              "ENV PIP_NO_CACHE_DIR=1",
-              "FROM debian:buster",
-              "RUN pip install six"
-            ]
-       in do
-        ruleCatches "DL3042" dockerFile
diff --git a/test/DL3043.hs b/test/DL3043.hs
deleted file mode 100644
--- a/test/DL3043.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-module DL3043 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3043 - `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction." $ do
-    it "error when using `ONBUILD` within `ONBUILD`" $
-      let dockerFile =
-            [ "ONBUILD ONBUILD RUN anything"
-            ]
-       in ruleCatches "DL3043" $ Text.unlines dockerFile
-    it "error when using `FROM` within `ONBUILD`" $
-      let dockerFile =
-            [ "ONBUILD FROM debian:buster"
-            ]
-       in ruleCatches "DL3043" $ Text.unlines dockerFile
-    it "error when using `MAINTAINER` within `ONBUILD`" $
-      let dockerFile =
-            [ "ONBUILD MAINTAINER \"BoJack Horseman\""
-            ]
-       in ruleCatches "DL3043" $ Text.unlines dockerFile
-    it "ok with `ADD`" $ ruleCatchesNot "DL3043" "ONBUILD ADD anything anywhere"
-    it "ok with `USER`" $ ruleCatchesNot "DL3043" "ONBUILD USER anything"
-    it "ok with `LABEL`" $ ruleCatchesNot "DL3043" "ONBUILD LABEL bla=\"blubb\""
-    it "ok with `STOPSIGNAL`" $ ruleCatchesNot "DL3043" "ONBUILD STOPSIGNAL anything"
-    it "ok with `COPY`" $ ruleCatchesNot "DL3043" "ONBUILD COPY anything anywhere"
-    it "ok with `RUN`" $ ruleCatchesNot "DL3043" "ONBUILD RUN anything"
-    it "ok with `CMD`" $ ruleCatchesNot "DL3043" "ONBUILD CMD anything"
-    it "ok with `SHELL`" $ ruleCatchesNot "DL3043" "ONBUILD SHELL anything"
-    it "ok with `WORKDIR`" $ ruleCatchesNot "DL3043" "ONBUILD WORKDIR anything"
-    it "ok with `EXPOSE`" $ ruleCatchesNot "DL3043" "ONBUILD EXPOSE 69"
-    it "ok with `VOLUME`" $ ruleCatchesNot "DL3043" "ONBUILD VOLUME anything"
-    it "ok with `ENTRYPOINT`" $ ruleCatchesNot "DL3043" "ONBUILD ENTRYPOINT anything"
-    it "ok with `ENV`" $ ruleCatchesNot "DL3043" "ONBUILD ENV MYVAR=\"bla\""
-    it "ok with `ARG`" $ ruleCatchesNot "DL3043" "ONBUILD ARG anything"
-    it "ok with `HEALTHCHECK`" $ ruleCatchesNot "DL3043" "ONBUILD HEALTHCHECK NONE"
-    it "ok with `FROM` outside of `ONBUILD`" $ ruleCatchesNot "DL3043" "FROM debian:buster"
-    it "ok with `MAINTAINER` outside of `ONBUILD`" $ ruleCatchesNot "DL3043" "MAINTAINER \"Some Guy\""
diff --git a/test/DL3044.hs b/test/DL3044.hs
deleted file mode 100644
--- a/test/DL3044.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module DL3044 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3044 - Do not refer to an environment variable within the same `ENV` statement where it is defined." $ do
-    it "ok with normal ENV" $
-      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\"\nENV BLUBB=\"${BLA}/blubb\""
-    it "ok with partial match 1" $
-      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${FOOBLA}/blubb\""
-    it "ok with partial match 2" $
-      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLAFOO}/blubb\""
-    it "ok with partial match 3" $
-      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$FOOBLA/blubb\""
-    it "ok with partial match 4" $
-      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLAFOO/blubb\""
-    it "fail with partial match 5" $
-      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA/$BLAFOO/blubb\""
-    it "ok with parial match 6" $
-      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"BLA/$BLAFOO/BLA\""
-    it "ok when previously defined in `ARG`" $
-      ruleCatchesNot "DL3044" "ARG BLA\nENV BLA=${BLA}"
-    it "ok when previously defined in `ENV`" $
-      ruleCatchesNot "DL3044" "ENV BLA blubb\nENV BLA=${BLA}"
-    it "ok with referencing a variable on its own right hand side" $
-      ruleCatchesNot "DL3044" "ENV PATH=/bla:${PATH}"
-    it "ok with referencing a variable on its own right side twice in different `ENV`s" $
-      ruleCatchesNot "DL3044" "ENV PATH=/bla:${PATH}\nENV PATH=/blubb:${PATH}"
-    it "fail when referencing a variable on its own right side twice within the same `ENV`" $
-      ruleCatches "DL3044" "ENV PATH=/bla:${PATH} PATH=/blubb:${PATH}"
-    it "fail with selfreferencing with curly braces ENV" $
-      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLA}/blubb\""
-    it "fail with selfreferencing without curly braces ENV" $
-      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA/blubb\""
-    it "fail with full match 1" $
-      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA\""
-    it "fail with full match 2" $
-      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLA}\""
diff --git a/test/DL3045.hs b/test/DL3045.hs
deleted file mode 100644
--- a/test/DL3045.hs
+++ /dev/null
@@ -1,156 +0,0 @@
-module DL3045 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3045 - `COPY` without `WORKDIR` set" $ do
-    it "ok: `COPY` with absolute destination and no `WORKDIR` set" $ do
-      ruleCatchesNot "DL3045" "COPY bla.sh /usr/local/bin/blubb.sh"
-      onBuildRuleCatchesNot "DL3045" "COPY bla.sh /usr/local/bin/blubb.sh"
-
-    it "ok: `COPY` with absolute destination and no `WORKDIR` set with quotes" $ do
-      ruleCatchesNot "DL3045" "COPY bla.sh \"/usr/local/bin/blubb.s\""
-      onBuildRuleCatchesNot "DL3045" "COPY bla.sh \"/usr/local/bin/blubb.sh\""
-
-    it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows" $ do
-      ruleCatchesNot "DL3045" "COPY bla.sh c:\\system32\\blubb.sh"
-      onBuildRuleCatchesNot "DL3045" "COPY bla.sh d:\\mypath\\blubb.sh"
-
-    it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows with quotes" $ do
-      ruleCatchesNot "DL3045" "COPY bla.sh \"c:\\system32\\blubb.sh\""
-      onBuildRuleCatchesNot "DL3045" "COPY bla.sh \"d:\\mypath\\blubb.sh\""
-
-    it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows with alternative paths" $ do
-      ruleCatchesNot "DL3045" "COPY bla.sh c:/system32/blubb.sh"
-      onBuildRuleCatchesNot "DL3045" "COPY bla.sh d:/mypath/blubb.sh"
-
-    it "ok: `COPY` with relative destination and `WORKDIR` set" $ do
-      ruleCatchesNot "DL3045" "FROM scratch\nWORKDIR /usr\nCOPY bla.sh blubb.sh"
-      onBuildRuleCatchesNot "FROM scratch\nDL3045" "WORKDIR /usr\nCOPY bla.sh blubb.sh"
-
-    it "ok: `COPY` with relative destination and `WORKDIR` set - windows" $ do
-      ruleCatchesNot "DL3045" "FROM scratch\nWORKDIR c:\\system32\nCOPY bla.sh blubb.sh"
-      onBuildRuleCatchesNot "DL3045" "FROM scratch\nWORKDIR c:\\system32\nCOPY bla.sh blubb.sh"
-
-    it "ok: `COPY` with destination being an environment variable 1" $ do
-      ruleCatchesNot "DL3045" "COPY src.sh ${SRC_BASE_ENV}"
-      onBuildRuleCatchesNot "DL3045" "COPY src.sh ${SRC_BASE_ENV}"
-
-    it "ok: `COPY` with destination being an environment variable 2" $ do
-      ruleCatchesNot "DL3045" "COPY src.sh $SRC_BASE_ENV"
-      onBuildRuleCatchesNot "DL3045" "COPY src.sh $SRC_BASE_ENV"
-
-    it "ok: `COPY` with destination being an environment variable 3" $ do
-      ruleCatchesNot "DL3045" "COPY src.sh \"${SRC_BASE_ENV}\""
-      onBuildRuleCatchesNot "DL3045" "COPY src.sh \"${SRC_BASE_ENV}\""
-
-    it "ok: `COPY` with destination being an environment variable 4" $ do
-      ruleCatchesNot "DL3045" "COPY src.sh \"$SRC_BASE_ENV\""
-      onBuildRuleCatchesNot "DL3045" "COPY src.sh \"$SRC_BASE_ENV\""
-
-    it "not ok: `COPY` with relative destination and no `WORKDIR` set" $ do
-      ruleCatches "DL3045" "COPY bla.sh blubb.sh"
-      onBuildRuleCatches "DL3045" "COPY bla.sh blubb.sh"
-
-    it "not ok: `COPY` with relative destination and no `WORKDIR` set with quotes" $ do
-      ruleCatches "DL3045" "COPY bla.sh \"blubb.sh\""
-      onBuildRuleCatches "DL3045" "COPY bla.sh \"blubb.sh\""
-
-    it "not ok: `COPY` to relative destination if `WORKDIR` is set in a previous stage but not inherited" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM debian:buster as stage1",
-                "WORKDIR /usr",
-                "FROM debian:buster",
-                "COPY foo bar"
-              ]
-       in do
-            ruleCatches "DL3045" dockerFile
-            onBuildRuleCatches "DL3045" dockerFile
-
-    it "not ok: `COPY` to relative destination if `WORKDIR` is set in a previous stage but not inherited - windows" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM microsoft/windowsservercore as stage1",
-                "WORKDIR c:\\system32",
-                "FROM microsoft/windowsservercore",
-                "COPY foo bar"
-              ]
-       in do
-            ruleCatches "DL3045" dockerFile
-            onBuildRuleCatches "DL3045" dockerFile
-
-    it "ok: `COPY` to relative destination if `WORKDIR` has been set in base image" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM debian:buster as base",
-                "WORKDIR /usr",
-                "FROM debian:buster as stage-inbetween",
-                "RUN foo",
-                "FROM base",
-                "COPY foo bar"
-              ]
-       in do
-            ruleCatchesNot "DL3045" dockerFile
-            onBuildRuleCatchesNot "DL3045" dockerFile
-
-    it "ok: `COPY` to relative destination if `WORKDIR` has been set in base image - windows" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM microsoft/windowsservercore as base",
-                "WORKDIR c:\\system32",
-                "FROM microsoft/windowsservercore as stage-inbetween",
-                "RUN foo",
-                "FROM base",
-                "COPY foo bar"
-              ]
-       in do
-            ruleCatchesNot "DL3045" dockerFile
-            onBuildRuleCatchesNot "DL3045" dockerFile
-
-    it "ok: `COPY` to relative destination if `WORKDIR` has been set in previous stage, deep case" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM debian:buster as base1",
-                "WORKDIR /usr",
-                "FROM base1 as base2",
-                "RUN foo",
-                "FROM base2",
-                "COPY foo bar"
-              ]
-       in do
-            ruleCatchesNot "DL3045" dockerFile
-            onBuildRuleCatchesNot "DL3045" dockerFile
-
-    it "ok: `COPY` to relative destination if `WORKDIR` has been set in previous stage, deep case - windows" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM microsoft/windowsservercore as base1",
-                "WORKDIR c:\\system32",
-                "FROM base1 as base2",
-                "RUN foo",
-                "FROM base2",
-                "COPY foo bar"
-              ]
-       in do
-            ruleCatchesNot "DL3045" dockerFile
-            onBuildRuleCatchesNot "DL3045" dockerFile
-
-    it "ok: `COPY` to relative destination if `WORKDIR` has been set, both within an `ONBUILD` context" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM debian:buster",
-                "ONBUILD WORKDIR /usr/local/lib",
-                "ONBUILD COPY foo bar"
-              ]
-       in do
-            ruleCatchesNot "DL3045" dockerFile
-            onBuildRuleCatchesNot "DL3045" dockerFile
-
-    it "regression: don't crash with single character paths" $ do
-      ruleCatches "DL3045" "COPY a b"
-      onBuildRuleCatches "DL3045" "COPY a b"
diff --git a/test/DL3046.hs b/test/DL3046.hs
deleted file mode 100644
--- a/test/DL3046.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module DL3046 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3046 - `useradd` without flag `-l` and high UID will result in excessively large Image." $ do
-    it "ok with `useradd` alone" $ ruleCatchesNot "DL3046" "RUN useradd luser"
-    it "ok with `useradd` short uid" $ ruleCatchesNot "DL3046" "RUN useradd -u 12345 luser"
-    it "ok with `useradd` long uid and flag `-l`" $ ruleCatchesNot "DL3046" "RUN useradd -l -u 123456 luser"
-    it "ok with `useradd` and just flag `-l`" $ ruleCatchesNot "DL3046" "RUN useradd -l luser"
-    it "warn when `useradd` and long uid without flag `-l`" $ ruleCatches "DL3046" "RUN useradd -u 123456 luser"
diff --git a/test/DL3047.hs b/test/DL3047.hs
deleted file mode 100644
--- a/test/DL3047.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-module DL3047 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3047 - `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files." $ do
-    it "warns when using wget without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz"
-            ]
-       in ruleCatches "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget --progress=dot:giga my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with -q (quiet short option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget -q my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with --quiet (quiet long option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget --quiet my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with -nv (no-verbose short option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget -nv my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with --no-verbose (no-verbose long option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget --no-verbose my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with --output-file (output-file long option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget --output-file=/tmp/wget.log my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with -o (output-file long option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget -o /tmp/wget.log my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with --append-output (append-output long option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget --append-output=/tmp/wget.log my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
-    it "does not warn when running with -a (append-output long option) and without --progress option" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget -a /tmp/wget.log my.xyz"
-            ]
-       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
diff --git a/test/DL3048.hs b/test/DL3048.hs
deleted file mode 100644
--- a/test/DL3048.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module DL3048 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3048 - Invalid Label Key Rule" $ do
-    it "not ok with reserved namespace" $ do
-      ruleCatches "DL3048" "LABEL com.docker.label=\"foo\""
-      ruleCatches "DL3048" "LABEL io.docker.label=\"foo\""
-      ruleCatches "DL3048" "LABEL org.dockerproject.label=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL com.docker.label=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL io.docker.label=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL org.dockerproject.label=\"foo\""
-    it "not ok with invalid character" $ do
-      ruleCatches "DL3048" "LABEL invalid$character=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL invalid$character=\"foo\""
-    it "not ok with invalid start and end characters" $ do
-      ruleCatches "DL3048" "LABEL .invalid =\"foo\""
-      ruleCatches "DL3048" "LABEL -invalid =\"foo\""
-      ruleCatches "DL3048" "LABEL 1invalid =\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL .invalid=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL -invalid=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL 1invalid=\"foo\""
-    it "not ok with consecutive dividers" $ do
-      ruleCatches "DL3048" "LABEL invalid..character=\"foo\""
-      ruleCatches "DL3048" "LABEL invalid--character=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL invalid..character=\"foo\""
-      onBuildRuleCatches "DL3048" "LABEL invalid--character=\"foo\""
-    it "ok with valid labels" $ do
-      ruleCatchesNot "DL3048" "LABEL org.valid-key.label3=\"foo\""
-      ruleCatchesNot "DL3048" "LABEL validlabel=\"foo\""
-      onBuildRuleCatchesNot "DL3048" "LABEL org.valid-key.label3=\"foo\""
-      onBuildRuleCatchesNot "DL3048" "LABEL validlabel=\"foo\""
diff --git a/test/DL3049.hs b/test/DL3049.hs
deleted file mode 100644
--- a/test/DL3049.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-module DL3049 (tests) where
-
-import Data.Text as Text
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("foo", Rule.RawText)]) False
-  describe "DL3049 - Missing label rule tests" $ do
-  -- single stage tests
-    it "not ok: single stage, no label" $ do
-      ruleCatches "DL3049" "FROM baseimage"
-      onBuildRuleCatches "DL3049" "FROM baseimage"
-    it "not ok: single stage, wrong label" $ do
-      ruleCatches "DL3049" "FROM baseimage\nLABEL bar=\"baz\""
-      onBuildRuleCatches "DL3049" "FROM baseimage\nLABEL bar=\"baz\""
-    it "ok: single stage, label present" $ do
-      ruleCatchesNot "DL3049" "FROM baseimage\nLABEL foo=\"bar\""
-      onBuildRuleCatchesNot "DL3049" "FROM baseimage\nLABEL foo=\"bar\""
-  -- multi stage tests
-    it "warn twice: two stages, no labels" $
-      let dockerFile =
-            [ "FROM stage1",
-              "FROM stage2"
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 2 "DL3049")
-    it "warn twice: two stages, wrong labels only" $
-      let dockerFile =
-            [ "FROM stage1",
-              "LABEL bar=\"baz\"",
-              "FROM stage2",
-              "LABEL buzz=\"fuzz\""
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 2 "DL3049")
-    it "warn once: two stages, label present in second only" $
-      let dockerFile =
-            [ "FROM baseimage",
-              "FROM newimage",
-              "LABEL foo=\"bar\""
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 1 "DL3049")
-    it "warn once: two stages, no inheritance, wrong label in one" $
-      let dockerFile =
-            [ "FROM baseimage",
-              "LABEL baz=\"bar\"",
-              "FROM newimage",
-              "LABEL foo=\"bar\""
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 1 "DL3049")
-    it "warn once: two stages, inheritance, label only defined in second stage" $
-      let dockerFile =
-            [ "FROM baseimage as base",
-              "FROM base",
-              "LABEL foo=\"bar\""
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 1 "DL3049")
-    it "don't warn: two stages, inheritance" $
-      let dockerFile =
-            [ "FROM baseimage as base",
-              "LABEL foo=\"bar\"",
-              "FROM base"
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 0 "DL3049")
diff --git a/test/DL3050.hs b/test/DL3050.hs
deleted file mode 100644
--- a/test/DL3050.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module DL3050 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("required", Rule.RawText)]) True
-  describe "DL3050 - Superfluous label(s) present." $ do
-    it "ok with no label" $ do
-      ruleCatchesNot "DL3050" ""
-      onBuildRuleCatchesNot "DL3050" ""
-    it "ok with required label" $ do
-      ruleCatchesNot "DL3050" "LABEL required=\"foo\""
-      onBuildRuleCatchesNot "DL3050" "LABEL required=\"bar\""
-    it "not ok with just other label" $ do
-      ruleCatches "DL3050" "LABEL other=\"bar\""
-      onBuildRuleCatches "DL3050" "LABEL other=\"bar\""
-    it "not ok with other label and required label" $ do
-      ruleCatches "DL3050" "LABEL required=\"foo\" other=\"bar\""
-      onBuildRuleCatches "DL3050" "LABEL required=\"foo\" other=\"bar\""
diff --git a/test/DL3051.hs b/test/DL3051.hs
deleted file mode 100644
--- a/test/DL3051.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module DL3051 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("emptylabel", Rule.RawText)]) False
-  describe "DL3051 - Label `<label>` is empty." $ do
-    it "not ok with label empty" $ do
-      ruleCatches "DL3051" "LABEL emptylabel=\"\""
-      onBuildRuleCatches "DL3051" "LABEL emptylabel=\"\""
-    it "ok with label not empty" $ do
-      ruleCatchesNot "DL3051" "LABEL emptylabel=\"foo\""
-      onBuildRuleCatchesNot "DL3051" "LABEL emptylabel=\"bar\""
-    it "ok with other label empty" $ do
-      ruleCatchesNot "DL3051" "LABEL other=\"\""
-      onBuildRuleCatchesNot "DL3051" "LABEL other=\"\""
diff --git a/test/DL3052.hs b/test/DL3052.hs
deleted file mode 100644
--- a/test/DL3052.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module DL3052 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("urllabel", Rule.Url)]) False
-  describe "DL3052 - Label `<label>` is not a valid URL." $ do
-    it "not ok with label not containing URL" $ do
-      ruleCatches "DL3052" "LABEL urllabel=\"not-url\""
-      onBuildRuleCatches "DL3052" "LABEL urllabel=\"not-url\""
-    it "ok with label containing URL" $ do
-      ruleCatchesNot "DL3052" "LABEL urllabel=\"http://example.com\""
-      onBuildRuleCatchesNot "DL3052" "LABEL urllabel=\"http://example.com\""
-    it "ok with other label not containing URL" $ do
-      ruleCatchesNot "DL3052" "LABEL other=\"foo\""
-      onBuildRuleCatchesNot "DL3052" "LABEL other=\"bar\""
diff --git a/test/DL3053.hs b/test/DL3053.hs
deleted file mode 100644
--- a/test/DL3053.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module DL3053 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("datelabel", Rule.Rfc3339)]) False
-  describe "DL3053 - Label `<label>` is not a valid time format - must be conform to RFC3339." $ do
-    it "not ok with label not containing RFC3339 date" $ do
-      ruleCatches "DL3053" "LABEL datelabel=\"not-date\""
-      onBuildRuleCatches "DL3053" "LABEL datelabel=\"not-date\""
-    it "ok with label containing RFC3339 date" $ do
-      ruleCatchesNot "DL3053" "LABEL datelabel=\"2021-03-10T10:26:33.564595127+01:00\""
-      onBuildRuleCatchesNot "DL3053" "LABEL datelabel=\"2021-03-10T10:26:33.564595127+01:00\""
-    it "ok with other label not containing RFC3339 date" $ do
-      ruleCatchesNot "DL3053" "LABEL other=\"doo\""
-      onBuildRuleCatchesNot "DL3053" "LABEL other=\"bar\""
diff --git a/test/DL3054.hs b/test/DL3054.hs
deleted file mode 100644
--- a/test/DL3054.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module DL3054 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("spdxlabel", Rule.Spdx)]) False
-  describe "DL3054 - Label `<label>` is not a valid SPDX license identifier." $ do
-    it "not ok with label not containing SPDX identifier" $ do
-      ruleCatches "DL3054" "LABEL spdxlabel=\"not-spdx\""
-      onBuildRuleCatches "DL3054" "LABEL spdxlabel=\"not-spdx\""
-    it "ok with label containing SPDX identifier" $ do
-      ruleCatchesNot "DL3054" "LABEL spdxlabel=\"BSD-3-Clause\""
-      onBuildRuleCatchesNot "DL3054" "LABEL spdxlabel=\"MIT\""
-    it "ok with other label not containing SPDX identifier" $ do
-      ruleCatchesNot "DL3054" "LABEL other=\"fooo\""
-      onBuildRuleCatchesNot "DL3054" "LABEL other=\"bar\""
diff --git a/test/DL3055.hs b/test/DL3055.hs
deleted file mode 100644
--- a/test/DL3055.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module DL3055 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("githash", Rule.GitHash)]) False
-  describe "DL3055 - Label `<label>` is not a valid git hash." $ do
-    it "not ok with label not containing git hash" $ do
-      ruleCatches "DL3055" "LABEL githash=\"not-git-hash\""
-      onBuildRuleCatches "DL3055" "LABEL githash=\"not-git-hash\""
-    it "ok with label containing short git hash" $ do
-      ruleCatchesNot "DL3055" "LABEL githash=\"2dbfae9\""
-      onBuildRuleCatchesNot "DL3055" "LABEL githash=\"2dbfae9\""
-    it "ok with label containing long git hash" $ do
-      ruleCatchesNot "DL3055" "LABEL githash=\"43c572f1272b6b3171dd1db9e41b7027128ce080\""
-      onBuildRuleCatchesNot "DL3055" "LABEL githash=\"43c572f1272b6b3171dd1db9e41b7027128ce080\""
-    it "ok with other label not containing git hash" $ do
-      ruleCatchesNot "DL3055" "LABEL other=\"foo\""
-      onBuildRuleCatchesNot "DL3055" "LABEL other=\"bar\""
diff --git a/test/DL3056.hs b/test/DL3056.hs
deleted file mode 100644
--- a/test/DL3056.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module DL3056 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("semver", Rule.SemVer)]) False
-  describe "DL3056 - Label `<label>` does not conform to semantic versioning." $ do
-    it "not ok with label not containing semantic version" $ do
-      ruleCatches "DL3056" "LABEL semver=\"not-sem-ver\""
-      onBuildRuleCatches "DL3056" "LABEL semver=\"not-sem-ver\""
-    it "ok with label containing semantic version" $ do
-      ruleCatchesNot "DL3056" "LABEL semver=\"1.0.0\""
-      onBuildRuleCatchesNot "DL3056" "LABEL semver=\"2.0.1-rc1\""
-    it "ok with other label not containing semantic version" $ do
-      ruleCatchesNot "DL3056" "LABEL other=\"foo\""
-      onBuildRuleCatchesNot "DL3056" "LABEL other=\"bar\""
diff --git a/test/DL3057.hs b/test/DL3057.hs
deleted file mode 100644
--- a/test/DL3057.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-module DL3057 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3057 - `HEALTHCHECK instruction missing" $ do
-    it "warn with no HEALTHCHECK instructions" $
-      ruleCatches "DL3057" "FROM scratch"
-    it "ok with one HEALTHCHECK instruction" $
-      ruleCatchesNot "DL3057" "FROM scratch\nHEALTHCHECK CMD /bin/bla"
-    it "ok with inheriting HEALTHCHECK instruction" $
-      ruleCatchesNot "DL3057" "FROM scratch AS base\nHEALTHCHECK CMD /bin/bla\nFROM base"
-    it "warn when not inheriting with no HEALTHCHECK instruction" $
-      ruleCatches "DL3057" "FROM scratch AS base\nHEALTHCHECK CMD /bin/bla\nFROM scratch"
diff --git a/test/DL3058.hs b/test/DL3058.hs
deleted file mode 100644
--- a/test/DL3058.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-module DL3058 (tests) where
-
-import qualified Data.Map as Map
-import qualified Hadolint.Process
-import qualified Hadolint.Rule as Rule
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("maintainer", Rule.Email)]) False
-  describe "DL3058 - Label `<label>` is not a valid email format - must be conform to RFC5322." $ do
-    it "not ok with label not containing valid email" $ do
-      ruleCatches "DL3058" "LABEL maintainer=\"not-email\""
-      onBuildRuleCatches "DL3058" "LABEL maintainer=\"not-email\""
-    it "ok with label containing valid email" $ do
-      ruleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""
-      onBuildRuleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""
-    it "ok with other label not containing valid email" $ do
-      ruleCatchesNot "DL3058" "LABEL other=\"doo\""
-      onBuildRuleCatchesNot "DL3058" "LABEL other=\"bar\""
diff --git a/test/DL3059.hs b/test/DL3059.hs
deleted file mode 100644
--- a/test/DL3059.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module DL3059 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3059 - Multiple consecutive `RUN` instructions" $ do
-    it "ok with no `RUN` at all" $ do
-      ruleCatchesNot "DL3059" "FROM debian:10"
-    it "ok with one `RUN`" $ do
-      ruleCatchesNot "DL3059" "RUN /foo.sh"
-    it "ok with two not consecutive `RUN`" $ do
-      ruleCatchesNot "DL3059" "RUN /foo.sh\nWORKDIR /\nRUN /bar.sh"
-    it "not ok with two consecutive `RUN`s" $ do
-      ruleCatches "DL3059" "RUN /foo.sh\nRUN /bar.sh"
-    it "ok with two consecutive `RUN`s when flags are different 1" $ do
-      ruleCatchesNot "DL3059" "RUN --mount=type=secret,id=foo /foo.sh\nRUN /bar.sh"
-    it "ok with two consecutive `RUN`s when flags are different 2" $ do
-      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",
-                    "RUN --mount=type=secret,id=bar /bar.sh"
-                  ]
-       in ruleCatchesNot "DL3059" $ Text.unlines dfile
-    it "not ok with two consecutive `RUN`s when flags are equal" $ do
-      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",
-                    "RUN --mount=type=secret,id=foo /bar.sh"
-                  ]
-       in ruleCatches "DL3059" $ Text.unlines dfile
diff --git a/test/DL3060.hs b/test/DL3060.hs
deleted file mode 100644
--- a/test/DL3060.hs
+++ /dev/null
@@ -1,68 +0,0 @@
-module DL3060 (tests) where
-
-import qualified Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL3060 - `yarn cache clean` missing after `yarn install`" $ do
-    it "ok with non-yarn commands" $ do
-      ruleCatchesNot "DL3060" "RUN foo"
-      onBuildRuleCatchesNot "DL3060" "RUN foo"
-    it "not ok with no cache clean" $ do
-      ruleCatches "DL3060" "RUN yarn install foo"
-      onBuildRuleCatches "DL3060" "RUN yarn install foo"
-    it "ok with cache clean" $ do
-      ruleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"
-      onBuildRuleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"
-
-    it "not ok when yarn install is in last stage w/o yarn clean" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM node:lts-alpine as foo",
-                "RUN hey!",
-                "FROM scratch",
-                "RUN yarn install"
-              ]
-       in do
-            ruleCatches "DL3060" dockerFile
-            onBuildRuleCatches "DL3060" dockerFile
-
-    it "not ok when inheriting from stage with yarn install w/o yarn clean" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM node:lts-alpine as foo",
-                "RUN yarn install",
-                "FROM foo",
-                "RUN hey!"
-              ]
-       in do
-            ruleCatches "DL3060" dockerFile
-            onBuildRuleCatches "DL3060" dockerFile
-
-    it "ok when inheriting from stage with yarn cache clear" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM node:lts-alpine as foo",
-                "RUN yarn install && yarn cache clean",
-                "FROM foo",
-                "RUN hey!"
-              ]
-       in do
-            ruleCatchesNot "DL3060" dockerFile
-            onBuildRuleCatchesNot "DL3060" dockerFile
-
-    it "ok when omitting yarn cache clean in stage that is not reused later" $
-      let dockerFile =
-            Text.unlines
-              [ "FROM node:lts-alpine as foo",
-                "RUN yarn install foo",
-                "FROM scratch",
-                "RUN hey!"
-              ]
-       in do
-            ruleCatchesNot "DL3060" dockerFile
-            onBuildRuleCatches "DL3060" dockerFile
diff --git a/test/DL4000.hs b/test/DL4000.hs
deleted file mode 100644
--- a/test/DL4000.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module DL4000 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL4000 - MAINTAINER is deprecated." $ do
-    it "has deprecated maintainer" $
-      ruleCatches "DL4000" "FROM busybox\nMAINTAINER hudu@mail.com"
-    it "has maintainer" $ ruleCatches "DL4000" "FROM debian\nMAINTAINER Lukas"
-    it "has maintainer first" $ ruleCatches "DL4000" "MAINTAINER Lukas\nFROM DEBIAN"
-    it "has no maintainer" $ ruleCatchesNot "DL4000" "FROM debian"
diff --git a/test/DL4001.hs b/test/DL4001.hs
deleted file mode 100644
--- a/test/DL4001.hs
+++ /dev/null
@@ -1,69 +0,0 @@
-module DL4001 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL4001 - Either use Wget or Curl but not both." $ do
-    it "warns when using both wget and curl" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz",
-              "RUN curl localhost"
-            ]
-       in ruleCatches "DL4001" $ Text.unlines dockerFile
-    it "warns when using both wget and curl in same instruction" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz && curl localhost"
-            ]
-       in ruleCatches "DL4001" $ Text.unlines dockerFile
-    it "does not warn when using only wget" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz"
-            ]
-       in ruleCatchesNot "DL4001" $ Text.unlines dockerFile
-    it "does not warn when using both curl and wget in different stages" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz",
-              "FROM scratch",
-              "RUN curl localhost"
-            ]
-       in ruleCatchesNot "DL4001" $ Text.unlines dockerFile
-    it "does not warns when using both, on a single stage" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz",
-              "RUN curl localhost",
-              "FROM scratch",
-              "RUN curl localhost"
-            ]
-       in ruleCatches "DL4001" $ Text.unlines dockerFile
-    it "only warns on the relevant RUN instruction" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz",
-              "RUN curl my.xyz",
-              "RUN echo hello"
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 1 "DL4001")
-
-    it "only warns on many relevant RUN instructions" $
-      let dockerFile =
-            [ "FROM node as foo",
-              "RUN wget my.xyz",
-              "RUN curl my.xyz",
-              "RUN echo hello",
-              "RUN wget foo.com"
-            ]
-       in assertChecks
-            (Text.unlines dockerFile)
-            (failsWith 2 "DL4001")
diff --git a/test/DL4003.hs b/test/DL4003.hs
deleted file mode 100644
--- a/test/DL4003.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-module DL4003 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL4003" $ do
-    it "many cmds" $
-      let dockerFile =
-            [ "FROM debian",
-              "CMD bash",
-              "RUN foo",
-              "CMD another"
-            ]
-       in ruleCatches "DL4003" $ Text.unlines dockerFile
-
-    it "single cmds, different stages" $
-      let dockerFile =
-            [ "FROM debian as distro1",
-              "CMD bash",
-              "RUN foo",
-              "FROM debian as distro2",
-              "CMD another"
-            ]
-       in ruleCatchesNot "DL4003" $ Text.unlines dockerFile
-
-    it "many cmds, different stages" $
-      let dockerFile =
-            [ "FROM debian as distro1",
-              "CMD bash",
-              "RUN foo",
-              "CMD another",
-              "FROM debian as distro2",
-              "CMD another"
-            ]
-       in ruleCatches "DL4003" $ Text.unlines dockerFile
-
-    it "single cmd" $ ruleCatchesNot "DL4003" "CMD /bin/true"
diff --git a/test/DL4004.hs b/test/DL4004.hs
deleted file mode 100644
--- a/test/DL4004.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-module DL4004 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL4004" $ do
-    it "no cmd" $ ruleCatchesNot "DL4004" "FROM busybox"
-
-    it "many entrypoints" $
-      let dockerFile =
-            [ "FROM debian",
-              "ENTRYPOINT bash",
-              "RUN foo",
-              "ENTRYPOINT another"
-            ]
-       in ruleCatches "DL4004" $ Text.unlines dockerFile
-
-    it "single entrypoint, different stages" $
-      let dockerFile =
-            [ "FROM debian as distro1",
-              "ENTRYPOINT bash",
-              "RUN foo",
-              "FROM debian as distro2",
-              "ENTRYPOINT another"
-            ]
-       in ruleCatchesNot "DL4004" $ Text.unlines dockerFile
-
-    it "many entrypoints, different stages" $
-      let dockerFile =
-            [ "FROM debian as distro1",
-              "ENTRYPOINT bash",
-              "RUN foo",
-              "ENTRYPOINT another",
-              "FROM debian as distro2",
-              "ENTRYPOINT another"
-            ]
-       in ruleCatches "DL4004" $ Text.unlines dockerFile
-
-    it "single entry" $ ruleCatchesNot "DL4004" "ENTRYPOINT /bin/true"
-    it "no entry" $ ruleCatchesNot "DL4004" "FROM busybox"
diff --git a/test/DL4005.hs b/test/DL4005.hs
deleted file mode 100644
--- a/test/DL4005.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module DL4005 (tests) where
-
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL4005 - Use `SHELL` to change the default shell." $ do
-    it "RUN ln" $ do
-      ruleCatches "DL4005" "RUN ln -sfv /bin/bash /bin/sh"
-      onBuildRuleCatches "DL4005" "RUN ln -sfv /bin/bash /bin/sh"
-    it "RUN ln with unrelated symlinks" $ do
-      ruleCatchesNot "DL4005" "RUN ln -sf /bin/true /sbin/initctl"
-      onBuildRuleCatchesNot "DL4005" "RUN ln -sf /bin/true /sbin/initctl"
-    it "RUN ln with multiple acceptable commands" $ do
-      ruleCatchesNot "DL4005" "RUN ln -s foo bar && unrelated && something_with /bin/sh"
-      onBuildRuleCatchesNot "DL4005" "RUN ln -s foo bar && unrelated && something_with /bin/sh"
diff --git a/test/DL4006.hs b/test/DL4006.hs
deleted file mode 100644
--- a/test/DL4006.hs
+++ /dev/null
@@ -1,104 +0,0 @@
-module DL4006 (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "DL4006 - Set the `SHELL` option `-o pipefail` before RUN with a pipe in it" $ do
-    it "warn on missing pipefail" $
-      let dockerFile =
-            [ "FROM scratch",
-              "RUN wget -O - https://some.site | wc -l > /number"
-            ]
-       in ruleCatches "DL4006" $ Text.unlines dockerFile
-    it "don't warn on commands with no pipes" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "RUN wget -O - https://some.site && wc -l file > /number"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "don't warn on commands with pipes and the pipefail option" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "don't warn on commands with pipes and the pipefail option 2" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "don't warn on commands with pipes and the pipefail option 3" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/bash\", \"-o\", \"errexit\", \"-o\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "don't warn on commands with pipes and the pipefail zsh" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/zsh\", \"-o\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "don't warn on powershell" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"pwsh\", \"-c\"]",
-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "warns when using plain sh" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatches "DL4006" $ Text.unlines dockerFile
-    it "warn on missing pipefail in the next image" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number",
-              "FROM scratch as build2",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatches "DL4006" $ Text.unlines dockerFile
-    it "warn on missing pipefail if next SHELL is not using it" $
-      let dockerFile =
-            [ "FROM scratch as build",
-              "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number",
-              "SHELL [\"/bin/sh\", \"-c\"]",
-              "RUN wget -O - https://some.site | wc -l file > /number"
-            ]
-       in ruleCatches "DL4006" $ Text.unlines dockerFile
-    it "ignore non posix shells: pwsh" $
-      let dockerFile =
-            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",
-              "SHELL [ \"pwsh\", \"-c\" ]",
-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "ignore non posix shells: powershell" $
-      let dockerFile =
-            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",
-              "SHELL [ \"powershell.exe\" ]",
-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
-    it "ignore non posix shells: cmd.exe" $
-      let dockerFile =
-            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",
-              "SHELL [ \"cmd.exe\", \"/c\" ]",
-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-            ]
-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Config/CommandlineSpec.hs b/test/Hadolint/Config/CommandlineSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Config/CommandlineSpec.hs
@@ -0,0 +1,346 @@
+module Hadolint.Config.CommandlineSpec (spec) where
+
+import Test.Hspec
+import Test.HUnit hiding (Label)
+import Hadolint
+import Options.Applicative
+  ( ParserResult (..),
+    defaultPrefs,
+    execParserPure,
+    fullDesc,
+    info,
+  )
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+
+spec :: SpecWith ()
+spec = do
+  describe "Config from Commandline" $ do
+    it "parse with no arguments given" $ do
+      checkCommandline [] $ CommandlineConfig
+                              False
+                              Nothing
+                              []
+                              Nothing
+                              mempty
+
+    describe "parse version flag" $ do
+      it "parse -v" $ do
+        checkCommandline ["-v"] $ CommandlineConfig
+                                True
+                                Nothing
+                                []
+                                Nothing
+                                mempty
+
+      it "parse --version" $ do
+        checkCommandline ["--version"] $ CommandlineConfig
+                                True
+                                Nothing
+                                []
+                                Nothing
+                                mempty
+
+    describe "parse config file option" $ do
+      it "parse -c hadolint.yaml" $ do
+        checkCommandline ["-c", "hadolint.yaml"] $ CommandlineConfig
+                                False
+                                (Just "hadolint.yaml")
+                                []
+                                Nothing
+                                mempty
+
+      it "parse --config hadolint.yaml" $ do
+        checkCommandline ["--config", "hadolint.yaml"] $ CommandlineConfig
+                                False
+                                (Just "hadolint.yaml")
+                                []
+                                Nothing
+                                mempty
+
+    describe "parse file arguments" $ do
+      it "parse `Dockerfile`" $ do
+        checkCommandline ["Dockerfile"] $ CommandlineConfig
+                                False
+                                Nothing
+                                ["Dockerfile"]
+                                Nothing
+                                mempty
+
+      it "parse `Dockerfile1 Dockerfile2`" $ do
+        checkCommandline ["Dockerfile1", "Dockerfile2"] $ CommandlineConfig
+                                False
+                                Nothing
+                                ["Dockerfile1", "Dockerfile2"]
+                                Nothing
+                                mempty
+
+      it "parse --file-path-in-report foobar/Dockerfile" $ do
+        checkCommandline
+          ["--file-path-in-report", "foobar/Dockerfile"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              (Just "foobar/Dockerfile")
+              mempty
+          )
+
+    describe "parse general configuration" $ do
+      it "parse --no-fail" $ do
+        checkCommandline ["--no-fail"] $ CommandlineConfig
+                                          False
+                                          Nothing
+                                          []
+                                          Nothing
+                                          mempty { partialNoFail = Just True }
+
+      it "parse --no-color" $ do
+        checkCommandline ["--no-color"] $ CommandlineConfig
+                                            False
+                                            Nothing
+                                            []
+                                            Nothing
+                                            mempty { partialNoColor = Just True }
+
+      it "parse -V" $ do
+        checkCommandline ["-V"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialVerbose = Just True }
+
+      it "parse --verbose" $ do
+        checkCommandline ["--verbose"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialVerbose = Just True }
+
+      it "parse -f json" $ do
+        checkCommandline ["-f", "json"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialFormat = Just Json }
+
+      it "parse --format" $ do
+        checkCommandline ["--format", "sarif"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialFormat = Just Sarif }
+
+    describe "parse severity overrides" $ do
+      it "parse --error=DL3010" $ do
+        checkCommandline ["--error", "DL3010"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialErrorRules = ["DL3010"] }
+
+      it "parse --error=DL3010 --error=DL3020" $ do
+        checkCommandline
+          ["--error", "DL3010", "--error=DL3020"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialErrorRules = ["DL3010", "DL3020"] }
+          )
+
+      it "parse --warning=DL3010" $ do
+        checkCommandline ["--warning", "DL3010"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialWarningRules = ["DL3010"] }
+
+      it "parse --warning=DL3010 --warning=DL3020" $ do
+        checkCommandline
+          ["--warning", "DL3010", "--warning=DL3020"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialWarningRules = ["DL3010", "DL3020"] }
+          )
+
+      it "parse --info=DL3010" $ do
+        checkCommandline ["--info", "DL3010"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialInfoRules = ["DL3010"] }
+
+      it "parse --info=DL3010 --info=DL3020" $ do
+        checkCommandline
+          ["--info", "DL3010", "--info=DL3020"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialInfoRules = ["DL3010", "DL3020"] }
+          )
+
+      it "parse --style=DL3010" $ do
+        checkCommandline ["--style", "DL3010"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialStyleRules = ["DL3010"] }
+
+      it "parse --style=DL3010 --style=DL3020" $ do
+        checkCommandline
+          ["--style", "DL3010", "--style=DL3020"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialStyleRules = ["DL3010", "DL3020"] }
+          )
+
+      it "parse --ignore=DL3010" $ do
+        checkCommandline ["--ignore", "DL3010"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty { partialIgnoreRules = ["DL3010"] }
+
+      it "parse --ignore=DL3010 --ignore=DL3020" $ do
+        checkCommandline
+          ["--ignore", "DL3010", "--ignore=DL3020"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialIgnoreRules = ["DL3010", "DL3020"] }
+          )
+
+    describe "parse trusted registries" $ do
+      it "parse --trusted-registry foobar.com" $ do
+        checkCommandline
+          ["--trusted-registry", "foobar.com"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialAllowedRegistries = Set.fromList ["foobar.com"] }
+          )
+
+      it "parse --trusted-registry foobar.com --trusted-registry barfoo.io" $ do
+        checkCommandline
+          [ "--trusted-registry",
+            "foobar.com",
+            "--trusted-registry",
+            "barfoo.io"
+          ]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty
+                { partialAllowedRegistries =
+                    Set.fromList ["foobar.com", "barfoo.io"]
+                }
+          )
+
+    describe "parse required labels" $ do
+      it "parse --require-label foo:email" $ do
+        checkCommandline
+          ["--require-label", "foo:email"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialLabelSchema = Map.fromList [("foo", Email)] }
+          )
+
+      it "parse --require-label foo:email --require-label bar:text" $ do
+        checkCommandline
+          ["--require-label", "foo:email", "--require-label", "bar:text"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty
+                { partialLabelSchema =
+                    Map.fromList [("foo", Email), ("bar", RawText)]
+                }
+          )
+
+    describe "parse strict labels" $ do
+      it "parse --strict-labels" $ do
+        checkCommandline
+          ["--strict-labels"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              mempty { partialStrictLabels = Just True }
+          )
+
+    describe "parse failure thresholds" $ do
+      it "parse -t warning" $ do
+        checkCommandline ["-t", "warning"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty
+                                  { partialFailureThreshold = Just DLWarningC }
+
+      it "parse --failure-threshold style" $ do
+        checkCommandline ["--failure-threshold", "style"] $ CommandlineConfig
+                                False
+                                Nothing
+                                []
+                                Nothing
+                                mempty
+                                  { partialFailureThreshold = Just DLStyleC }
+
+    describe "fail parsing on garbage input" $ do
+      it "fail parsing --blafoo" $ do
+        checkCommandlineFail
+          ["--blafoo"]
+
+      it "fail parsing --require-label foo:bar" $ do
+        checkCommandlineFail
+          ["--require-label", "foo:bar"]
+
+
+checkCommandline :: [String] -> CommandlineConfig -> Assertion
+checkCommandline args config = do
+  let inf = info parseCommandline fullDesc
+      res = execParserPure defaultPrefs inf args
+  case res of
+    Success cfg -> cfg `shouldBe` config
+    _ -> assertFailure "should have parsed commandline arguments"
+
+checkCommandlineFail :: [String] -> Assertion
+checkCommandlineFail args = do
+  let inf = info parseCommandline fullDesc
+      res = execParserPure defaultPrefs inf args
+  case res of
+    Failure _ -> assertEqual "" (0 :: Int) (0 :: Int)  -- inverse asserFailure
+    _ -> assertFailure "should have failed parsing commandline arguments"
diff --git a/test/Hadolint/Config/ConfigfileSpec.hs b/test/Hadolint/Config/ConfigfileSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Config/ConfigfileSpec.hs
@@ -0,0 +1,169 @@
+module Hadolint.Config.ConfigfileSpec (spec) where
+
+import Hadolint
+import Test.Hspec
+import qualified Data.ByteString.Char8 as Bytes
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified Data.YAML as Yaml
+
+
+spec :: SpecWith ()
+spec =
+  describe "Config from Configfile" $ do
+    it "parse empty config file" $ do
+      let yaml = []
+          conf = parseYaml yaml
+      conf `shouldBe` Left "empty YAML stream"
+
+    it "parse `no-fail: true`" $ do
+      let yaml = ["no-fail: true"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialNoFail = Just True }
+
+    it "parse `no-fail: false`" $ do
+      let yaml = ["no-fail: false"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialNoFail = Just False }
+
+    it "parse `no-color: true`" $ do
+      let yaml = ["no-color: true"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialNoColor = Just True }
+
+    it "parse `no-color: false`" $ do
+      let yaml = ["no-color: false"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialNoColor = Just False }
+
+    it "parse `verbose: true`" $ do
+      let yaml = ["verbose: true"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialVerbose = Just True }
+
+    it "parse `verbose: false`" $ do
+      let yaml = ["verbose: false"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialVerbose = Just False }
+
+    it "parse `output-format: json`" $ do
+      let yaml = ["output-format: json"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialFormat = Just Json }
+
+    it "parse `output-format: sarif`" $ do
+      let yaml = ["output-format: sarif"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialFormat = Just Sarif }
+
+    it "parse override error rules" $ do
+      let yaml = [ "override:",
+                   "  error:",
+                   "    - DL3020",
+                   "    - DL3040",
+                   "    - SC1020"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialErrorRules = ["DL3020", "DL3040", "SC1020"] }
+
+    it "parse override warning rules" $ do
+      let yaml = [ "override:",
+                   "  warning:",
+                   "    - DL3020",
+                   "    - DL3040",
+                   "    - SC1020"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialWarningRules = ["DL3020", "DL3040", "SC1020"]
+                          }
+
+    it "parse override info rules" $ do
+      let yaml = [ "override:",
+                   "  info:",
+                   "    - DL3020",
+                   "    - DL3040",
+                   "    - SC1020"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialInfoRules = ["DL3020", "DL3040", "SC1020"] }
+
+    it "parse override style rules" $ do
+      let yaml = [ "override:",
+                   "  style:",
+                   "    - DL3020",
+                   "    - DL3040",
+                   "    - SC1020"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialStyleRules = ["DL3020", "DL3040", "SC1020"] }
+
+    it "parse ignored rules" $ do
+      let yaml = [ "ignored:",
+                   "  - DL3020",
+                   "  - DL3040",
+                   "  - SC1020"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialIgnoreRules = ["DL3020", "DL3040", "SC1020"] }
+
+    it "parse trusted registries" $ do
+      let yaml = [ "trusted-registries:",
+                   "  - foobar.com",
+                   "  - barfoo.com"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialAllowedRegistries =
+                              Set.fromList ["foobar.com", "barfoo.com"]
+                          }
+
+    it "parse label schema" $ do
+      let yaml = [ "label-schema:",
+                   "  foo: email",
+                   "  bar: text"
+                 ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right
+                        mempty
+                          { partialLabelSchema =
+                              Map.fromList [("foo", Email), ("bar", RawText)]
+                          }
+
+    it "parse strict-labels: true" $ do
+      let yaml = [ "strict-labels: true" ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialStrictLabels = Just True }
+
+    it "parse strict-labels: false" $ do
+      let yaml = [ "strict-labels: false" ]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialStrictLabels = Just False }
+
+    it "parse `failure-threshold: warning`" $ do
+      let yaml = ["failure-threshold: warning"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialFailureThreshold = Just DLWarningC }
+
+    it "parse `failure-threshold: style`" $ do
+      let yaml = ["failure-threshold: style"]
+          conf = parseYaml yaml
+      conf `shouldBe` Right mempty { partialFailureThreshold = Just DLStyleC }
+
+-- Helper functions for parsing config files
+
+parseYaml :: [Bytes.ByteString] -> Either String PartialConfiguration
+parseYaml ls =
+  case (Yaml.decode1Strict . Bytes.unlines) ls of
+    Left (_, err) -> Left err
+    Right res -> Right res
diff --git a/test/Hadolint/Config/ConfigurationSpec.hs b/test/Hadolint/Config/ConfigurationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Config/ConfigurationSpec.hs
@@ -0,0 +1,219 @@
+module Hadolint.Config.ConfigurationSpec where
+
+import Data.Default
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import Hadolint
+import Test.Hspec
+
+spec :: SpecWith ()
+spec = do
+  describe "Configuration" $ do
+    it "default configuration" $ do
+      def
+        `shouldBe` Configuration
+          False
+          False
+          False
+          TTY
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
+
+    it "override default configuration with empty config" $ do
+      applyPartialConfiguration def mempty
+        `shouldBe` Configuration
+          False
+          False
+          False
+          TTY
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
+
+    it "override default with specific configuration: no-fail" $ do
+      let config =
+            PartialConfiguration
+              (Just True)
+              Nothing
+              Nothing
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              Nothing
+              mempty
+      applyPartialConfiguration def config
+        `shouldBe` Configuration
+          True
+          False
+          False
+          TTY
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
+
+    it "override default with specific configuration: no-color" $ do
+      let config =
+            PartialConfiguration
+              Nothing
+              (Just True)
+              Nothing
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              Nothing
+              mempty
+      applyPartialConfiguration def config
+        `shouldBe` Configuration
+          False
+          True
+          False
+          TTY
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
+
+    it "empty should not override: no-color" $ do
+      let config =
+            PartialConfiguration
+              Nothing
+              (Just True)
+              Nothing
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              Nothing
+              mempty
+          config2 =
+            PartialConfiguration
+              Nothing
+              Nothing
+              Nothing
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              Nothing
+              mempty
+      applyPartialConfiguration def (config <> config2)
+        `shouldBe` Configuration
+          False
+          True
+          False
+          TTY
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
+
+    it "override default with specific configuration: verbose" $ do
+      let config =
+            PartialConfiguration
+              Nothing
+              Nothing
+              (Just True)
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              Nothing
+              mempty
+      applyPartialConfiguration def config
+        `shouldBe` Configuration
+          False
+          False
+          True
+          TTY
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
+
+    it "override default with specific configuration: output-format json" $ do
+      let config =
+            PartialConfiguration
+              Nothing
+              Nothing
+              Nothing
+              (Just Json)
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              mempty
+              Nothing
+              mempty
+      applyPartialConfiguration def config
+        `shouldBe` Configuration
+          False
+          False
+          False
+          Json
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          mempty
+          False
+          DLInfoC
diff --git a/test/Hadolint/Config/EnvironmentSpec.hs b/test/Hadolint/Config/EnvironmentSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Config/EnvironmentSpec.hs
@@ -0,0 +1,186 @@
+module Hadolint.Config.EnvironmentSpec (spec) where
+
+import Hadolint
+import System.Environment
+import Test.Hspec
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+
+spec :: SpecWith ()
+spec = do
+  describe "Config from Environment" $ do
+    withoutAll $ do
+      it "Empty config when nothing is set" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty
+
+    withJustEnv "HADOLINT_NOFAIL" "1" $ do
+      it "parse HADOLINT_NOFAIL=1" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialNoFail = Just True }
+
+    withJustEnv "HADOLINT_NOFAIL" "0" $ do
+      it "parse HADOLINT_NOFAIL=0" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty {partialNoFail = Just False }
+
+    withJustEnv "NO_COLOR" "y" $ do
+      it "parse NO_COLOR=y" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialNoColor = Just True }
+
+    withJustEnv "NO_COLOR" "n" $ do
+      it "parse NO_COLOR=n" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialNoColor = Just False }
+
+    withJustEnv "HADOLINT_VERBOSE" "on" $ do
+      it "parse HADOLINT_VERBOSE=on" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialVerbose = Just True }
+
+    withJustEnv "HADOLINT_VERBOSE" "off" $ do
+      it "parse HADOLINT_VERBOSE=off" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialVerbose = Just False }
+
+    withJustEnv "HADOLINT_FORMAT" "json" $ do
+      it "parse HADOLINT_FORMAT=json" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialFormat = Just Json }
+
+    withJustEnv "HADOLINT_FORMAT" "sarif" $ do
+      it "parse HADOLINT_FORMAT=sarif" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialFormat = Just Sarif }
+
+    withJustEnv "HADOLINT_OVERRIDE_ERROR" "DL3010" $ do
+      it "parse HADOLINT_OVERRIDE_ERROR=DL3010" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialErrorRules = ["DL3010"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_ERROR" "DL3010,DL3011" $ do
+      it "parse HADOLINT_OVERRIDE_ERROR=DL3010,DL3011" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialErrorRules = ["DL3010", "DL3011"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_WARNING" "DL3010" $ do
+      it "parse HADOLINT_OVERRIDE_WARNING=DL3010" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialWarningRules = ["DL3010"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_WARNING" "DL3010,DL3011" $ do
+      it "parse HADOLINT_OVERRIDE_WARNING=DL3010,DL3011" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialWarningRules = ["DL3010", "DL3011"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_INFO" "DL3010" $ do
+      it "parse HADOLINT_OVERRIDE_INFO=DL3010" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialInfoRules = ["DL3010"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_INFO" "DL3010,DL3011" $ do
+      it "parse HADOLINT_OVERRIDE_INFO=DL3010,DL3011" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialInfoRules = ["DL3010", "DL3011"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_STYLE" "DL3010" $ do
+      it "parse HADOLINT_OVERRIDE_STYLE=DL3010" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialStyleRules = ["DL3010"] }
+
+    withJustEnv "HADOLINT_OVERRIDE_STYLE" "DL3010,DL3011" $ do
+      it "parse HADOLINT_OVERRIDE_STYLE=DL3010,DL3011" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialStyleRules = ["DL3010", "DL3011"] }
+
+    withJustEnv "HADOLINT_IGNORE" "DL3010" $ do
+      it "parse HADOLINT_IGNORE=DL3010" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialIgnoreRules = ["DL3010"] }
+
+    withJustEnv "HADOLINT_IGNORE" "DL3010,DL3011" $ do
+      it "parse HADOLINT_IGNORE=DL3010,DL3011" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialIgnoreRules = ["DL3010", "DL3011"] }
+
+    withJustEnv "HADOLINT_ALLOWED_REGISTRIES" "foobar.com" $ do
+      it "parse HADOLINT_ALLOWED_REGISTRIES=foobar.com" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty
+                          { partialAllowedRegistries =
+                              Set.fromList ["foobar.com"]
+                          }
+
+    withJustEnv "HADOLINT_ALLOWED_REGISTRIES" "foobar.com,barfoo.com" $ do
+      it "parse HADOLINT_ALLOWED_REGISTIRES=foobar.com,barfoo.com" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty
+                          { partialAllowedRegistries =
+                              Set.fromList ["foobar.com", "barfoo.com"]
+                          }
+
+    withJustEnv "HADOLINT_REQUIRE_LABELS" "foo:email" $ do
+      it "parse HADOLINT_REQUIRE_LABELS=foo:email" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty
+                          { partialLabelSchema = Map.fromList [("foo", Email)] }
+
+    withJustEnv "HADOLINT_REQUIRE_LABELS" "foo:email,bar:text" $ do
+      it "parse HADOLINT_REQUIRE_LABELS=foo:email,bar:text" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty
+                          { partialLabelSchema =
+                              Map.fromList [("foo", Email), ("bar", RawText)]
+                          }
+
+    withJustEnv "HADOLINT_STRICT_LABELS" "yes" $ do
+      it "parse HADOLINT_STRICT_LABELS=yes" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialStrictLabels = Just True }
+
+    withJustEnv "HADOLINT_STRICT_LABELS" "false" $ do
+      it "parse HADOLINT_STRICT_LABELS=false" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialStrictLabels = Just False }
+
+    withJustEnv "HADOLINT_FAILURE_THRESHOLD" "error" $ do
+      it "parse HADOLINT_FAILURE_THRESHOLD=error" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialFailureThreshold = Just DLErrorC }
+
+    withJustEnv "HADOLINT_FAILURE_THRESHOLD" "style" $ do
+      it "parse HADOLINT_FAILURE_THRESHOLD=style" $ do
+        conf <- getConfigFromEnvironment
+        conf `shouldBe` mempty { partialFailureThreshold = Just DLStyleC }
+
+-- Helper functions for setup and teardown of the environment
+
+withoutAll :: SpecWith () -> SpecWith ()
+withoutAll = before unsetAll
+
+withJustEnv :: String -> String -> SpecWith () -> SpecWith ()
+withJustEnv name value test =
+  before_ (setJustEnv name value) $ after_ (unsetEnv name) test
+
+setJustEnv :: String -> String -> IO ()
+setJustEnv name value = do
+  unsetAll
+  setEnv name value
+
+unsetAll :: IO ()
+unsetAll = do
+  unsetEnv "HADOLINT_NOFAIL"
+  unsetEnv "NO_COLOR"
+  unsetEnv "HADOLINT_VERBOSE"
+  unsetEnv "HADOLINT_FORMAT"
+  unsetEnv "HADOLINT_OVERRIDE_ERROR"
+  unsetEnv "HADOLINT_OVERRIDE_WARNING"
+  unsetEnv "HADOLINT_OVERRIDE_INFO"
+  unsetEnv "HADOLINT_OVERRIDE_STYLE"
+  unsetEnv "HADOLINT_IGNORE"
+  unsetEnv "HADOLINT_ALLOWED_REGISTRIES"
+  unsetEnv "HADOLINT_REQUIRE_LABELS"
+  unsetEnv "HADOLINT_STRICT_LABELS"
+  unsetEnv "HADOLINT_FAILURE_THRESHOLD"
diff --git a/test/Hadolint/Config/SpecHook.hs b/test/Hadolint/Config/SpecHook.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Config/SpecHook.hs
@@ -0,0 +1,8 @@
+module Hadolint.Config.SpecHook (hook) where
+
+import System.Environment
+import Test.Hspec
+
+
+hook :: SpecWith () -> SpecWith ()
+hook = before_ (setEnv "BAR" "bar")
diff --git a/test/Hadolint/Formatter/ParseErrorSpec.hs b/test/Hadolint/Formatter/ParseErrorSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Formatter/ParseErrorSpec.hs
@@ -0,0 +1,31 @@
+module Hadolint.Formatter.ParseErrorSpec (spec) where
+
+import Hadolint.Formatter.Format (errorMessage)
+import Hadolint.Formatter.TTY (formatError)
+import Language.Docker.Parser
+import Test.HUnit hiding (Label)
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  describe "Parse Errors" $ do
+    it "display error after line pos" $ do
+      let ast = parseText "FOM debian:jessie"
+          expectedMsg =
+            "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY,\
+            \ ENTRYPOINT, ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER,\
+            \ ONBUILD, RUN, SHELL, STOPSIGNAL, USER, VOLUME, WORKDIR, a pragma,\
+            \ end of input, or whitespaces "
+      case ast of
+        Left err ->
+          assertEqual "Unexpected error msg" expectedMsg (formatError err)
+        Right _ -> assertFailure "AST should fail parsing"
+
+    it "display just the error message" $ do
+      let ast = parseText "RUNNN"
+          expectedMsg = "missing whitespace"
+      case ast of
+        Left err ->
+          assertEqual "Unexpected error msg" expectedMsg (errorMessage err)
+        Right _ -> assertFailure "AST should fail parsing"
diff --git a/test/Hadolint/Formatter/SarifSpec.hs b/test/Hadolint/Formatter/SarifSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Formatter/SarifSpec.hs
@@ -0,0 +1,32 @@
+module Hadolint.Formatter.SarifSpec (spec) where
+
+import Helpers
+import Hadolint
+  ( CheckFailure (..),
+    DLSeverity (..),
+    Result(..),
+    printResults,
+    OutputFormat (..),
+    getShortVersion,
+  )
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?noColor = True
+
+  describe "Formatter: Sarif" $ do
+    it "print empty results" $ do
+      let checkFails = []
+          expectation = "{\"runs\":[{\"defaultSourceLanguage\":\"dockerfi\
+                        \le\",\"results\":[],\"tool\":{\"driver\":{\"full\
+                        \Name\":\"Haskell Dockerfile Linter\",\"downloadU\
+                        \ri\":\"https://github.com/hadolint/hadolint\",\"\
+                        \shortDescription\":{\"text\":\"Dockerfile linter\
+                        \, validate inline bash, written in Haskell\"},\"\
+                        \name\":\"Hadolint\",\"version\":\""
+                        ++ getShortVersion
+                        ++ "\"}}}],\"version\":\"2.1.0\",\"$schema\
+                           \\":\"http://json.schemastore.org/sarif-2.1.0\"}"
+      assertFormatter Sarif checkFails expectation
diff --git a/test/Hadolint/Formatter/TTYSpec.hs b/test/Hadolint/Formatter/TTYSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Formatter/TTYSpec.hs
@@ -0,0 +1,87 @@
+module Hadolint.Formatter.TTYSpec (spec) where
+
+import Helpers
+import Data.List.NonEmpty as NonEmpty
+import Hadolint (OutputFormat (..), printResults)
+import Hadolint.Formatter.Format (Result (..))
+import Hadolint.Rule (CheckFailure (..), DLSeverity (..))
+import Test.Hspec
+import qualified Data.Sequence as Seq
+
+
+spec :: SpecWith ()
+spec = do
+  let ?noColor = True
+  describe "Formatter: TTY" $ do
+    it "print empty results" $ do
+      let results = NonEmpty.fromList [Result "<string>" mempty Seq.empty]
+      printResults TTY True (Just "<string>") results `shouldReturn` ()
+
+    it "print some result: no colors" $ do
+      let checkFails = [ CheckFailure
+                          { code = "DL2001",
+                            severity = DLInfoC,
+                            message = "test",
+                            line = 1
+                          }
+                       ]
+          expectation = unlines
+                          [ "<string>:1 DL2001 info: test"
+                          ]
+      assertFormatter TTY checkFails expectation
+
+    it "print some result: with colors" $ do
+      let ?noColor = False
+      let checkFails = [ CheckFailure
+                          { code = "DL2001",
+                            severity = DLInfoC,
+                            message = "test",
+                            line = 1
+                          }
+                       ]
+          expectation = unlines
+                          [ "<string>:1 DL2001 \ESC[92minfo\ESC[0m: test"
+                          ]
+      assertFormatter TTY checkFails expectation
+
+    it "print multiple results: no colors" $ do
+      let checkFails = [ CheckFailure
+                          { code = "DL2001",
+                            severity = DLInfoC,
+                            message = "test",
+                            line = 1
+                          },
+                         CheckFailure
+                          { code = "DL2002",
+                            severity = DLWarningC,
+                            message = "foo",
+                            line = 3
+                          }
+                       ]
+          expectation = unlines
+                          [ "<string>:1 DL2001 info: test",
+                            "<string>:3 DL2002 warning: foo"
+                          ]
+      assertFormatter TTY checkFails expectation
+
+    it "print multiple results: with colors" $ do
+      let ?noColor = False
+      let checkFails = [ CheckFailure
+                          { code = "DL2001",
+                            severity = DLInfoC,
+                            message = "test",
+                            line = 1
+                          },
+                         CheckFailure
+                          { code = "DL2002",
+                            severity = DLWarningC,
+                            message = "foo",
+                            line = 3
+                          }
+                       ]
+          expectation = unlines
+                          [ "<string>:1 DL2001 \ESC[92minfo\ESC[0m: test",
+                            "<string>:3 DL2002 \ESC[1m\ESC[93mwarning\ESC[0m:\
+                            \ foo"
+                          ]
+      assertFormatter TTY checkFails expectation
diff --git a/test/Hadolint/PragmaSpec.hs b/test/Hadolint/PragmaSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/PragmaSpec.hs
@@ -0,0 +1,57 @@
+module Hadolint.PragmaSpec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+import qualified Data.Text as Text
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "Rules can be ignored with inline comments" $ do
+    it "ignores single rule" $
+      let dockerFile =
+            [ "FROM ubuntu",
+              "# hadolint ignore=DL3002",
+              "USER root"
+            ]
+       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
+    it "ignores only the given rule" $
+      let dockerFile =
+            [ "FROM scratch",
+              "# hadolint ignore=DL3001",
+              "USER root"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+    it "ignores only the given rule, when multiple passed" $
+      let dockerFile =
+            [ "FROM scratch",
+              "# hadolint ignore=DL3001,DL3002",
+              "USER root"
+            ]
+       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
+    it "ignores the rule only if directly above the instruction" $
+      let dockerFile =
+            [ "# hadolint ignore=DL3001,DL3002",
+              "FROM ubuntu",
+              "USER root"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+    it "won't ignore the rule if passed invalid rule names" $
+      let dockerFile =
+            [ "FROM scratch",
+              "# hadolint ignore=crazy,DL3002",
+              "USER root"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+    it "ignores multiple rules correctly, even with some extra whitespace" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "# hadolint ignore=DL3023, DL3021",
+              "COPY --from=foo bar baz ."
+            ]
+       in do
+            ruleCatchesNot "DL3023" $ Text.unlines dockerFile
+            ruleCatchesNot "DL3021" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3000Spec.hs b/test/Hadolint/Rule/DL3000Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3000Spec.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3000Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3000 - Use absolute WORKDIR." $ do
+    it "workdir relative" $ ruleCatches "DL3000" "WORKDIR relative/dir"
+    it "workdir absolute" $ ruleCatchesNot "DL3000" "WORKDIR /usr/local"
+    it "workdir variable" $ ruleCatchesNot "DL3000" "WORKDIR ${work}"
+    it "workdir relative single quotes" $ ruleCatches "DL3000" "WORKDIR \'relative/dir\'"
+    it "workdir absolute single quotes" $ ruleCatchesNot "DL3000" "WORKDIR \'/usr/local\'"
+    -- no test for variable/single quotes since the variable would not expand.
+    it "workdir relative double quotes" $ ruleCatches "DL3000" "WORKDIR \"relative/dir\""
+    it "workdir absolute double quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"/usr/local\""
+    it "workdir variable double quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"${dir}\""
+    --
+    it "workdir absolute windows" $ ruleCatchesNot "DL3000" "WORKDIR \'C:\\\'"
+    it "workdir absolute windows quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"C:\\\""
+    it "workdir absolute windows alternative" $
+      ruleCatchesNot "DL3000" "WORKDIR C:/"
+    it "workdir absolute windows quotes alternative" $
+      ruleCatchesNot "DL3000" "WORKDIR \"C:/\""
diff --git a/test/Hadolint/Rule/DL3001Spec.hs b/test/Hadolint/Rule/DL3001Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3001Spec.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3001Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3001 - invalid CMD rules" $ do
+    it "invalid cmd" $ do
+      ruleCatches "DL3001" "RUN top"
+      onBuildRuleCatches "DL3001" "RUN top"
+    it "install ssh" $ do
+      ruleCatchesNot "DL3001" "RUN apt-get install ssh"
+      onBuildRuleCatchesNot "DL3001" "RUN apt-get install ssh"
diff --git a/test/Hadolint/Rule/DL3002Spec.hs b/test/Hadolint/Rule/DL3002Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3002Spec.hs
@@ -0,0 +1,88 @@
+module Hadolint.Rule.DL3002Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3002 - Last user should not be root." $ do
+    it "last user should not be root" $
+      let dockerFile =
+            [ "FROM scratch",
+              "USER root"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+
+    it "no root" $
+      let dockerFile =
+            [ "FROM scratch",
+              "USER foo"
+            ]
+       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
+
+    it "no root UID" $
+      let dockerFile =
+            [ "FROM scratch",
+              "USER 0"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+
+    it "no root:root" $
+      let dockerFile =
+            [ "FROM scratch",
+              "USER root:root"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+
+    it "no UID:GID" $
+      let dockerFile =
+            [ "FROM scratch",
+              "USER 0:0"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+
+    it "can switch back to non root" $
+      let dockerFile =
+            [ "FROM scratch",
+              "USER root",
+              "RUN something",
+              "USER foo"
+            ]
+       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
+
+    it "warns on transitive root user" $
+      let dockerFile =
+            [ "FROM debian as base",
+              "USER root",
+              "RUN something",
+              "FROM base",
+              "RUN something else"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+
+    it "warns on multiple stages" $
+      let dockerFile =
+            [ "FROM debian as base",
+              "USER root",
+              "RUN something",
+              "FROM scratch",
+              "USER foo",
+              "RUN something else"
+            ]
+       in ruleCatches "DL3002" $ Text.unlines dockerFile
+
+    it "does not warn when switching in multiple stages" $
+      let dockerFile =
+            [ "FROM debian as base",
+              "USER root",
+              "RUN something",
+              "USER foo",
+              "FROM scratch",
+              "RUN something else"
+            ]
+       in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3003Spec.hs b/test/Hadolint/Rule/DL3003Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3003Spec.hs
@@ -0,0 +1,14 @@
+module Hadolint.Rule.DL3003Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3003 - Use WORKDIR to switch to a directory." $ do
+    it "ok using WORKDIR" $ ruleCatchesNot "DL3003" "WORKDIR /opt"
+    it "not ok using cd" $ ruleCatches "DL3003" "RUN cd /opt"
diff --git a/test/Hadolint/Rule/DL3004Spec.hs b/test/Hadolint/Rule/DL3004Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3004Spec.hs
@@ -0,0 +1,21 @@
+module Hadolint.Rule.DL3004Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3004 - Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce root." $ do
+    it "sudo" $ do
+      ruleCatches "DL3004" "RUN sudo apt-get update"
+      onBuildRuleCatches "DL3004" "RUN sudo apt-get update"
+    it "install sudo" $ do
+      ruleCatchesNot "DL3004" "RUN apt-get install sudo"
+      onBuildRuleCatchesNot "DL3004" "RUN apt-get install sudo"
+    it "sudo chained programs" $ do
+      ruleCatches "DL3004" "RUN apt-get update && sudo apt-get install"
+      onBuildRuleCatches "DL3004" "RUN apt-get update && sudo apt-get install"
diff --git a/test/Hadolint/Rule/DL3005Spec.hs b/test/Hadolint/Rule/DL3005Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3005Spec.hs
@@ -0,0 +1,15 @@
+module Hadolint.Rule.DL3005Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3005" $ do
+    it "apt-get dist-upgrade" $ do
+      ruleCatches "DL3005" "RUN apt-get update && apt-get dist-upgrade"
+      onBuildRuleCatches "DL3005" "RUN apt-get update && apt-get dist-upgrade"
diff --git a/test/Hadolint/Rule/DL3006Spec.hs b/test/Hadolint/Rule/DL3006Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3006Spec.hs
@@ -0,0 +1,44 @@
+module Hadolint.Rule.DL3006Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3006 - Always tag the version of an image explicitly." $ do
+    it "no untagged" $ ruleCatches "DL3006" "FROM debian"
+    it "no untagged with name" $ ruleCatches "DL3006" "FROM debian AS builder"
+    it "untagged digest is not an error" $
+      ruleCatchesNot "DL3006" "FROM ruby@sha256:f1dbca0f5dbc9"
+    it "untagged digest is not an error" $
+      ruleCatchesNot "DL3006" "FROM ruby:2"
+    it "local aliases are OK to be untagged" $
+      let dockerFile =
+            [ "FROM golang:1.9.3-alpine3.7 AS build",
+              "RUN foo",
+              "FROM build as unit-test",
+              "RUN bar",
+              "FROM alpine:3.7",
+              "RUN baz"
+            ]
+       in do
+            ruleCatchesNot "DL3006" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3006" $ Text.unlines dockerFile
+    it "other untagged cases are not ok" $
+      let dockerFile =
+            [ "FROM golang:1.9.3-alpine3.7 AS build",
+              "RUN foo",
+              "FROM node as unit-test",
+              "RUN bar",
+              "FROM alpine:3.7",
+              "RUN baz"
+            ]
+       in do
+            ruleCatches "DL3006" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3006" $ Text.unlines dockerFile
+    it "scratch" $ ruleCatchesNot "DL3006" "FROM scratch"
diff --git a/test/Hadolint/Rule/DL3007Spec.hs b/test/Hadolint/Rule/DL3007Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3007Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3007Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3007 - Using latest is prone to errors if the image will ever up\
+           \date. Pin the version explicitly to a release tag." $ do
+    it "explicit latest" $ ruleCatches "DL3007" "FROM debian:latest"
+    it "explicit latest with name" $
+      ruleCatches "DL3007" "FROM debian:latest AS builder"
+    it "explicit tagged" $ ruleCatchesNot "DL3007" "FROM debian:jessie"
+    it "explicit SHA" $
+      ruleCatchesNot
+        "DL3007"
+        "FROM hub.docker.io/debian@sha256:\
+        \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"
+    it "explicit tagged with name" $
+      ruleCatchesNot "DL3007" "FROM debian:jessie AS builder"
diff --git a/test/Hadolint/Rule/DL3008Spec.hs b/test/Hadolint/Rule/DL3008Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3008Spec.hs
@@ -0,0 +1,63 @@
+module Hadolint.Rule.DL3008Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3008 - Pin versions in apt-get install." $ do
+    it "apt-get version pinning" $ do
+      ruleCatches "DL3008" "RUN apt-get update && apt-get install python"
+      onBuildRuleCatches "DL3008" "RUN apt-get update && apt-get install python"
+    it "apt-get pinned chained" $
+      let dockerFile =
+            [ "RUN apt-get update \\",
+              " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\",
+              " && rm -rf /var/lib/apt/lists/*"
+            ]
+       in do
+            ruleCatchesNot "DL3008" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile
+
+    it "apt-get pinned regression" $
+      let dockerFile =
+            [ "RUN apt-get update && apt-get install --no-install-recommends -y \\",
+              "python-demjson=2.2.2* \\",
+              "wget=1.16.1* \\",
+              "git=1:2.5.0* \\",
+              "ruby=1:2.1.*"
+            ]
+       in do
+            ruleCatchesNot "DL3008" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile
+    it "apt-get version" $ do
+      ruleCatchesNot "DL3008" "RUN apt-get install -y python=1.2.2"
+      onBuildRuleCatchesNot "DL3008" "RUN apt-get install -y python=1.2.2"
+    it "apt-get version" $ do
+      ruleCatchesNot "DL3008" "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
+      onBuildRuleCatchesNot "DL3008" "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
+    it "apt-get pinned" $ do
+      ruleCatchesNot
+        "DL3008"
+        "RUN apt-get -y --no-install-recommends install nodejs=0.10"
+      onBuildRuleCatchesNot
+        "DL3008"
+        "RUN apt-get -y --no-install-recommends install nodejs=0.10"
+    it "apt-get tolerate target-release" $
+      let dockerFile =
+            [ "RUN set -e &&\\",
+              " echo \"deb http://http.debian.net/debian jessie-backports main\" \
+              \> /etc/apt/sources.list.d/jessie-backports.list &&\\",
+              " apt-get update &&\\",
+              " apt-get install -y --no-install-recommends -t jessie-backports \
+              \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\",
+              " rm -rf /var/lib/apt/lists/*"
+            ]
+       in do
+            ruleCatchesNot "DL3008" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3009Spec.hs b/test/Hadolint/Rule/DL3009Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3009Spec.hs
@@ -0,0 +1,133 @@
+module Hadolint.Rule.DL3009Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3009 - Delete the apt-get lists after installing something." $ do
+    it "apt-get no cleanup" $
+      let dockerFile =
+            [ "FROM scratch",
+              "RUN apt-get update && apt-get install python"
+            ]
+       in do
+            ruleCatches "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+    it "apt-get cleanup in stage image" $
+      let dockerFile =
+            [ "FROM ubuntu as foo",
+              "RUN apt-get update && apt-get install python",
+              "FROM scratch",
+              "RUN echo hey!"
+            ]
+       in do
+            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+    it "apt-get no cleanup in last stage" $
+      let dockerFile =
+            [ "FROM ubuntu as foo",
+              "RUN hey!",
+              "FROM scratch",
+              "RUN apt-get update && apt-get install python"
+            ]
+       in do
+            ruleCatches "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+    it "apt-get no cleanup in intermediate stage" $
+      let dockerFile =
+            [ "FROM ubuntu as foo",
+              "RUN apt-get update && apt-get install python",
+              "FROM foo",
+              "RUN hey!"
+            ]
+       in do
+            ruleCatches "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+    it "no warn apt-get cleanup in intermediate stage that cleans lists" $
+      let dockerFile =
+            [ "FROM ubuntu as foo",
+              "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*",
+              "FROM foo",
+              "RUN hey!"
+            ]
+       in do
+            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3009" $ Text.unlines dockerFile
+    it "no warn apt-get cleanup in intermediate stage when stage not used later" $
+      let dockerFile =
+            [ "FROM ubuntu as foo",
+              "RUN apt-get update && apt-get install python",
+              "FROM scratch",
+              "RUN hey!"
+            ]
+       in do
+            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+    it "apt-get cleanup" $
+      let dockerFile =
+            [ "FROM scratch",
+              "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"
+            ]
+       in do
+            ruleCatchesNot "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3009" $ Text.unlines dockerFile
+
+    it "don't warn: BuildKit cache mount to apt lists directory" $ do
+      ruleCatchesNot
+        "DL3009"
+        "RUN --mount=type=cache,target=/var/lib/apt/lists \\\
+        \    apt-get update && apt-get install python"
+      onBuildRuleCatchesNot
+        "DL3009"
+        "RUN --mount=type=cache,target=/var/lib/apt/lists \\\
+        \    apt-get update && apt-get install python"
+
+    it "don't warn: BuildKit cache mount to apt directories 1" $ do
+      ruleCatchesNot
+        "DL3009"
+        "RUN --mount=type=cache,target=/var/lib/apt \\\
+        \    --mount=type=cache,target=/var/cache/apt \\\
+        \    rm -f /etc/apt/apt.conf.d/docker-clean && \\\
+        \    apt-get update && apt-get install python"
+      onBuildRuleCatchesNot
+        "DL3009"
+        "RUN --mount=type=cache,target=/var/lib/apt \\\
+        \    --mount=type=cache,target=/var/cache/apt \\\
+        \    rm -f /etc/apt/apt.conf.d/docker-clean && \\\
+        \    apt-get update && apt-get install python"
+
+    it "don't warn: BuildKit cache mount to apt directories 2" $ do
+      let dockerFile =
+            [ "RUN rm -f /etc/apt/apt.conf.d/docker-clean",
+              "RUN --mount=type=cache,target=/var/cache/apt \\",
+              "    --mount=type=cache,target=/var/lib/apt \\",
+              "    apt-get update && apt-get install foo"
+            ]
+      ruleCatchesNot "DL3009" $ Text.unlines dockerFile
+      onBuildRuleCatchesNot "DL3009" $ Text.unlines dockerFile
+
+    it "warn: BuildKit cache mount to apt cache directory only" $ do
+      let dockerFile =
+            [ "RUN --mount=type=cache,target=/var/cache/apt \\",
+              "    rm -f /etc/apt/apt.conf.d/docker-clean && \\",
+              "    apt-get update && apt-get install python"
+            ]
+       in do
+            ruleCatches "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+
+    it "warn: BuildKit cache mount to apt lists directory only" $ do
+      let dockerFile =
+            [ "RUN rm -f /etc/apt/apt.conf.d/docker-clean",
+              "RUN --mount=type=cache,target=/var/lib/apt \\",
+              "    apt-get update && apt-get install python"
+            ]
+       in do
+            ruleCatches "DL3009" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3010Spec.hs b/test/Hadolint/Rule/DL3010Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3010Spec.hs
@@ -0,0 +1,61 @@
+module Hadolint.Rule.DL3010Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3010 - Use `ADD` for extracting archives into an image" $ do
+    it "catch: copy archive then extract 1" $
+      let dockerFile =
+            [ "COPY packaged-app.tar /usr/src/app",
+              "RUN tar -xf /usr/src/app/packaged-app.tar"
+            ]
+      in do
+        ruleCatches "DL3010" $ Text.unlines dockerFile
+    it "catch: copy archive then extract 2" $
+      let dockerFile =
+            [ "COPY packaged-app.tar /usr/src/app",
+              "WORKDIR /usr/src/app",
+              "RUN foo bar && echo something && tar -xf packaged-app.tar"
+            ]
+      in do
+        ruleCatches "DL3010" $ Text.unlines dockerFile
+    it "catch: copy archive then extract 3" $
+      let dockerFile =
+            [ "COPY foo/bar/packaged-app.tar /foo.tar",
+              "RUN tar -xf /foo.tar"
+            ]
+      in do
+        ruleCatches "DL3010" $ Text.unlines dockerFile
+    it "catch: copy archive then extract windows paths 1" $
+      let dockerFile =
+            [ "COPY build\\foo\\bar.tar.gz \"C:\\Program Files\\Foo\"",
+              "RUN tar -xf \"C:\\Program Files\\Foo\\bar.tar.gz\""
+            ]
+      in do
+        ruleCatches "DL3010" $ Text.unlines dockerFile
+    it "catch: copy archive then extract windows paths 2" $
+      let dockerFile =
+            [ "COPY build\\foo\\bar.tar.gz \"C:\\Program Files\\foo.tar.gz\"",
+              "RUN tar -xf foo.tar.gz"
+            ]
+      in do
+        ruleCatches "DL3010" $ Text.unlines dockerFile
+    it "ignore: copy archive without extract" $
+      let dockerFile =
+            [ "COPY packaged-app.tar /usr/src/app",
+              "FROM debian:11 as newstage"
+            ]
+      in do
+        ruleCatchesNot "DL3010" $ Text.unlines dockerFile
+    it "ignore: non archive" $
+      ruleCatchesNot "DL3010" "COPY package.json /usr/src/app"
+    it "ignore: copy from previous stage" $
+      ruleCatchesNot "DL3010"
+        "COPY --from=builder /usr/local/share/some.tar /opt/some.tar"
diff --git a/test/Hadolint/Rule/DL3011Spec.hs b/test/Hadolint/Rule/DL3011Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3011Spec.hs
@@ -0,0 +1,14 @@
+module Hadolint.Rule.DL3011Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "EXPOSE rules" $ do
+    it "invalid port" $ ruleCatches "DL3011" "EXPOSE 80000"
+    it "valid port" $ ruleCatchesNot "DL3011" "EXPOSE 60000"
diff --git a/test/Hadolint/Rule/DL3012Spec.hs b/test/Hadolint/Rule/DL3012Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3012Spec.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3012Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3012 - Multiple `HEALTHCHECK` instructions" $ do
+    it "ok with no HEALTHCHECK instruction" $
+      ruleCatchesNot "DL3012" "FROM scratch"
+    it "ok with one HEALTHCHECK instruction" $
+      ruleCatchesNot "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla"
+    it "ok with two HEALTHCHECK instructions in two stages" $
+      ruleCatchesNot "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla1\nFROM scratch\nHEALTHCHECK CMD /bin/bla2"
+    it "warn with two HEALTHCHECK instructions" $
+      ruleCatches "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla1\nHEALTHCHECK CMD /bin/bla2"
diff --git a/test/Hadolint/Rule/DL3013Spec.hs b/test/Hadolint/Rule/DL3013Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3013Spec.hs
@@ -0,0 +1,141 @@
+module Hadolint.Rule.DL3013Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3013 - Pin versions in pip." $ do
+    it "pip2 version not pinned" $ do
+      ruleCatches "DL3013" "RUN pip2 install MySQL_python"
+      onBuildRuleCatches "DL3013" "RUN pip2 install MySQL_python"
+    it "pip3 version not pinned" $ do
+      ruleCatches "DL3013" "RUN pip3 install MySQL_python"
+      onBuildRuleCatches "DL3013" "RUN pip2 install MySQL_python"
+    it "pip3 version pinned" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install MySQL_python==1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install MySQL_python==1.2.2"
+    it "pip3 install from local package" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install mypkg.whl"
+      ruleCatchesNot "DL3013" "RUN pip3 install mypkg.tar.gz"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install mypkg.whl"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install mypkg.tar.gz"
+    it "pip install requirements" $ do
+      ruleCatchesNot "DL3013" "RUN pip install -r requirements.txt"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install -r requirements.txt"
+    it "pip install requirements with long flag" $ do
+      ruleCatchesNot "DL3013" "RUN pip install --requirement requirements.txt"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install --requirement requirements.txt"
+    it "pip install use setup.py" $ do
+      ruleCatchesNot "DL3013" "RUN pip install ."
+      onBuildRuleCatchesNot "DL3013" "RUN pip install ."
+    it "pip version not pinned" $ do
+      ruleCatches "DL3013" "RUN pip install MySQL_python"
+      onBuildRuleCatches "DL3013" "RUN pip install MySQL_python"
+    it "pip version pinned" $ do
+      ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2"
+    it "pip version pinned with ~= operator" $ do
+      ruleCatchesNot "DL3013" "RUN pip install MySQL_python~=1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python~=1.2.2"
+    it "pip version pinned with === operator" $ do
+      ruleCatchesNot "DL3013" "RUN pip install MySQL_python===1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python===1.2.2"
+    it "pip version pinned with flag --ignore-installed" $ do
+      ruleCatchesNot "DL3013" "RUN pip install --ignore-installed MySQL_python==1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install --ignore-installed MySQL_python==1.2.2"
+    it "pip version pinned with flag --build" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
+    it "pip version pinned with flag --prefix" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
+    it "pip version pinned with flag --root" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
+    it "pip version pinned with flag --target" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
+    it "pip version pinned with flag --trusted-host" $ do
+      ruleCatchesNot "DL3013" "RUN pip3 install --trusted-host host example==1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN pip3 install --trusted-host host example==1.2.2"
+    it "pip version pinned with python -m" $ do
+      ruleCatchesNot "DL3013" "RUN python -m pip install example==1.2.2"
+      onBuildRuleCatchesNot "DL3013" "RUN python -m pip install example==1.2.2"
+    it "pip version not pinned with python -m" $ do
+      ruleCatches "DL3013" "RUN python -m pip install example"
+      onBuildRuleCatches "DL3013" "RUN python -m pip install --index-url url example"
+    it "pip install git" $ do
+      ruleCatchesNot
+        "DL3013"
+        "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
+      onBuildRuleCatchesNot
+        "DL3013"
+        "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
+    it "pip install unversioned git" $ do
+      ruleCatches
+        "DL3013"
+        "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
+      onBuildRuleCatches
+        "DL3013"
+        "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
+    it "pip install upper bound" $ do
+      ruleCatchesNot "DL3013" "RUN pip install 'alabaster>=0.7'"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster>=0.7'"
+    it "pip install lower bound" $ do
+      ruleCatchesNot "DL3013" "RUN pip install 'alabaster<0.7'"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster<0.7'"
+    it "pip install excluded version" $ do
+      ruleCatchesNot "DL3013" "RUN pip install 'alabaster!=0.7'"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster!=0.7'"
+    it "pip install user directory" $ do
+      ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --user"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --user"
+    it "pip install no pip version check" $ do
+      ruleCatchesNot
+        "DL3013"
+        "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
+      onBuildRuleCatchesNot
+        "DL3013"
+        "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
+    it "pip install --index-url" $ do
+      ruleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+      onBuildRuleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+    it "pip install index-url with -i flag" $ do
+      ruleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+      onBuildRuleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
+    it "pip install --index-url with --extra-index-url" $ do
+      ruleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
+      onBuildRuleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
+    it "pip install no cache dir" $ do
+      ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
+    it "pip install constraints file - long version argument" $ do
+      ruleCatchesNot "DL3013" "RUN pip install pykafka --constraint http://foo.bar.baz"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install pykafka --constraint http://foo.bar.baz"
+    it "pip install constraints file - short version argument" $ do
+      ruleCatchesNot "DL3013" "RUN pip install pykafka -c http://foo.bar.baz"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install pykafka -c http://foo.bar.baz"
+    it "pip install --index-url with --extra-index-url with basic auth" $ do
+      ruleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
+      onBuildRuleCatchesNot
+        "DL3013"
+        "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"
diff --git a/test/Hadolint/Rule/DL3014Spec.hs b/test/Hadolint/Rule/DL3014Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3014Spec.hs
@@ -0,0 +1,33 @@
+module Hadolint.Rule.DL3014Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3014 - Use the -y switch." $ do
+    it "apt-get auto yes" $ do
+      ruleCatches "DL3014" "RUN apt-get install python"
+      onBuildRuleCatches "DL3014" "RUN apt-get install python"
+    it "apt-get yes shortflag" $ do
+      ruleCatchesNot "DL3014" "RUN apt-get install -yq python"
+      onBuildRuleCatchesNot "DL3014" "RUN apt-get install -yq python"
+    it "apt-get yes quiet level 2 implies -y" $ do
+      ruleCatchesNot "DL3014" "RUN apt-get install -qq python"
+      onBuildRuleCatchesNot "DL3014" "RUN apt-get install -qq python"
+    it "apt-get yes different pos" $ do
+      ruleCatchesNot "DL3014" "RUN apt-get install -y python"
+      onBuildRuleCatchesNot "DL3014" "RUN apt-get install -y python"
+    it "apt-get with auto yes" $ do
+      ruleCatchesNot "DL3014" "RUN apt-get -y install python"
+      onBuildRuleCatchesNot "DL3014" "RUN apt-get -y install python"
+    it "apt-get with auto expanded yes" $ do
+      ruleCatchesNot "DL3014" "RUN apt-get --yes install python"
+      onBuildRuleCatchesNot "DL3014" "RUN apt-get --yes install python"
+    it "apt-get with assume-yes" $ do
+      ruleCatchesNot "DL3014" "RUN apt-get --assume-yes install python"
+      onBuildRuleCatchesNot "DL3014" "RUN apt-get --assume-yes install python"
diff --git a/test/Hadolint/Rule/DL3015Spec.hs b/test/Hadolint/Rule/DL3015Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3015Spec.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3015Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3015 - Avoid additional packages by specifying `--no-install-recommends`." $ do
+    it "apt-get install recommends" $ do
+      ruleCatchesNot
+        "DL3015"
+        "RUN apt-get install --no-install-recommends python"
+      onBuildRuleCatchesNot
+        "DL3015"
+        "RUN apt-get install --no-install-recommends python"
+    it "apt-get no install recommends" $ do
+      ruleCatches "DL3015" "RUN apt-get install python"
+      onBuildRuleCatches "DL3015" "RUN apt-get install python"
+    it "apt-get no install recommends" $ do
+      ruleCatches "DL3015" "RUN apt-get -y install python"
+      onBuildRuleCatches "DL3015" "RUN apt-get -y install python"
+    it "apt-get no install recommends via option" $ do
+      ruleCatchesNot "DL3015" "RUN apt-get -o APT::Install-Recommends=false install python"
+      onBuildRuleCatchesNot "DL3015" "RUN apt-get -o APT::Install-Recommends=false install python"
diff --git a/test/Hadolint/Rule/DL3016Spec.hs b/test/Hadolint/Rule/DL3016Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3016Spec.hs
@@ -0,0 +1,124 @@
+module Hadolint.Rule.DL3016Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+  describe "DL3016 - Pin versions in npm." $ do
+    it "version pinned in package.json" $ do
+      ruleCatchesNot "DL3016" "RUN npm install"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install"
+    it "version pinned in package.json with arguments" $ do
+      ruleCatchesNot "DL3016" "RUN npm install --progress=false"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install --progress=false"
+    it "version pinned" $ do
+      ruleCatchesNot "DL3016" "RUN npm install express@4.1.1"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install express@4.1.1"
+    it "version pinned with scope" $ do
+      ruleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0\""
+      onBuildRuleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0\""
+    it "version pinned multiple packages" $ do
+      ruleCatchesNot "DL3016" "RUN npm install express@\"4.1.1\" sax@0.1.1"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install express@\"4.1.1\" sax@0.1.1"
+    it "version pinned with --global" $ do
+      ruleCatchesNot "DL3016" "RUN npm install --global express@\"4.1.1\""
+      onBuildRuleCatchesNot "DL3016" "RUN npm install --global express@\"4.1.1\""
+    it "version pinned with -g" $ do
+      ruleCatchesNot "DL3016" "RUN npm install -g express@\"4.1.1\""
+      onBuildRuleCatchesNot "DL3016" "RUN npm install -g express@\"4.1.1\""
+    it "version does not have to be pinned for tarball suffix .tar" $ do
+      ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar"
+    it "version does not have to be pinned for tarball suffix .tar.gz" $ do
+      ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar.gz"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar.gz"
+    it "version does not have to be pinned for tarball suffix .tgz" $ do
+      ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tgz"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tgz"
+    it "version does not have to be pinned for folder - absolute path" $ do
+      ruleCatchesNot "DL3016" "RUN npm install /folder"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install /folder"
+    it "version does not have to be pinned for folder - relative path from current folder" $ do
+      ruleCatchesNot "DL3016" "RUN npm install ./folder"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install ./folder"
+    it "version does not have to be pinned for folder - relative path to parent folder" $ do
+      ruleCatchesNot "DL3016" "RUN npm install ../folder"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install ../folder"
+    it "version does not have to be pinned for folder - relative path from home" $ do
+      ruleCatchesNot "DL3016" "RUN npm install ~/folder"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install ~/folder"
+    it "commit pinned for git+ssh" $ do
+      ruleCatchesNot
+        "DL3016"
+        "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
+      onBuildRuleCatchesNot
+        "DL3016"
+        "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
+    it "commit pinned for git+http" $ do
+      ruleCatchesNot
+        "DL3016"
+        "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
+      onBuildRuleCatchesNot
+        "DL3016"
+        "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
+    it "commit pinned for git+https" $ do
+      ruleCatchesNot
+        "DL3016"
+        "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
+      onBuildRuleCatchesNot
+        "DL3016"
+        "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
+    it "commit pinned for git" $ do
+      ruleCatchesNot
+        "DL3016"
+        "RUN npm install git://github.com/npm/npm.git#v1.0.27"
+      onBuildRuleCatchesNot
+        "DL3016"
+        "RUN npm install git://github.com/npm/npm.git#v1.0.27"
+    it "npm run install is fine" $ do
+      ruleCatchesNot
+        "DL3016"
+        "RUN npm run --crazy install"
+      onBuildRuleCatchesNot
+        "DL3016"
+        "RUN npm run --crazy install"
+
+    --version range is not supported
+    it "version pinned with scope" $ do
+      ruleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
+      onBuildRuleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
+    it "version not pinned" $ do
+      ruleCatches "DL3016" "RUN npm install express"
+      onBuildRuleCatches "DL3016" "RUN npm install express"
+    it "version not pinned with scope" $ do
+      ruleCatches "DL3016" "RUN npm install @myorg/privatepackage"
+      onBuildRuleCatches "DL3016" "RUN npm install @myorg/privatepackage"
+    it "version not pinned multiple packages" $ do
+      ruleCatches "DL3016" "RUN npm install express sax@0.1.1"
+      onBuildRuleCatches "DL3016" "RUN npm install express sax@0.1.1"
+    it "version not pinned with --global" $ do
+      ruleCatches "DL3016" "RUN npm install --global express"
+      onBuildRuleCatches "DL3016" "RUN npm install --global express"
+    it "commit not pinned for git+ssh" $ do
+      ruleCatches "DL3016" "RUN npm install git+ssh://git@github.com:npm/npm.git"
+      onBuildRuleCatches "DL3016" "RUN npm install git+ssh://git@github.com:npm/npm.git"
+    it "commit not pinned for git+http" $ do
+      ruleCatches "DL3016" "RUN npm install git+http://isaacs@github.com/npm/npm"
+      onBuildRuleCatches "DL3016" "RUN npm install git+http://isaacs@github.com/npm/npm"
+    it "commit not pinned for git+https" $ do
+      ruleCatches
+        "DL3016"
+        "RUN npm install git+https://isaacs@github.com/npm/npm.git"
+      onBuildRuleCatches
+        "DL3016"
+        "RUN npm install git+https://isaacs@github.com/npm/npm.git"
+    it "commit not pinned for git" $ do
+      ruleCatches "DL3016" "RUN npm install git://github.com/npm/npm.git"
+      onBuildRuleCatches "DL3016" "RUN npm install git://github.com/npm/npm.git"
+    it "don't fire on loglevel flag" $ do
+      ruleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
diff --git a/test/Hadolint/Rule/DL3018Spec.hs b/test/Hadolint/Rule/DL3018Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3018Spec.hs
@@ -0,0 +1,92 @@
+module Hadolint.Rule.DL3018Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3018 - Pin versions in `apk add`." $ do
+    it "apk add version pinning single" $ do
+      ruleCatches "DL3018" "RUN apk add flex"
+      onBuildRuleCatches "DL3018" "RUN apk add flex"
+    it "apk add no version pinning single" $ do
+      ruleCatchesNot "DL3018" "RUN apk add flex=2.6.4-r1"
+      onBuildRuleCatchesNot "DL3018" "RUN apk add flex=2.6.4-r1"
+    it "apk add version pinned chained" $
+      let dockerFile =
+            [ "RUN apk add --no-cache flex=2.6.4-r1 \\",
+              " && pip install -r requirements.txt"
+            ]
+       in do
+            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+    it "apk add version pinned regression" $
+      let dockerFile =
+            [ "RUN apk add --no-cache \\",
+              "flex=2.6.4-r1 \\",
+              "libffi=3.2.1-r3 \\",
+              "python2=2.7.13-r1 \\",
+              "libbz2=1.0.6-r5"
+            ]
+       in do
+            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+    it "apk add version pinned regression - one missed" $
+      let dockerFile =
+            [ "RUN apk add --no-cache \\",
+              "flex=2.6.4-r1 \\",
+              "libffi \\",
+              "python2=2.7.13-r1 \\",
+              "libbz2=1.0.6-r5"
+            ]
+       in do
+            ruleCatches "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3018" $ Text.unlines dockerFile
+    it "apk add virtual package" $
+      let dockerFile =
+            [ "RUN apk add \\",
+              "--virtual build-dependencies \\",
+              "python-dev=1.1.1 build-base=2.2.2 wget=3.3.3 \\",
+              "&& pip install -r requirements.txt \\",
+              "&& python setup.py install \\",
+              "&& apk del build-dependencies"
+            ]
+       in do
+            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+    it "apk add with repository without equal sign" $
+      let dockerFile =
+            [ "RUN apk add --no-cache \\",
+              "--repository https://nl.alpinelinux.org/alpine/edge/testing \\",
+              "flow=0.78.0-r0"
+            ]
+       in do
+            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+    it "apk add with repository with equal sign" $
+      let dockerFile =
+            [ "RUN apk add --no-cache \\",
+              "--repository=https://nl.alpinelinux.org/alpine/edge/testing \\",
+              "flow=0.78.0-r0"
+            ]
+       in do
+            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+    it "apk add with repository (-X) without equal sign" $
+      let dockerFile =
+            [ "RUN apk add --no-cache \\",
+              "-X https://nl.alpinelinux.org/alpine/edge/testing \\",
+              "flow=0.78.0-r0"
+            ]
+       in do
+            ruleCatchesNot "DL3018" $ Text.unlines dockerFile
+            onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+
+    it "don't trigger when installing from .apk file" $ do
+      ruleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"
+      onBuildRuleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"
diff --git a/test/Hadolint/Rule/DL3019Spec.hs b/test/Hadolint/Rule/DL3019Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3019Spec.hs
@@ -0,0 +1,27 @@
+module Hadolint.Rule.DL3019Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3019 - Use the --no-cache switch." $ do
+    it "apk add with --no-cache" $ do
+      ruleCatches "DL3019" "RUN apk add flex=2.6.4-r1"
+      onBuildRuleCatches "DL3019" "RUN apk add flex=2.6.4-r1"
+    it "apk add without --no-cache" $ do
+      ruleCatchesNot "DL3019" "RUN apk add --no-cache flex=2.6.4-r1"
+      onBuildRuleCatchesNot "DL3019" "RUN apk add --no-cache flex=2.6.4-r1"
+    it "don't warn: apk add with BuildKit cache mount" $ do
+      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"
+      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"
+    it "don't warn: apk add with BuildKit cache mount in wrong dir and --no-cache" $ do
+      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"
+      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"
+    it "warn: apk add with BuildKit cache mount to wrong dir" $ do
+      ruleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"
+      onBuildRuleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"
diff --git a/test/Hadolint/Rule/DL3020Spec.hs b/test/Hadolint/Rule/DL3020Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3020Spec.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3020Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3020 - Use `COPY` instead of `ADD` for files and folders." $ do
+    it "add for tar" $ ruleCatchesNot "DL3020" "ADD file.tar /usr/src/app/"
+    it "add for gzip" $ ruleCatchesNot "DL3020" "ADD file.gz /usr/src/app/"
+    it "add for bz2" $ ruleCatchesNot "DL3020" "ADD file.bz2 /usr/src/app/"
+    it "add for xz" $ ruleCatchesNot "DL3020" "ADD file.xz /usr/src/app/"
+    it "add for tgz" $ ruleCatchesNot "DL3020" "ADD file.tgz /usr/src/app/"
+    it "add for url" $ ruleCatchesNot "DL3020" "ADD http://file.com /usr/src/app/"
+    it "using add" $ ruleCatches "DL3020" "ADD file /usr/src/app/"
+    it "warn for zip" $ ruleCatches "DL3020" "ADD file.zip /usr/src/app/"
+    it "add for tgz with quotes" $ ruleCatchesNot "DL3020" "ADD \"file.tgz\" /usr/src/app/"
+    it "add for url with quotes" $ ruleCatchesNot "DL3020" "ADD \"http://file.com\" /usr/src/app/"
+    it "warn for zip with quotes" $ ruleCatches "DL3020" "ADD \"file.zip\" /usr/src/app/"
diff --git a/test/Hadolint/Rule/DL3021Spec.hs b/test/Hadolint/Rule/DL3021Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3021Spec.hs
@@ -0,0 +1,15 @@
+module Hadolint.Rule.DL3021Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3021 - `COPY` with more than 2 arguments requires the last argument to end with `/`" $ do
+    it "no warn on 2 args" $ ruleCatchesNot "DL3021" "COPY foo bar"
+    it "warn on 3 args" $ ruleCatches "DL3021" "COPY foo bar baz"
+    it "no warn on 3 args" $ ruleCatchesNot "DL3021" "COPY foo bar baz/"
diff --git a/test/Hadolint/Rule/DL3022Spec.hs b/test/Hadolint/Rule/DL3022Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3022Spec.hs
@@ -0,0 +1,31 @@
+module Hadolint.Rule.DL3022Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3022 - `COPY --from` should reference a previously defined `FROM` alias" $ do
+    it "warn on missing alias" $ ruleCatches "DL3022" "COPY --from=foo bar ."
+    it "warn on alias defined after" $
+      let dockerFile =
+            [ "FROM scratch",
+              "COPY --from=build foo .",
+              "FROM node as build",
+              "RUN baz"
+            ]
+       in ruleCatches "DL3022" $ Text.unlines dockerFile
+    it "don't warn on correctly defined aliases" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "RUN foo",
+              "FROM node",
+              "COPY --from=build foo .",
+              "RUN baz"
+            ]
+       in ruleCatchesNot "DL3022" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3023Spec.hs b/test/Hadolint/Rule/DL3023Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3023Spec.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3023Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3023 - `COPY --from` cannot reference its own `FROM` alias" $ do
+    it "warn on copying from your the same FROM" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "COPY --from=foo bar ."
+            ]
+       in ruleCatches "DL3023" $ Text.unlines dockerFile
+    it "don't warn on copying from other sources" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "RUN foo",
+              "FROM node as run",
+              "COPY --from=build foo .",
+              "RUN baz"
+            ]
+       in ruleCatchesNot "DL3023" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3024Spec.hs b/test/Hadolint/Rule/DL3024Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3024Spec.hs
@@ -0,0 +1,29 @@
+module Hadolint.Rule.DL3024Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3024 - Duplicate aliases" $ do
+    it "warn on duplicate aliases" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN something",
+              "FROM scratch as foo",
+              "RUN something"
+            ]
+       in ruleCatches "DL3024" $ Text.unlines dockerFile
+    it "don't warn on unique aliases" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "RUN foo",
+              "FROM node as run",
+              "RUN baz"
+            ]
+       in ruleCatchesNot "DL3024" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3025Spec.hs b/test/Hadolint/Rule/DL3025Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3025Spec.hs
@@ -0,0 +1,38 @@
+module Hadolint.Rule.DL3025Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3025 - Use arguments JSON notation for `CMD` and `ENTRYPOINT` arguments" $ do
+    it "warn on ENTRYPOINT" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "ENTRYPOINT something"
+            ]
+       in ruleCatches "DL3025" $ Text.unlines dockerFile
+    it "don't warn on ENTRYPOINT json notation" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "ENTRYPOINT [\"foo\", \"bar\"]"
+            ]
+       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
+    it "warn on CMD" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "CMD something"
+            ]
+       in ruleCatches "DL3025" $ Text.unlines dockerFile
+    it "don't warn on CMD json notation" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "CMD [\"foo\", \"bar\"]",
+              "CMD [ \"foo\", \"bar\" ]"
+            ]
+       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3026Spec.hs b/test/Hadolint/Rule/DL3026Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3026Spec.hs
@@ -0,0 +1,62 @@
+module Hadolint.Rule.DL3026Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Hadolint (Configuration (..))
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL2036 - Use only an allowed registry in the FROM image" $ do
+    it "does not warn on empty allowed registries" $ do
+      let dockerFile =
+            [ "FROM random.com/debian"
+            ]
+      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
+
+    it "warn on non-allowed registry" $ do
+      let dockerFile =
+            [ "FROM random.com/debian"
+            ]
+      let ?config = def { allowedRegistries = ["docker.io"] }
+
+      ruleCatches "DL3026" $ Text.unlines dockerFile
+
+    it "does not warn on allowed registries" $ do
+      let dockerFile =
+            [ "FROM random.com/debian"
+            ]
+      let ?config = def { allowedRegistries = ["x.com", "random.com"] }
+
+      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
+
+    it "doesn't warn on scratch image" $ do
+      let dockerFile =
+            [ "FROM scratch"
+            ]
+      let ?config = def { allowedRegistries = ["x.com", "random.com"] }
+
+      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
+
+    it "allows boths all forms of docker.io" $ do
+      let dockerFile =
+            [ "FROM ubuntu:18.04 AS builder1",
+              "FROM zemanlx/ubuntu:18.04 AS builder2",
+              "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
+            ]
+      let ?config = def { allowedRegistries = ["docker.io"] }
+
+      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
+
+    it "allows using previous stages" $ do
+      let dockerFile =
+            [ "FROM random.com/foo AS builder1",
+              "FROM builder1 AS builder2"
+            ]
+      let ?config = def { allowedRegistries = ["random.com"] }
+
+      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3027Spec.hs b/test/Hadolint/Rule/DL3027Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3027Spec.hs
@@ -0,0 +1,21 @@
+module Hadolint.Rule.DL3027Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3027 - Do not use `apt` as it is meant to be a end-user tool, use `apt-get` or `apt-cache` instead" $ do
+    it "apt" $
+      let dockerFile =
+            [ "FROM ubuntu",
+              "RUN apt install python"
+            ]
+       in do
+            ruleCatches "DL3027" $ Text.unlines dockerFile
+            onBuildRuleCatches "DL3027" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3028Spec.hs b/test/Hadolint/Rule/DL3028Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3028Spec.hs
@@ -0,0 +1,44 @@
+module Hadolint.Rule.DL3028Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3028 - Pin versions in gem install." $
+    describe "version pinning" $ do
+      describe "i" $ do
+        it "unpinned" $ do
+          ruleCatches "DL3028" "RUN gem i bundler"
+          onBuildRuleCatches "DL3028" "RUN gem i bundler"
+        it "pinned" $ do
+          ruleCatchesNot "DL3028" "RUN gem i bundler:1"
+          onBuildRuleCatchesNot "DL3028" "RUN gem i bundler:1"
+        it "multi" $ do
+          ruleCatches "DL3028" "RUN gem i bunlder:1 nokogiri"
+          onBuildRuleCatches "DL3028" "RUN gem i bunlder:1 nokogiri"
+          ruleCatchesNot "DL3028" "RUN gem i bunlder:1 nokogirii:1"
+          onBuildRuleCatchesNot "DL3028" "RUN gem i bunlder:1 nokogiri:1"
+      describe "install" $ do
+        it "unpinned" $ do
+          ruleCatches "DL3028" "RUN gem install bundler"
+          onBuildRuleCatches "DL3028" "RUN gem install bundler"
+        it "pinned" $ do
+          ruleCatchesNot "DL3028" "RUN gem install bundler:1"
+          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler:1"
+        it "does not warn on -v" $ do
+          ruleCatchesNot "DL3028" "RUN gem install bundler -v '2.0.1'"
+          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler -v '2.0.1'"
+        it "does not warn on --version without =" $ do
+          ruleCatchesNot "DL3028" "RUN gem install bundler --version '2.0.1'"
+          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler --version '2.0.1'"
+        it "does not warn on --version with =" $ do
+          ruleCatchesNot "DL3028" "RUN gem install bundler --version='2.0.1'"
+          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler --version='2.0.1'"
+        it "does not warn on extra flags" $ do
+          ruleCatchesNot "DL3028" "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
+          onBuildRuleCatchesNot "DL3028" "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
diff --git a/test/Hadolint/Rule/DL3029Spec.hs b/test/Hadolint/Rule/DL3029Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3029Spec.hs
@@ -0,0 +1,14 @@
+module Hadolint.Rule.DL3029Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3029 - Do not use --platform flag with FROM." $ do
+    it "explicit platform flag" $ ruleCatches "DL3029" "FROM --platform=linux debian:jessie"
+    it "no platform flag" $ ruleCatchesNot "DL3029" "FROM debian:jessie"
diff --git a/test/Hadolint/Rule/DL3030Spec.hs b/test/Hadolint/Rule/DL3030Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3030Spec.hs
@@ -0,0 +1,19 @@
+module Hadolint.Rule.DL3030Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+  describe "DL3030 - Use the `-y` switch to avoid manual input `yum install -y <package>`" $ do
+    it "not ok when not using `-y` switch" $ do
+      ruleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"
+      onBuildRuleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"
+    it "ok when using `-y` switch" $ do
+      ruleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"
+      ruleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
+      onBuildRuleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"
+      onBuildRuleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
diff --git a/test/Hadolint/Rule/DL3032Spec.hs b/test/Hadolint/Rule/DL3032Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3032Spec.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3032Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3032 - `yum clean all` missing after yum command." $ do
+    it "not ok with no clean all" $ do
+      ruleCatches "DL3032" "RUN yum install -y mariadb-10.4"
+      onBuildRuleCatches "DL3032" "RUN yum install -y mariadb-10.4"
+    it "ok with yum clean all " $ do
+      ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
+      ruleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
+      onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
+      onBuildRuleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
diff --git a/test/Hadolint/Rule/DL3033Spec.hs b/test/Hadolint/Rule/DL3033Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3033Spec.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3033Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3033 - Specify version with `yum install -y <package>-<version>`" $ do
+    it "not ok wihout yum version pinning" $ do
+      ruleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"
+      onBuildRuleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"
+    it "ok with yum version pinning" $ do
+      ruleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"
+      ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
+      onBuildRuleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"
+      onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
+    it "not ok wihout yum version pinning - modules" $ do
+      ruleCatches "DL3033" "RUN yum module install -y tomcat && yum clean all"
+      onBuildRuleCatches "DL3033" "RUN yum module install -y tomcat && yum clean all"
+    it "ok with yum version pinning - modules" $ do
+      ruleCatchesNot "DL3033" "RUN yum module install -y tomcat:9 && yum clean all"
+      ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
+      onBuildRuleCatchesNot "DL3033" "RUN yum module install -y tomcat:9 && yum clean all"
+      onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
diff --git a/test/Hadolint/Rule/DL3034Spec.hs b/test/Hadolint/Rule/DL3034Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3034Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3034Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3034 - Non-interactive switch missing from zypper command: `zypper install -y`" $ do
+    it "not ok without non-interactive switch" $ do
+      ruleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"
+      onBuildRuleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"
+    it "ok with non-interactive switch present" $ do
+      ruleCatchesNot "DL3034" "RUN zypper install -n httpd=2.4.24 && zypper clean"
+      ruleCatchesNot "DL3034" "RUN zypper install --non-interactive httpd=2.4.24 && zypper clean"
+      ruleCatchesNot "DL3034" "RUN zypper install -y httpd=2.4.24 && zypper clean"
+      ruleCatchesNot "DL3034" "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"
+      onBuildRuleCatchesNot "DL3034" "RUN zypper install -n httpd=2.4.24 && zypper clean"
+      onBuildRuleCatchesNot "DL3034" "RUN zypper install --non-interactive httpd=2.4.24 && zypper clean"
+      onBuildRuleCatchesNot "DL3034" "RUN zypper install -y httpd=2.4.24 && zypper clean"
+      onBuildRuleCatchesNot "DL3034" "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"
diff --git a/test/Hadolint/Rule/DL3035Spec.hs b/test/Hadolint/Rule/DL3035Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3035Spec.hs
@@ -0,0 +1,17 @@
+module Hadolint.Rule.DL3035Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3035" $ do
+    it "not ok: zypper dist-upgrade" $ do
+      ruleCatches "DL3035" "RUN zypper dist-upgrade"
+      ruleCatches "DL3035" "RUN zypper dup"
+      onBuildRuleCatches "DL3035" "RUN zypper dist-upgrade"
+      onBuildRuleCatches "DL3035" "RUN zypper dup"
diff --git a/test/Hadolint/Rule/DL3036Spec.hs b/test/Hadolint/Rule/DL3036Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3036Spec.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3036Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3036 - `zypper clean` missing after zypper use." $ do
+    it "not ok without zypper clean" $ do
+      ruleCatches "DL3036" "RUN zypper install -y mariadb=10.4"
+      onBuildRuleCatches "DL3036" "RUN zypper install -y mariadb=10.4"
+    it "ok with zypper clean" $ do
+      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
+      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
+      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
+      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
diff --git a/test/Hadolint/Rule/DL3037Spec.hs b/test/Hadolint/Rule/DL3037Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3037Spec.hs
@@ -0,0 +1,30 @@
+module Hadolint.Rule.DL3037Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3037 - Specify version with `zypper install -y <package>[=]<version>`." $ do
+    -- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'
+    -- and '<' have to be escaped. Hence the double escaping.
+    it "not ok without zypper version pinning" $ do
+      ruleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"
+      onBuildRuleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"
+    it "ok with different variants of zypper version pinning" $ do
+      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat=9.0.39 && zypper clean"
+      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>=9.0 && zypper clean"
+      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>9.0 && zypper clean"
+      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<=9.0 && zypper clean"
+      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<9.0 && zypper clean"
+      ruleCatchesNot "DL3037" "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"
+      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat=9.0.39 && zypper clean"
+      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>=9.0 && zypper clean"
+      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>9.0 && zypper clean"
+      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<=9.0 && zypper clean"
+      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<9.0 && zypper clean"
+      onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"
diff --git a/test/Hadolint/Rule/DL3038Spec.hs b/test/Hadolint/Rule/DL3038Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3038Spec.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3038Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3038 - Use the `-y` switch to avoid manual input `dnf install -y <package>`" $ do
+    it "not ok without dnf non-interactive flag" $ do
+      ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
+      onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
+    it "ok with dnf non-interactive flag" $ do
+      ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
+      ruleCatchesNot "DL3038" "RUN notdnf install httpd"
+      onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
+      onBuildRuleCatchesNot "DL3038" "RUN notdnf install httpd"
diff --git a/test/Hadolint/Rule/DL3040Spec.hs b/test/Hadolint/Rule/DL3040Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3040Spec.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3040Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3040 - `dnf clean all` missing after dnf command." $ do
+    it "no ok without dnf clean all" $ do
+      ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
+      onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
+    it "ok with dnf clean all" $ do
+      ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN notdnf install mariadb"
+      onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
+      onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"
diff --git a/test/Hadolint/Rule/DL3041Spec.hs b/test/Hadolint/Rule/DL3041Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3041Spec.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3041Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3041 - Specify version with `dnf install -y <package>-<version>`" $ do
+    it "not ok without dnf version pinning" $ do
+      ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
+      onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
+    it "ok with dnf version pinning" $ do
+      ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
+      ruleCatchesNot "DL3041" "RUN notdnf install tomcat"
+      onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
+      onBuildRuleCatchesNot "DL3041" "RUN notdnf install tomcat"
+    it "not ok without dnf version pinning - moudles" $ do
+      ruleCatches "DL3041" "RUN dnf module install -y tomcat && dnf clean all"
+      onBuildRuleCatches "DL3041" "RUN dnf module install -y tomcat && dnf clean all"
+    it "ok with dnf version pinning - modules" $ do
+      ruleCatchesNot "DL3041" "RUN dnf module install -y tomcat:9 && dnf clean all"
+      ruleCatchesNot "DL3041" "RUN notdnf module install tomcat"
+      onBuildRuleCatchesNot "DL3041" "RUN dnf module install -y tomcat:9 && dnf clean all"
+      onBuildRuleCatchesNot "DL3041" "RUN notdnf module install tomcat"
diff --git a/test/Hadolint/Rule/DL3042Spec.hs b/test/Hadolint/Rule/DL3042Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3042Spec.hs
@@ -0,0 +1,88 @@
+module Hadolint.Rule.DL3042Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3042 - Avoid cache directory with `pip install --no-cache-dir <package>`." $ do
+    it "pip2 --no-cache-dir not used" $ do
+      ruleCatches "DL3042" "RUN pip2 install MySQL_python"
+      onBuildRuleCatches "DL3042" "RUN pip2 install MySQL_python"
+    it "pip3 --no-cache-dir not used" $ do
+      ruleCatches "DL3042" "RUN pip3 install MySQL_python"
+      onBuildRuleCatches "DL3042" "RUN pip3 install MySQL_python"
+    it "pip --no-cache-dir not used" $ do
+      ruleCatches "DL3042" "RUN pip install MySQL_python"
+      onBuildRuleCatches "DL3042" "RUN pip install MySQL_python"
+    it "pip2 --no-cache-dir used" $ do
+      ruleCatchesNot "DL3042" "RUN pip2 install MySQL_python --no-cache-dir"
+      onBuildRuleCatchesNot "DL3042" "RUN pip2 install MySQL_python --no-cache-dir"
+    it "pip3 --no-cache-dir used" $ do
+      ruleCatchesNot "DL3042" "RUN pip3 install --no-cache-dir MySQL_python"
+      onBuildRuleCatchesNot "DL3042" "RUN pip3 install --no-cache-dir MySQL_python"
+    it "pip --no-cache-dir used" $ do
+      ruleCatchesNot "DL3042" "RUN pip install MySQL_python --no-cache-dir"
+      onBuildRuleCatchesNot "DL3042" "RUN pip install MySQL_python --no-cache-dir"
+    it "don't match on pipx" $ do
+      ruleCatchesNot "DL3042" "RUN pipx install software"
+      onBuildRuleCatchesNot "DL3042" "Run pipx install software"
+    it "don't match on pipenv" $ do
+      ruleCatchesNot "DL3042" "RUN pipenv install library"
+      onBuildRuleCatchesNot "DL3042" "RUN pipenv install library"
+
+    -- ENV won't work with the onBuildRuleCatches[Not] wrapper:
+    it "respect ENV PIP_NO_CACHE_DIR with truthy values" $ do
+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=1\nRUN pip install MySQL_python"
+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=on\nRUN pip install MySQL_python"
+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=yes\nRUN pip install MySQL_python"
+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=true\nRUN pip install MySQL_python"
+    it "respect RUN PIP_NO_CACHE_DIR=... with truthy values" $ do
+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=1 pip install MySQL_python"
+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=on pip install MySQL_python"
+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=yes pip install MySQL_python"
+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=true pip install MySQL_python"
+    it "respect RUN export PIP_NO_CACHE_DIR=... with truthy values" $ do
+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=1 && pip install MySQL_python"
+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=on && pip install MySQL_python"
+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=yes && pip install MySQL_python"
+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=true && pip install MySQL_python"
+    it "respect ENV PIP_NO_CACHE_DIR with falsy values" $ do
+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=0\nRUN pip install MySQL_python"
+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=off\nRUN pip install MySQL_python"
+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=no\nRUN pip install MySQL_python"
+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=false\nRUN pip install MySQL_python"
+    it "respect RUN PIP_NO_CACHE_DIR=... with falsy values" $ do
+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=0 pip install MySQL_python"
+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=off pip install MySQL_python"
+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=no pip install MySQL_python"
+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=false pip install MySQL_python"
+    it "respect RUN export PIP_NO_CACHE_DIR=... with falsy values" $ do
+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=0 && pip install MySQL_python"
+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=off && pip install MySQL_python"
+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=no && pip install MySQL_python"
+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=false && pip install MySQL_python"
+
+    it "don't trigger if PIP_NO_CACHE_DIR is inherited" $
+      let dockerFile = Text.unlines
+            [ "FROM debian:buster as base",
+              "ENV PIP_NO_CACHE_DIR=1",
+              "FROM base",
+              "RUN pip install six"
+            ]
+       in do
+        ruleCatchesNot "DL3042" dockerFile
+    it "trigger if PIP_NO_CACHE_DIR is not inherited" $
+      let dockerFile = Text.unlines
+            [ "FROM debian:buster as base",
+              "ENV PIP_NO_CACHE_DIR=1",
+              "FROM debian:buster",
+              "RUN pip install six"
+            ]
+       in do
+        ruleCatches "DL3042" dockerFile
diff --git a/test/Hadolint/Rule/DL3043Spec.hs b/test/Hadolint/Rule/DL3043Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3043Spec.hs
@@ -0,0 +1,45 @@
+module Hadolint.Rule.DL3043Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3043 - `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction." $ do
+    it "error when using `ONBUILD` within `ONBUILD`" $
+      let dockerFile =
+            [ "ONBUILD ONBUILD RUN anything"
+            ]
+       in ruleCatches "DL3043" $ Text.unlines dockerFile
+    it "error when using `FROM` within `ONBUILD`" $
+      let dockerFile =
+            [ "ONBUILD FROM debian:buster"
+            ]
+       in ruleCatches "DL3043" $ Text.unlines dockerFile
+    it "error when using `MAINTAINER` within `ONBUILD`" $
+      let dockerFile =
+            [ "ONBUILD MAINTAINER \"BoJack Horseman\""
+            ]
+       in ruleCatches "DL3043" $ Text.unlines dockerFile
+    it "ok with `ADD`" $ ruleCatchesNot "DL3043" "ONBUILD ADD anything anywhere"
+    it "ok with `USER`" $ ruleCatchesNot "DL3043" "ONBUILD USER anything"
+    it "ok with `LABEL`" $ ruleCatchesNot "DL3043" "ONBUILD LABEL bla=\"blubb\""
+    it "ok with `STOPSIGNAL`" $ ruleCatchesNot "DL3043" "ONBUILD STOPSIGNAL anything"
+    it "ok with `COPY`" $ ruleCatchesNot "DL3043" "ONBUILD COPY anything anywhere"
+    it "ok with `RUN`" $ ruleCatchesNot "DL3043" "ONBUILD RUN anything"
+    it "ok with `CMD`" $ ruleCatchesNot "DL3043" "ONBUILD CMD anything"
+    it "ok with `SHELL`" $ ruleCatchesNot "DL3043" "ONBUILD SHELL anything"
+    it "ok with `WORKDIR`" $ ruleCatchesNot "DL3043" "ONBUILD WORKDIR anything"
+    it "ok with `EXPOSE`" $ ruleCatchesNot "DL3043" "ONBUILD EXPOSE 69"
+    it "ok with `VOLUME`" $ ruleCatchesNot "DL3043" "ONBUILD VOLUME anything"
+    it "ok with `ENTRYPOINT`" $ ruleCatchesNot "DL3043" "ONBUILD ENTRYPOINT anything"
+    it "ok with `ENV`" $ ruleCatchesNot "DL3043" "ONBUILD ENV MYVAR=\"bla\""
+    it "ok with `ARG`" $ ruleCatchesNot "DL3043" "ONBUILD ARG anything"
+    it "ok with `HEALTHCHECK`" $ ruleCatchesNot "DL3043" "ONBUILD HEALTHCHECK NONE"
+    it "ok with `FROM` outside of `ONBUILD`" $ ruleCatchesNot "DL3043" "FROM debian:buster"
+    it "ok with `MAINTAINER` outside of `ONBUILD`" $ ruleCatchesNot "DL3043" "MAINTAINER \"Some Guy\""
diff --git a/test/Hadolint/Rule/DL3044Spec.hs b/test/Hadolint/Rule/DL3044Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3044Spec.hs
@@ -0,0 +1,44 @@
+module Hadolint.Rule.DL3044Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3044 - Do not refer to an environment variable within the same `ENV` statement where it is defined." $ do
+    it "ok with normal ENV" $
+      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\"\nENV BLUBB=\"${BLA}/blubb\""
+    it "ok with partial match 1" $
+      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${FOOBLA}/blubb\""
+    it "ok with partial match 2" $
+      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLAFOO}/blubb\""
+    it "ok with partial match 3" $
+      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$FOOBLA/blubb\""
+    it "ok with partial match 4" $
+      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLAFOO/blubb\""
+    it "fail with partial match 5" $
+      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA/$BLAFOO/blubb\""
+    it "ok with parial match 6" $
+      ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"BLA/$BLAFOO/BLA\""
+    it "ok when previously defined in `ARG`" $
+      ruleCatchesNot "DL3044" "ARG BLA\nENV BLA=${BLA}"
+    it "ok when previously defined in `ENV`" $
+      ruleCatchesNot "DL3044" "ENV BLA blubb\nENV BLA=${BLA}"
+    it "ok with referencing a variable on its own right hand side" $
+      ruleCatchesNot "DL3044" "ENV PATH=/bla:${PATH}"
+    it "ok with referencing a variable on its own right side twice in different `ENV`s" $
+      ruleCatchesNot "DL3044" "ENV PATH=/bla:${PATH}\nENV PATH=/blubb:${PATH}"
+    it "fail when referencing a variable on its own right side twice within the same `ENV`" $
+      ruleCatches "DL3044" "ENV PATH=/bla:${PATH} PATH=/blubb:${PATH}"
+    it "fail with selfreferencing with curly braces ENV" $
+      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLA}/blubb\""
+    it "fail with selfreferencing without curly braces ENV" $
+      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA/blubb\""
+    it "fail with full match 1" $
+      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA\""
+    it "fail with full match 2" $
+      ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLA}\""
diff --git a/test/Hadolint/Rule/DL3045Spec.hs b/test/Hadolint/Rule/DL3045Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3045Spec.hs
@@ -0,0 +1,158 @@
+module Hadolint.Rule.DL3045Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3045 - `COPY` without `WORKDIR` set" $ do
+    it "ok: `COPY` with absolute destination and no `WORKDIR` set" $ do
+      ruleCatchesNot "DL3045" "COPY bla.sh /usr/local/bin/blubb.sh"
+      onBuildRuleCatchesNot "DL3045" "COPY bla.sh /usr/local/bin/blubb.sh"
+
+    it "ok: `COPY` with absolute destination and no `WORKDIR` set with quotes" $ do
+      ruleCatchesNot "DL3045" "COPY bla.sh \"/usr/local/bin/blubb.s\""
+      onBuildRuleCatchesNot "DL3045" "COPY bla.sh \"/usr/local/bin/blubb.sh\""
+
+    it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows" $ do
+      ruleCatchesNot "DL3045" "COPY bla.sh c:\\system32\\blubb.sh"
+      onBuildRuleCatchesNot "DL3045" "COPY bla.sh d:\\mypath\\blubb.sh"
+
+    it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows with quotes" $ do
+      ruleCatchesNot "DL3045" "COPY bla.sh \"c:\\system32\\blubb.sh\""
+      onBuildRuleCatchesNot "DL3045" "COPY bla.sh \"d:\\mypath\\blubb.sh\""
+
+    it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows with alternative paths" $ do
+      ruleCatchesNot "DL3045" "COPY bla.sh c:/system32/blubb.sh"
+      onBuildRuleCatchesNot "DL3045" "COPY bla.sh d:/mypath/blubb.sh"
+
+    it "ok: `COPY` with relative destination and `WORKDIR` set" $ do
+      ruleCatchesNot "DL3045" "FROM scratch\nWORKDIR /usr\nCOPY bla.sh blubb.sh"
+      onBuildRuleCatchesNot "FROM scratch\nDL3045" "WORKDIR /usr\nCOPY bla.sh blubb.sh"
+
+    it "ok: `COPY` with relative destination and `WORKDIR` set - windows" $ do
+      ruleCatchesNot "DL3045" "FROM scratch\nWORKDIR c:\\system32\nCOPY bla.sh blubb.sh"
+      onBuildRuleCatchesNot "DL3045" "FROM scratch\nWORKDIR c:\\system32\nCOPY bla.sh blubb.sh"
+
+    it "ok: `COPY` with destination being an environment variable 1" $ do
+      ruleCatchesNot "DL3045" "COPY src.sh ${SRC_BASE_ENV}"
+      onBuildRuleCatchesNot "DL3045" "COPY src.sh ${SRC_BASE_ENV}"
+
+    it "ok: `COPY` with destination being an environment variable 2" $ do
+      ruleCatchesNot "DL3045" "COPY src.sh $SRC_BASE_ENV"
+      onBuildRuleCatchesNot "DL3045" "COPY src.sh $SRC_BASE_ENV"
+
+    it "ok: `COPY` with destination being an environment variable 3" $ do
+      ruleCatchesNot "DL3045" "COPY src.sh \"${SRC_BASE_ENV}\""
+      onBuildRuleCatchesNot "DL3045" "COPY src.sh \"${SRC_BASE_ENV}\""
+
+    it "ok: `COPY` with destination being an environment variable 4" $ do
+      ruleCatchesNot "DL3045" "COPY src.sh \"$SRC_BASE_ENV\""
+      onBuildRuleCatchesNot "DL3045" "COPY src.sh \"$SRC_BASE_ENV\""
+
+    it "not ok: `COPY` with relative destination and no `WORKDIR` set" $ do
+      ruleCatches "DL3045" "COPY bla.sh blubb.sh"
+      onBuildRuleCatches "DL3045" "COPY bla.sh blubb.sh"
+
+    it "not ok: `COPY` with relative destination and no `WORKDIR` set with quotes" $ do
+      ruleCatches "DL3045" "COPY bla.sh \"blubb.sh\""
+      onBuildRuleCatches "DL3045" "COPY bla.sh \"blubb.sh\""
+
+    it "not ok: `COPY` to relative destination if `WORKDIR` is set in a previous stage but not inherited" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM debian:buster as stage1",
+                "WORKDIR /usr",
+                "FROM debian:buster",
+                "COPY foo bar"
+              ]
+       in do
+            ruleCatches "DL3045" dockerFile
+            onBuildRuleCatches "DL3045" dockerFile
+
+    it "not ok: `COPY` to relative destination if `WORKDIR` is set in a previous stage but not inherited - windows" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM microsoft/windowsservercore as stage1",
+                "WORKDIR c:\\system32",
+                "FROM microsoft/windowsservercore",
+                "COPY foo bar"
+              ]
+       in do
+            ruleCatches "DL3045" dockerFile
+            onBuildRuleCatches "DL3045" dockerFile
+
+    it "ok: `COPY` to relative destination if `WORKDIR` has been set in base image" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM debian:buster as base",
+                "WORKDIR /usr",
+                "FROM debian:buster as stage-inbetween",
+                "RUN foo",
+                "FROM base",
+                "COPY foo bar"
+              ]
+       in do
+            ruleCatchesNot "DL3045" dockerFile
+            onBuildRuleCatchesNot "DL3045" dockerFile
+
+    it "ok: `COPY` to relative destination if `WORKDIR` has been set in base image - windows" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM microsoft/windowsservercore as base",
+                "WORKDIR c:\\system32",
+                "FROM microsoft/windowsservercore as stage-inbetween",
+                "RUN foo",
+                "FROM base",
+                "COPY foo bar"
+              ]
+       in do
+            ruleCatchesNot "DL3045" dockerFile
+            onBuildRuleCatchesNot "DL3045" dockerFile
+
+    it "ok: `COPY` to relative destination if `WORKDIR` has been set in previous stage, deep case" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM debian:buster as base1",
+                "WORKDIR /usr",
+                "FROM base1 as base2",
+                "RUN foo",
+                "FROM base2",
+                "COPY foo bar"
+              ]
+       in do
+            ruleCatchesNot "DL3045" dockerFile
+            onBuildRuleCatchesNot "DL3045" dockerFile
+
+    it "ok: `COPY` to relative destination if `WORKDIR` has been set in previous stage, deep case - windows" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM microsoft/windowsservercore as base1",
+                "WORKDIR c:\\system32",
+                "FROM base1 as base2",
+                "RUN foo",
+                "FROM base2",
+                "COPY foo bar"
+              ]
+       in do
+            ruleCatchesNot "DL3045" dockerFile
+            onBuildRuleCatchesNot "DL3045" dockerFile
+
+    it "ok: `COPY` to relative destination if `WORKDIR` has been set, both within an `ONBUILD` context" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM debian:buster",
+                "ONBUILD WORKDIR /usr/local/lib",
+                "ONBUILD COPY foo bar"
+              ]
+       in do
+            ruleCatchesNot "DL3045" dockerFile
+            onBuildRuleCatchesNot "DL3045" dockerFile
+
+    it "regression: don't crash with single character paths" $ do
+      ruleCatches "DL3045" "COPY a b"
+      onBuildRuleCatches "DL3045" "COPY a b"
diff --git a/test/Hadolint/Rule/DL3046Spec.hs b/test/Hadolint/Rule/DL3046Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3046Spec.hs
@@ -0,0 +1,17 @@
+module Hadolint.Rule.DL3046Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3046 - `useradd` without flag `-l` and high UID will result in excessively large Image." $ do
+    it "ok with `useradd` alone" $ ruleCatchesNot "DL3046" "RUN useradd luser"
+    it "ok with `useradd` short uid" $ ruleCatchesNot "DL3046" "RUN useradd -u 12345 luser"
+    it "ok with `useradd` long uid and flag `-l`" $ ruleCatchesNot "DL3046" "RUN useradd -l -u 123456 luser"
+    it "ok with `useradd` and just flag `-l`" $ ruleCatchesNot "DL3046" "RUN useradd -l luser"
+    it "warn when `useradd` and long uid without flag `-l`" $ ruleCatches "DL3046" "RUN useradd -u 123456 luser"
diff --git a/test/Hadolint/Rule/DL3047Spec.hs b/test/Hadolint/Rule/DL3047Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3047Spec.hs
@@ -0,0 +1,73 @@
+module Hadolint.Rule.DL3047Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3047 - `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files." $ do
+    it "warns when using wget without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz"
+            ]
+       in ruleCatches "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget --progress=dot:giga my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with -q (quiet short option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget -q my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with --quiet (quiet long option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget --quiet my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with -nv (no-verbose short option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget -nv my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with --no-verbose (no-verbose long option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget --no-verbose my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with --output-file (output-file long option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget --output-file=/tmp/wget.log my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with -o (output-file long option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget -o /tmp/wget.log my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with --append-output (append-output long option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget --append-output=/tmp/wget.log my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
+    it "does not warn when running with -a (append-output long option) and without --progress option" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget -a /tmp/wget.log my.xyz"
+            ]
+       in ruleCatchesNot "DL3047" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/DL3048Spec.hs b/test/Hadolint/Rule/DL3048Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3048Spec.hs
@@ -0,0 +1,39 @@
+module Hadolint.Rule.DL3048Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3048 - Invalid Label Key Rule" $ do
+    it "not ok with reserved namespace" $ do
+      ruleCatches "DL3048" "LABEL com.docker.label=\"foo\""
+      ruleCatches "DL3048" "LABEL io.docker.label=\"foo\""
+      ruleCatches "DL3048" "LABEL org.dockerproject.label=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL com.docker.label=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL io.docker.label=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL org.dockerproject.label=\"foo\""
+    it "not ok with invalid character" $ do
+      ruleCatches "DL3048" "LABEL invalid$character=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL invalid$character=\"foo\""
+    it "not ok with invalid start and end characters" $ do
+      ruleCatches "DL3048" "LABEL .invalid =\"foo\""
+      ruleCatches "DL3048" "LABEL -invalid =\"foo\""
+      ruleCatches "DL3048" "LABEL 1invalid =\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL .invalid=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL -invalid=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL 1invalid=\"foo\""
+    it "not ok with consecutive dividers" $ do
+      ruleCatches "DL3048" "LABEL invalid..character=\"foo\""
+      ruleCatches "DL3048" "LABEL invalid--character=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL invalid..character=\"foo\""
+      onBuildRuleCatches "DL3048" "LABEL invalid--character=\"foo\""
+    it "ok with valid labels" $ do
+      ruleCatchesNot "DL3048" "LABEL org.valid-key.label3=\"foo\""
+      ruleCatchesNot "DL3048" "LABEL validlabel=\"foo\""
+      onBuildRuleCatchesNot "DL3048" "LABEL org.valid-key.label3=\"foo\""
+      onBuildRuleCatchesNot "DL3048" "LABEL validlabel=\"foo\""
diff --git a/test/Hadolint/Rule/DL3049Spec.hs b/test/Hadolint/Rule/DL3049Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3049Spec.hs
@@ -0,0 +1,82 @@
+module Hadolint.Rule.DL3049Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import Data.Text as Text
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("foo", Rule.RawText)] }
+
+  describe "DL3049 - Missing label rule spec" $ do
+  -- single stage spec
+    it "not ok: single stage, no label" $ do
+      ruleCatches "DL3049" "FROM baseimage"
+      onBuildRuleCatches "DL3049" "FROM baseimage"
+    it "not ok: single stage, wrong label" $ do
+      ruleCatches "DL3049" "FROM baseimage\nLABEL bar=\"baz\""
+      onBuildRuleCatches "DL3049" "FROM baseimage\nLABEL bar=\"baz\""
+    it "ok: single stage, label present" $ do
+      ruleCatchesNot "DL3049" "FROM baseimage\nLABEL foo=\"bar\""
+      onBuildRuleCatchesNot "DL3049" "FROM baseimage\nLABEL foo=\"bar\""
+  -- multi stage spec
+    it "warn twice: two stages, no labels" $
+      let dockerFile =
+            [ "FROM stage1",
+              "FROM stage2"
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 2 "DL3049")
+    it "warn twice: two stages, wrong labels only" $
+      let dockerFile =
+            [ "FROM stage1",
+              "LABEL bar=\"baz\"",
+              "FROM stage2",
+              "LABEL buzz=\"fuzz\""
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 2 "DL3049")
+    it "warn once: two stages, label present in second only" $
+      let dockerFile =
+            [ "FROM baseimage",
+              "FROM newimage",
+              "LABEL foo=\"bar\""
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 1 "DL3049")
+    it "warn once: two stages, no inheritance, wrong label in one" $
+      let dockerFile =
+            [ "FROM baseimage",
+              "LABEL baz=\"bar\"",
+              "FROM newimage",
+              "LABEL foo=\"bar\""
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 1 "DL3049")
+    it "warn once: two stages, inheritance, label only defined in second stage" $
+      let dockerFile =
+            [ "FROM baseimage as base",
+              "FROM base",
+              "LABEL foo=\"bar\""
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 1 "DL3049")
+    it "don't warn: two stages, inheritance" $
+      let dockerFile =
+            [ "FROM baseimage as base",
+              "LABEL foo=\"bar\"",
+              "FROM base"
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 0 "DL3049")
diff --git a/test/Hadolint/Rule/DL3050Spec.hs b/test/Hadolint/Rule/DL3050Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3050Spec.hs
@@ -0,0 +1,31 @@
+module Hadolint.Rule.DL3050Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config =
+        def
+          { labelSchema = Map.fromList [("required", Rule.RawText)],
+            strictLabels = True
+          }
+
+  describe "DL3050 - Superfluous label(s) present." $ do
+    it "ok with no label" $ do
+      ruleCatchesNot "DL3050" ""
+      onBuildRuleCatchesNot "DL3050" ""
+    it "ok with required label" $ do
+      ruleCatchesNot "DL3050" "LABEL required=\"foo\""
+      onBuildRuleCatchesNot "DL3050" "LABEL required=\"bar\""
+    it "not ok with just other label" $ do
+      ruleCatches "DL3050" "LABEL other=\"bar\""
+      onBuildRuleCatches "DL3050" "LABEL other=\"bar\""
+    it "not ok with other label and required label" $ do
+      ruleCatches "DL3050" "LABEL required=\"foo\" other=\"bar\""
+      onBuildRuleCatches "DL3050" "LABEL required=\"foo\" other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3051Spec.hs b/test/Hadolint/Rule/DL3051Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3051Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3051Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config =
+        def { labelSchema = Map.fromList [("emptylabel", Rule.RawText)] }
+  describe "DL3051 - Label `<label>` is empty." $ do
+    it "not ok with label empty" $ do
+      ruleCatches "DL3051" "LABEL emptylabel=\"\""
+      onBuildRuleCatches "DL3051" "LABEL emptylabel=\"\""
+    it "ok with label not empty" $ do
+      ruleCatchesNot "DL3051" "LABEL emptylabel=\"foo\""
+      onBuildRuleCatchesNot "DL3051" "LABEL emptylabel=\"bar\""
+    it "ok with other label empty" $ do
+      ruleCatchesNot "DL3051" "LABEL other=\"\""
+      onBuildRuleCatchesNot "DL3051" "LABEL other=\"\""
diff --git a/test/Hadolint/Rule/DL3052Spec.hs b/test/Hadolint/Rule/DL3052Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3052Spec.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3052Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..), LabelType (..))
+import qualified Data.Map as Map
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("urllabel", Url)] }
+
+  describe "DL3052 - Label `<label>` is not a valid URL." $ do
+    it "not ok with label not containing URL" $ do
+      ruleCatches "DL3052" "LABEL urllabel=\"not-url\""
+      onBuildRuleCatches "DL3052" "LABEL urllabel=\"not-url\""
+    it "ok with label containing URL" $ do
+      ruleCatchesNot "DL3052" "LABEL urllabel=\"http://example.com\""
+      onBuildRuleCatchesNot "DL3052" "LABEL urllabel=\"http://example.com\""
+    it "ok with other label not containing URL" $ do
+      ruleCatchesNot "DL3052" "LABEL other=\"foo\""
+      onBuildRuleCatchesNot "DL3052" "LABEL other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3053Spec.hs b/test/Hadolint/Rule/DL3053Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3053Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3053Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("datelabel", Rule.Rfc3339)] }
+
+  describe "DL3053 - Label `<label>` is not a valid time format - must be conform to RFC3339." $ do
+    it "not ok with label not containing RFC3339 date" $ do
+      ruleCatches "DL3053" "LABEL datelabel=\"not-date\""
+      onBuildRuleCatches "DL3053" "LABEL datelabel=\"not-date\""
+    it "ok with label containing RFC3339 date" $ do
+      ruleCatchesNot "DL3053" "LABEL datelabel=\"2021-03-10T10:26:33.564595127+01:00\""
+      onBuildRuleCatchesNot "DL3053" "LABEL datelabel=\"2021-03-10T10:26:33.564595127+01:00\""
+    it "ok with other label not containing RFC3339 date" $ do
+      ruleCatchesNot "DL3053" "LABEL other=\"doo\""
+      onBuildRuleCatchesNot "DL3053" "LABEL other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3054Spec.hs b/test/Hadolint/Rule/DL3054Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3054Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3054Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("spdxlabel", Rule.Spdx)] }
+
+  describe "DL3054 - Label `<label>` is not a valid SPDX license identifier." $ do
+    it "not ok with label not containing SPDX identifier" $ do
+      ruleCatches "DL3054" "LABEL spdxlabel=\"not-spdx\""
+      onBuildRuleCatches "DL3054" "LABEL spdxlabel=\"not-spdx\""
+    it "ok with label containing SPDX identifier" $ do
+      ruleCatchesNot "DL3054" "LABEL spdxlabel=\"BSD-3-Clause\""
+      onBuildRuleCatchesNot "DL3054" "LABEL spdxlabel=\"MIT\""
+    it "ok with other label not containing SPDX identifier" $ do
+      ruleCatchesNot "DL3054" "LABEL other=\"fooo\""
+      onBuildRuleCatchesNot "DL3054" "LABEL other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3055Spec.hs b/test/Hadolint/Rule/DL3055Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3055Spec.hs
@@ -0,0 +1,27 @@
+module Hadolint.Rule.DL3055Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("githash", Rule.GitHash)] }
+
+  describe "DL3055 - Label `<label>` is not a valid git hash." $ do
+    it "not ok with label not containing git hash" $ do
+      ruleCatches "DL3055" "LABEL githash=\"not-git-hash\""
+      onBuildRuleCatches "DL3055" "LABEL githash=\"not-git-hash\""
+    it "ok with label containing short git hash" $ do
+      ruleCatchesNot "DL3055" "LABEL githash=\"2dbfae9\""
+      onBuildRuleCatchesNot "DL3055" "LABEL githash=\"2dbfae9\""
+    it "ok with label containing long git hash" $ do
+      ruleCatchesNot "DL3055" "LABEL githash=\"43c572f1272b6b3171dd1db9e41b7027128ce080\""
+      onBuildRuleCatchesNot "DL3055" "LABEL githash=\"43c572f1272b6b3171dd1db9e41b7027128ce080\""
+    it "ok with other label not containing git hash" $ do
+      ruleCatchesNot "DL3055" "LABEL other=\"foo\""
+      onBuildRuleCatchesNot "DL3055" "LABEL other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3056Spec.hs b/test/Hadolint/Rule/DL3056Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3056Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3056Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("semver", Rule.SemVer)] }
+
+  describe "DL3056 - Label `<label>` does not conform to semantic versioning." $ do
+    it "not ok with label not containing semantic version" $ do
+      ruleCatches "DL3056" "LABEL semver=\"not-sem-ver\""
+      onBuildRuleCatches "DL3056" "LABEL semver=\"not-sem-ver\""
+    it "ok with label containing semantic version" $ do
+      ruleCatchesNot "DL3056" "LABEL semver=\"1.0.0\""
+      onBuildRuleCatchesNot "DL3056" "LABEL semver=\"2.0.1-rc1\""
+    it "ok with other label not containing semantic version" $ do
+      ruleCatchesNot "DL3056" "LABEL other=\"foo\""
+      onBuildRuleCatchesNot "DL3056" "LABEL other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3057Spec.hs b/test/Hadolint/Rule/DL3057Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3057Spec.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3057Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3057 - `HEALTHCHECK instruction missing" $ do
+    it "warn with no HEALTHCHECK instructions" $
+      ruleCatches "DL3057" "FROM scratch"
+    it "ok with one HEALTHCHECK instruction" $
+      ruleCatchesNot "DL3057" "FROM scratch\nHEALTHCHECK CMD /bin/bla"
+    it "ok with inheriting HEALTHCHECK instruction" $
+      ruleCatchesNot "DL3057" "FROM scratch AS base\nHEALTHCHECK CMD /bin/bla\nFROM base"
+    it "warn when not inheriting with no HEALTHCHECK instruction" $
+      ruleCatches "DL3057" "FROM scratch AS base\nHEALTHCHECK CMD /bin/bla\nFROM scratch"
diff --git a/test/Hadolint/Rule/DL3058Spec.hs b/test/Hadolint/Rule/DL3058Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3058Spec.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3058Spec (spec) where
+
+import Data.Default
+import Hadolint (Configuration (..))
+import qualified Data.Map as Map
+import qualified Hadolint.Rule as Rule
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def { labelSchema = Map.fromList [("maintainer", Rule.Email)] }
+
+  describe "DL3058 - Label `<label>` is not a valid email format - must be conform to RFC5322." $ do
+    it "not ok with label not containing valid email" $ do
+      ruleCatches "DL3058" "LABEL maintainer=\"not-email\""
+      onBuildRuleCatches "DL3058" "LABEL maintainer=\"not-email\""
+    it "ok with label containing valid email" $ do
+      ruleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""
+      onBuildRuleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""
+    it "ok with other label not containing valid email" $ do
+      ruleCatchesNot "DL3058" "LABEL other=\"doo\""
+      onBuildRuleCatchesNot "DL3058" "LABEL other=\"bar\""
diff --git a/test/Hadolint/Rule/DL3059Spec.hs b/test/Hadolint/Rule/DL3059Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3059Spec.hs
@@ -0,0 +1,73 @@
+module Hadolint.Rule.DL3059Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3059 - Multiple consecutive `RUN` instructions" $ do
+    it "ok with no `RUN` at all" $ do
+      ruleCatchesNot "DL3059" "FROM debian:10"
+    it "ok with one `RUN`" $ do
+      ruleCatchesNot "DL3059" "RUN /foo.sh"
+    it "ok with two not consecutive `RUN`" $ do
+      ruleCatchesNot "DL3059" "RUN /foo.sh\nWORKDIR /\nRUN /bar.sh"
+    it "not ok with two consecutive `RUN`s" $ do
+      ruleCatches "DL3059" "RUN /foo.sh\nRUN /bar.sh"
+    it "not ok with two `RUN`s separated by a comment" $ do
+      ruleCatches "DL3059" "RUN /foo.sh\n# a comment\nRUN /bar.sh"
+    it "not ok with two `RUN`s separated by two comment" $ do
+      ruleCatches "DL3059" "RUN /foo.sh\n# a comment\n# another comment\nRUN\
+                           \ /bar.sh"
+    it "ok with one `RUN` after a comment" $ do
+      ruleCatchesNot "DL3059" "# a comment\nRUN /foo.sh"
+    it "ok with two consecutive `RUN`s when flags are different 1" $ do
+      ruleCatchesNot "DL3059" "RUN --mount=type=secret,id=foo /foo.sh\nRUN\
+                              \ /bar.sh"
+    it "ok with two consecutive `RUN`s when flags are different 2" $ do
+      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",
+                    "RUN --mount=type=secret,id=bar /bar.sh"
+                  ]
+       in ruleCatchesNot "DL3059" $ Text.unlines dfile
+    it "not ok with two consecutive `RUN`s when flags are equal" $ do
+      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",
+                    "RUN --mount=type=secret,id=foo /bar.sh"
+                  ]
+       in ruleCatches "DL3059" $ Text.unlines dfile
+
+    it "ok with two consecutive `RUN`s when commands are chained 1" $ do
+      let dfile = [ "RUN foo && bar",
+                    "RUN foobar"
+                  ]
+       in ruleCatchesNot "DL3059" $ Text.unlines dfile
+    it "ok with two consecutive `RUN`s when commands are chained 2" $ do
+      let dfile = [ "RUN foo; bar",
+                    "RUN foobar"
+                  ]
+       in ruleCatchesNot "DL3059" $ Text.unlines dfile
+    it "ok with two consecutive `RUN`s when commands are chained 3" $ do
+      let dfile = [ "RUN foobar",
+                    "RUN foo && bar"
+                  ]
+       in ruleCatchesNot "DL3059" $ Text.unlines dfile
+    it "ok with two consecutive `RUN`s when commands are chained 4" $ do
+      let dfile = [ "RUN foobar",
+                    "RUN foo; bar"
+                  ]
+       in ruleCatchesNot "DL3059" $ Text.unlines dfile
+    it "ok with two consecutive `RUN`s when commands are chained 5" $ do
+      let dfile = [ "RUN foo && bar",
+                    "RUN foo; bar"
+                  ]
+       in ruleCatchesNot "DL3059" $ Text.unlines dfile
+    it "not ok when more than one `RUN` has just one command in a row" $ do
+      let dfile = [ "RUN foo && bar",
+                    "RUN foo",
+                    "RUN bar"
+                  ]
+       in ruleCatches "DL3059" $ Text.unlines dfile
diff --git a/test/Hadolint/Rule/DL3060Spec.hs b/test/Hadolint/Rule/DL3060Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3060Spec.hs
@@ -0,0 +1,70 @@
+module Hadolint.Rule.DL3060Spec (spec) where
+
+import Data.Default
+import qualified Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3060 - `yarn cache clean` missing after `yarn install`" $ do
+    it "ok with non-yarn commands" $ do
+      ruleCatchesNot "DL3060" "RUN foo"
+      onBuildRuleCatchesNot "DL3060" "RUN foo"
+    it "not ok with no cache clean" $ do
+      ruleCatches "DL3060" "RUN yarn install foo"
+      onBuildRuleCatches "DL3060" "RUN yarn install foo"
+    it "ok with cache clean" $ do
+      ruleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"
+      onBuildRuleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"
+
+    it "not ok when yarn install is in last stage w/o yarn clean" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM node:lts-alpine as foo",
+                "RUN hey!",
+                "FROM scratch",
+                "RUN yarn install"
+              ]
+       in do
+            ruleCatches "DL3060" dockerFile
+            onBuildRuleCatches "DL3060" dockerFile
+
+    it "not ok when inheriting from stage with yarn install w/o yarn clean" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM node:lts-alpine as foo",
+                "RUN yarn install",
+                "FROM foo",
+                "RUN hey!"
+              ]
+       in do
+            ruleCatches "DL3060" dockerFile
+            onBuildRuleCatches "DL3060" dockerFile
+
+    it "ok when inheriting from stage with yarn cache clear" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM node:lts-alpine as foo",
+                "RUN yarn install && yarn cache clean",
+                "FROM foo",
+                "RUN hey!"
+              ]
+       in do
+            ruleCatchesNot "DL3060" dockerFile
+            onBuildRuleCatchesNot "DL3060" dockerFile
+
+    it "ok when omitting yarn cache clean in stage that is not reused later" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM node:lts-alpine as foo",
+                "RUN yarn install foo",
+                "FROM scratch",
+                "RUN hey!"
+              ]
+       in do
+            ruleCatchesNot "DL3060" dockerFile
+            onBuildRuleCatches "DL3060" dockerFile
diff --git a/test/Hadolint/Rule/DL4000Spec.hs b/test/Hadolint/Rule/DL4000Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL4000Spec.hs
@@ -0,0 +1,16 @@
+module Hadolint.Rule.DL4000Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+  describe "DL4000 - MAINTAINER is deprecated." $ do
+    it "has deprecated maintainer" $
+      ruleCatches "DL4000" "FROM busybox\nMAINTAINER hudu@mail.com"
+    it "has maintainer" $ ruleCatches "DL4000" "FROM debian\nMAINTAINER Lukas"
+    it "has maintainer first" $ ruleCatches "DL4000" "MAINTAINER Lukas\nFROM DEBIAN"
+    it "has no maintainer" $ ruleCatchesNot "DL4000" "FROM debian"
diff --git a/test/Hadolint/Rule/DL4001Spec.hs b/test/Hadolint/Rule/DL4001Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL4001Spec.hs
@@ -0,0 +1,71 @@
+module Hadolint.Rule.DL4001Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL4001 - Either use Wget or Curl but not both." $ do
+    it "warns when using both wget and curl" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz",
+              "RUN curl localhost"
+            ]
+       in ruleCatches "DL4001" $ Text.unlines dockerFile
+    it "warns when using both wget and curl in same instruction" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz && curl localhost"
+            ]
+       in ruleCatches "DL4001" $ Text.unlines dockerFile
+    it "does not warn when using only wget" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz"
+            ]
+       in ruleCatchesNot "DL4001" $ Text.unlines dockerFile
+    it "does not warn when using both curl and wget in different stages" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz",
+              "FROM scratch",
+              "RUN curl localhost"
+            ]
+       in ruleCatchesNot "DL4001" $ Text.unlines dockerFile
+    it "does not warns when using both, on a single stage" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz",
+              "RUN curl localhost",
+              "FROM scratch",
+              "RUN curl localhost"
+            ]
+       in ruleCatches "DL4001" $ Text.unlines dockerFile
+    it "only warns on the relevant RUN instruction" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz",
+              "RUN curl my.xyz",
+              "RUN echo hello"
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 1 "DL4001")
+
+    it "only warns on many relevant RUN instructions" $
+      let dockerFile =
+            [ "FROM node as foo",
+              "RUN wget my.xyz",
+              "RUN curl my.xyz",
+              "RUN echo hello",
+              "RUN wget foo.com"
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (failsWith 2 "DL4001")
diff --git a/test/Hadolint/Rule/DL4003Spec.hs b/test/Hadolint/Rule/DL4003Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL4003Spec.hs
@@ -0,0 +1,44 @@
+module Hadolint.Rule.DL4003Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL4003" $ do
+    it "many cmds" $
+      let dockerFile =
+            [ "FROM debian",
+              "CMD bash",
+              "RUN foo",
+              "CMD another"
+            ]
+       in ruleCatches "DL4003" $ Text.unlines dockerFile
+
+    it "single cmds, different stages" $
+      let dockerFile =
+            [ "FROM debian as distro1",
+              "CMD bash",
+              "RUN foo",
+              "FROM debian as distro2",
+              "CMD another"
+            ]
+       in ruleCatchesNot "DL4003" $ Text.unlines dockerFile
+
+    it "many cmds, different stages" $
+      let dockerFile =
+            [ "FROM debian as distro1",
+              "CMD bash",
+              "RUN foo",
+              "CMD another",
+              "FROM debian as distro2",
+              "CMD another"
+            ]
+       in ruleCatches "DL4003" $ Text.unlines dockerFile
+
+    it "single cmd" $ ruleCatchesNot "DL4003" "CMD /bin/true"
diff --git a/test/Hadolint/Rule/DL4004Spec.hs b/test/Hadolint/Rule/DL4004Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL4004Spec.hs
@@ -0,0 +1,47 @@
+module Hadolint.Rule.DL4004Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL4004" $ do
+    it "no cmd" $ ruleCatchesNot "DL4004" "FROM busybox"
+
+    it "many entrypoints" $
+      let dockerFile =
+            [ "FROM debian",
+              "ENTRYPOINT bash",
+              "RUN foo",
+              "ENTRYPOINT another"
+            ]
+       in ruleCatches "DL4004" $ Text.unlines dockerFile
+
+    it "single entrypoint, different stages" $
+      let dockerFile =
+            [ "FROM debian as distro1",
+              "ENTRYPOINT bash",
+              "RUN foo",
+              "FROM debian as distro2",
+              "ENTRYPOINT another"
+            ]
+       in ruleCatchesNot "DL4004" $ Text.unlines dockerFile
+
+    it "many entrypoints, different stages" $
+      let dockerFile =
+            [ "FROM debian as distro1",
+              "ENTRYPOINT bash",
+              "RUN foo",
+              "ENTRYPOINT another",
+              "FROM debian as distro2",
+              "ENTRYPOINT another"
+            ]
+       in ruleCatches "DL4004" $ Text.unlines dockerFile
+
+    it "single entry" $ ruleCatchesNot "DL4004" "ENTRYPOINT /bin/true"
+    it "no entry" $ ruleCatchesNot "DL4004" "FROM busybox"
diff --git a/test/Hadolint/Rule/DL4005Spec.hs b/test/Hadolint/Rule/DL4005Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL4005Spec.hs
@@ -0,0 +1,21 @@
+module Hadolint.Rule.DL4005Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL4005 - Use `SHELL` to change the default shell." $ do
+    it "RUN ln" $ do
+      ruleCatches "DL4005" "RUN ln -sfv /bin/bash /bin/sh"
+      onBuildRuleCatches "DL4005" "RUN ln -sfv /bin/bash /bin/sh"
+    it "RUN ln with unrelated symlinks" $ do
+      ruleCatchesNot "DL4005" "RUN ln -sf /bin/true /sbin/initctl"
+      onBuildRuleCatchesNot "DL4005" "RUN ln -sf /bin/true /sbin/initctl"
+    it "RUN ln with multiple acceptable commands" $ do
+      ruleCatchesNot "DL4005" "RUN ln -s foo bar && unrelated && something_with /bin/sh"
+      onBuildRuleCatchesNot "DL4005" "RUN ln -s foo bar && unrelated && something_with /bin/sh"
diff --git a/test/Hadolint/Rule/DL4006Spec.hs b/test/Hadolint/Rule/DL4006Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL4006Spec.hs
@@ -0,0 +1,106 @@
+module Hadolint.Rule.DL4006Spec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL4006 - Set the `SHELL` option `-o pipefail` before RUN with a pipe in it" $ do
+    it "warn on missing pipefail" $
+      let dockerFile =
+            [ "FROM scratch",
+              "RUN wget -O - https://some.site | wc -l > /number"
+            ]
+       in ruleCatches "DL4006" $ Text.unlines dockerFile
+    it "don't warn on commands with no pipes" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "RUN wget -O - https://some.site && wc -l file > /number"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "don't warn on commands with pipes and the pipefail option" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "don't warn on commands with pipes and the pipefail option 2" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "don't warn on commands with pipes and the pipefail option 3" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/bash\", \"-o\", \"errexit\", \"-o\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "don't warn on commands with pipes and the pipefail zsh" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/zsh\", \"-o\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "don't warn on powershell" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"pwsh\", \"-c\"]",
+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "warns when using plain sh" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatches "DL4006" $ Text.unlines dockerFile
+    it "warn on missing pipefail in the next image" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number",
+              "FROM scratch as build2",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatches "DL4006" $ Text.unlines dockerFile
+    it "warn on missing pipefail if next SHELL is not using it" $
+      let dockerFile =
+            [ "FROM scratch as build",
+              "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number",
+              "SHELL [\"/bin/sh\", \"-c\"]",
+              "RUN wget -O - https://some.site | wc -l file > /number"
+            ]
+       in ruleCatches "DL4006" $ Text.unlines dockerFile
+    it "ignore non posix shells: pwsh" $
+      let dockerFile =
+            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",
+              "SHELL [ \"pwsh\", \"-c\" ]",
+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "ignore non posix shells: powershell" $
+      let dockerFile =
+            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",
+              "SHELL [ \"powershell.exe\" ]",
+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+    it "ignore non posix shells: cmd.exe" $
+      let dockerFile =
+            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",
+              "SHELL [ \"cmd.exe\", \"/c\" ]",
+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+            ]
+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
diff --git a/test/Hadolint/Rule/ShellcheckSpec.hs b/test/Hadolint/Rule/ShellcheckSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/ShellcheckSpec.hs
@@ -0,0 +1,171 @@
+module Hadolint.Rule.ShellcheckSpec (spec) where
+
+import Data.Default
+import Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "Shellcheck" $ do
+    it "runs shellchek on RUN instructions" $ do
+      assertChecks "RUN echo $MISSING_QUOTES" failsShellcheck
+      assertOnBuildChecks "RUN echo $MISSING_QUOTES" failsShellcheck
+    it "not warns on valid scripts" $ do
+      assertChecks "RUN echo foo" passesShellcheck
+      assertOnBuildChecks "RUN echo foo" passesShellcheck
+
+    it "Does not complain on default env vars" $
+      let dockerFile =
+            Text.unlines
+              [ "RUN echo \"$HTTP_PROXY\"",
+                "RUN echo \"$http_proxy\"",
+                "RUN echo \"$HTTPS_PROXY\"",
+                "RUN echo \"$https_proxy\"",
+                "RUN echo \"$FTP_PROXY\"",
+                "RUN echo \"$ftp_proxy\"",
+                "RUN echo \"$NO_PROXY\"",
+                "RUN echo \"$no_proxy\""
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
+
+    it "Complain on missing env vars" $
+      let dockerFile =
+            Text.unlines
+              [ "RUN echo \"$RTTP_PROXY\""
+              ]
+       in do
+            assertChecks dockerFile failsShellcheck
+            assertOnBuildChecks dockerFile failsShellcheck
+
+    it "Is aware of ARGS and ENV" $
+      let dockerFile =
+            Text.unlines
+              [ "ARG foo=bar",
+                "ARG another_foo",
+                "ENV bar=10 baz=20",
+                "RUN echo \"$foo\"",
+                "RUN echo \"$another_foo\"",
+                "RUN echo \"$bar\"",
+                "RUN echo \"$baz\""
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile failsShellcheck
+
+    it "Resets env vars after a FROM" $
+      let dockerFile =
+            Text.unlines
+              [ "ARG foo=bar",
+                "ARG another_foo",
+                "ENV bar=10 baz=20",
+                "FROM debian",
+                "RUN echo \"$foo\""
+              ]
+       in do
+            assertChecks dockerFile failsShellcheck
+            assertOnBuildChecks dockerFile failsShellcheck
+
+    it "Defaults the shell to sh" $
+      let dockerFile =
+            Text.unlines
+              [ "RUN echo $RANDOM"
+              ]
+       in do
+            assertChecks dockerFile failsShellcheck
+            assertOnBuildChecks dockerFile failsShellcheck
+
+    it "Can change the shell check to bash" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
+                "RUN echo $RANDOM"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            -- This is debatable, as it should actaully pass, but detecting it correctly
+            -- is quite difficult
+            assertOnBuildChecks dockerFile failsShellcheck
+    it "Resets the SHELL to sh after a FROM" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
+                "FROM debian",
+                "RUN echo $RANDOM"
+              ]
+       in do
+            assertChecks dockerFile failsShellcheck
+            assertOnBuildChecks dockerFile failsShellcheck
+
+    it "Does not complain on ash shell" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]",
+                "RUN echo hello"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
+
+    it "Does not complain on non-posix shells: pwsh" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"pwsh\", \"-c\"]",
+                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
+    it "Does not complain on non-posix shells: cmd.exe" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"cmd.exe\", \"/c\"]",
+                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
+
+    it "Does not complain on non-posix shells, powershell - absolute path" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\\
+                \v1.0\\\\powershell.exe\", \"-noprofile\", \"-noninteractive\"\
+                \, \"-command\"]",
+                "RUN Get-Variable PSVersionTable | Select-Object \
+                \-ExpandProperty Value"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
+
+    it "Respects shell pragma" $
+      let dockerFile =
+            Text.unlines
+              [ "FROM mcr.microsoft.com/foo/bar/windows:10",
+                "# hadolint shell = powershell.exe",
+                "RUN Get-Variable PSVersionTable | Select-Object \
+                \-ExpandProperty Value"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
+    it "Respects global shell pragma" $
+      let dockerFile =
+            Text.unlines
+              [ "# hadolint shell=powershell.exe",
+                "FROM mcr.microsoft.com/foo/bar/windows:10",
+                "RUN Get-Variable PSVersionTable | Select-Object \
+                \-ExpandProperty Value",
+                "FROM mcr.microsoft.com/foo/bar/windows:10",
+                "RUN Get-Variable PSVersionTable | Select-Object \
+                \-ExpandProperty Value"
+              ]
+       in do
+            assertChecks dockerFile passesShellcheck
+            assertOnBuildChecks dockerFile passesShellcheck
diff --git a/test/Hadolint/ShellSpec.hs b/test/Hadolint/ShellSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/ShellSpec.hs
@@ -0,0 +1,53 @@
+module Hadolint.ShellSpec (spec) where
+
+import Hadolint.Shell
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec =
+  describe "Shell unit tests" $ do
+  --
+    it "getFlagArgs" $ do
+      let cmd = Command "useradd"
+                        [ CmdPart "12345" 2,
+                          CmdPart "67890" 4,
+                          CmdPart "luser" 5 ]
+                        [ CmdPart "u" 1,
+                          CmdPart "u" 3 ]
+       in getFlagArg "u" cmd `shouldBe` [ "12345",
+                                          "67890" ]
+      let cmd = Command "useradd"
+                        [ CmdPart "12345" 2,
+                          CmdPart "luser" 3 ]
+                        [ CmdPart "u" 1 ]
+       in getFlagArg "u" cmd `shouldBe` [ "12345" ]
+
+      let cmd = Command "useradd"
+                        [ CmdPart "12345" 2,
+                          CmdPart "luser" 3 ]
+                        [ CmdPart "u" 1 ]
+       in getFlagArg "f" cmd `shouldBe` []
+  --
+    it "hasFlag" $ do
+      let cmd = Command "useradd"
+                        [ CmdPart "luser" 1 ]
+                        [ CmdPart "l" 0 ]
+       in hasFlag "l" cmd `shouldBe` True
+      let cmd = Command "useradd"
+                        [ CmdPart "luser" 1 ]
+                        [ CmdPart "l" 0 ]
+       in hasFlag "f" cmd `shouldBe` False
+  --
+    it "dropFlagArg" $ do
+      let cmd = Command "npm"
+                        [ CmdPart "verbose" 2,
+                          CmdPart "install" 3,
+                          CmdPart "bla@1.0.0" 4]
+                        [ CmdPart "loglevel" 1]
+      let res = Command "npm"
+                        [ CmdPart "install" 3,
+                          CmdPart "bla@1.0.0" 4]
+                        [ CmdPart "loglevel" 1]
+       in do
+        dropFlagArg ["loglevel"] cmd `shouldBe` res
diff --git a/test/Helpers.hs b/test/Helpers.hs
--- a/test/Helpers.hs
+++ b/test/Helpers.hs
@@ -1,30 +1,34 @@
 module Helpers where
 
-import qualified Control.Foldl as Foldl
 import Control.Monad (unless, when)
-import qualified Data.Sequence as Seq
-import qualified Data.Text as Text
+import Hadolint (Configuration (..), OutputFormat (..), printResults)
+import Hadolint.Formatter.Format (Result (..))
 import Hadolint.Formatter.TTY (formatCheck)
-import qualified Hadolint.Process
 import Hadolint.Rule (CheckFailure (..), Failures, RuleCode (..))
 import Language.Docker.Parser
 import Language.Docker.Syntax
+import System.IO.Silently
 import Test.HUnit hiding (Label)
 import Test.Hspec
+import qualified Control.Foldl as Foldl
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
+import qualified Hadolint.Process
 
 
 assertChecks ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  (HasCallStack, ?config :: Configuration) =>
   Text.Text ->
   (Failures -> IO a) ->
   IO a
 assertChecks dockerfile makeAssertions =
   case parseText (dockerfile <> "\n") of
     Left err -> assertFailure $ show err
-    Right dockerFile -> makeAssertions $ Hadolint.Process.run ?rulesConfig dockerFile
+    Right dockerFile -> makeAssertions $ Hadolint.Process.run ?config dockerFile
 
 assertOnBuildChecks ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  (HasCallStack, ?config :: Configuration) =>
   Text.Text ->
   (Failures -> IO a) ->
   IO a
@@ -33,7 +37,7 @@
     Left err -> assertFailure $ show err
     Right dockerFile -> checkOnBuild dockerFile
   where
-    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?rulesConfig (fmap wrapInOnBuild dockerFile)
+    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?config (fmap wrapInOnBuild dockerFile)
     wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li
     wrapInOnBuild i = i
 
@@ -44,7 +48,7 @@
 
 -- Assert a failed check exists for rule
 ruleCatches ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  (HasCallStack, ?config :: Configuration) =>
   RuleCode ->
   Text.Text ->
   Assertion
@@ -55,7 +59,7 @@
       assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks
 
 onBuildRuleCatches ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  (HasCallStack, ?config :: Configuration) =>
   RuleCode ->
   Text.Text ->
   Assertion
@@ -66,7 +70,7 @@
       assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks
 
 ruleCatchesNot ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  (HasCallStack, ?config :: Configuration) =>
   RuleCode ->
   Text.Text ->
   Assertion
@@ -75,7 +79,7 @@
     f = failsWith 0 ruleCode
 
 onBuildRuleCatchesNot ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  (HasCallStack, ?config :: Configuration) =>
   RuleCode ->
   Text.Text ->
   Assertion
@@ -118,3 +122,17 @@
         <> (Text.unpack . formatChecksNoColor $ matched)
   where
     matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks
+
+
+assertFormatter ::
+  (HasCallStack, ?noColor :: Bool) =>
+  OutputFormat ->
+  [CheckFailure] ->
+  String ->
+  Assertion
+assertFormatter formatter failures expectation = do
+  let results =
+        NonEmpty.fromList [Result "<string>" mempty (Seq.fromList failures)]
+  (cap, _) <- capture
+                (printResults formatter ?noColor (Just "<string>") results)
+  cap `shouldBe` expectation
diff --git a/test/RegressionSpec.hs b/test/RegressionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/RegressionSpec.hs
@@ -0,0 +1,32 @@
+module RegressionSpec (spec) where
+
+import Data.Default
+import qualified Data.Text as Text
+import Helpers
+import Test.HUnit hiding (Label)
+import Test.Hspec
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def  -- default implicit parameter running the checkers
+  describe "Regression Tests" $ do
+    it "Comments with backslashes at the end are just comments" $
+      let dockerFile =
+            [ "FROM alpine:3.6",
+              "# The following comment makes hadolint still complain about DL4006",
+              "# \\",
+              "# should solve DL4006",
+              "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",
+              "# RUN with pipe. causes DL4006, but should be fixed by above SHELL",
+              "RUN echo \"kaka\" | sed 's/a/o/g' >> /root/afile"
+            ]
+       in ruleCatches "DL4006" $ Text.unlines dockerFile
+    it "`ARG` can correctly unset variables" $
+      let dockerFile =
+            [ "ARG A_WITHOUT_EQ",
+              "ARG A_WITH_EQ=",
+              "RUN echo bla"
+            ]
+       in assertChecks
+            (Text.unlines dockerFile)
+            (assertBool "No Warnings or Errors should be triggered" . null)
diff --git a/test/ShellSpec.hs b/test/ShellSpec.hs
deleted file mode 100644
--- a/test/ShellSpec.hs
+++ /dev/null
@@ -1,52 +0,0 @@
-module ShellSpec where
-
-import Hadolint.Shell
-import Test.Hspec
-
-tests :: SpecWith ()
-tests =
-  describe "Shell unit tests" $ do
-  --
-    it "getFlagArgs" $ do
-      let cmd = Command "useradd"
-                        [ CmdPart "12345" 2,
-                          CmdPart "67890" 4,
-                          CmdPart "luser" 5 ]
-                        [ CmdPart "u" 1,
-                          CmdPart "u" 3 ]
-       in getFlagArg "u" cmd `shouldBe` [ "12345",
-                                          "67890" ]
-      let cmd = Command "useradd"
-                        [ CmdPart "12345" 2,
-                          CmdPart "luser" 3 ]
-                        [ CmdPart "u" 1 ]
-       in getFlagArg "u" cmd `shouldBe` [ "12345" ]
-
-      let cmd = Command "useradd"
-                        [ CmdPart "12345" 2,
-                          CmdPart "luser" 3 ]
-                        [ CmdPart "u" 1 ]
-       in getFlagArg "f" cmd `shouldBe` []
-  --
-    it "hasFlag" $ do
-      let cmd = Command "useradd"
-                        [ CmdPart "luser" 1 ]
-                        [ CmdPart "l" 0 ]
-       in hasFlag "l" cmd `shouldBe` True
-      let cmd = Command "useradd"
-                        [ CmdPart "luser" 1 ]
-                        [ CmdPart "l" 0 ]
-       in hasFlag "f" cmd `shouldBe` False
-  --
-    it "dropFlagArg" $ do
-      let cmd = Command "npm"
-                        [ CmdPart "verbose" 2,
-                          CmdPart "install" 3,
-                          CmdPart "bla@1.0.0" 4]
-                        [ CmdPart "loglevel" 1]
-      let res = Command "npm"
-                        [ CmdPart "install" 3,
-                          CmdPart "bla@1.0.0" 4]
-                        [ CmdPart "loglevel" 1]
-       in do
-        dropFlagArg ["loglevel"] cmd `shouldBe` res
diff --git a/test/Shellcheck.hs b/test/Shellcheck.hs
deleted file mode 100644
--- a/test/Shellcheck.hs
+++ /dev/null
@@ -1,130 +0,0 @@
-module Shellcheck (tests) where
-
-import Data.Text as Text
-import Helpers
-import Test.Hspec
-
-
-tests :: SpecWith ()
-tests = do
-  let ?rulesConfig = mempty
-  describe "Shellcheck" $ do
-    it "runs shellchek on RUN instructions" $ do
-      assertChecks "RUN echo $MISSING_QUOTES" failsShellcheck
-      assertOnBuildChecks "RUN echo $MISSING_QUOTES" failsShellcheck
-    it "not warns on valid scripts" $ do
-      assertChecks "RUN echo foo" passesShellcheck
-      assertOnBuildChecks "RUN echo foo" passesShellcheck
-
-    it "Does not complain on default env vars" $
-      let dockerFile =
-            Text.unlines
-              [ "RUN echo \"$HTTP_PROXY\"",
-                "RUN echo \"$http_proxy\"",
-                "RUN echo \"$HTTPS_PROXY\"",
-                "RUN echo \"$https_proxy\"",
-                "RUN echo \"$FTP_PROXY\"",
-                "RUN echo \"$ftp_proxy\"",
-                "RUN echo \"$NO_PROXY\"",
-                "RUN echo \"$no_proxy\""
-              ]
-       in do
-            assertChecks dockerFile passesShellcheck
-            assertOnBuildChecks dockerFile passesShellcheck
-
-    it "Complain on missing env vars" $
-      let dockerFile =
-            Text.unlines
-              [ "RUN echo \"$RTTP_PROXY\""
-              ]
-       in do
-            assertChecks dockerFile failsShellcheck
-            assertOnBuildChecks dockerFile failsShellcheck
-
-    it "Is aware of ARGS and ENV" $
-      let dockerFile =
-            Text.unlines
-              [ "ARG foo=bar",
-                "ARG another_foo",
-                "ENV bar=10 baz=20",
-                "RUN echo \"$foo\"",
-                "RUN echo \"$another_foo\"",
-                "RUN echo \"$bar\"",
-                "RUN echo \"$baz\""
-              ]
-       in do
-            assertChecks dockerFile passesShellcheck
-            assertOnBuildChecks dockerFile failsShellcheck
-
-    it "Resets env vars after a FROM" $
-      let dockerFile =
-            Text.unlines
-              [ "ARG foo=bar",
-                "ARG another_foo",
-                "ENV bar=10 baz=20",
-                "FROM debian",
-                "RUN echo \"$foo\""
-              ]
-       in do
-            assertChecks dockerFile failsShellcheck
-            assertOnBuildChecks dockerFile failsShellcheck
-
-    it "Defaults the shell to sh" $
-      let dockerFile =
-            Text.unlines
-              [ "RUN echo $RANDOM"
-              ]
-       in do
-            assertChecks dockerFile failsShellcheck
-            assertOnBuildChecks dockerFile failsShellcheck
-
-    it "Can change the shell check to bash" $
-      let dockerFile =
-            Text.unlines
-              [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
-                "RUN echo $RANDOM"
-              ]
-       in do
-            assertChecks dockerFile passesShellcheck
-            -- This is debatable, as it should actaully pass, but detecting it correctly
-            -- is quite difficult
-            assertOnBuildChecks dockerFile failsShellcheck
-    it "Resets the SHELL to sh after a FROM" $
-      let dockerFile =
-            Text.unlines
-              [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
-                "FROM debian",
-                "RUN echo $RANDOM"
-              ]
-       in do
-            assertChecks dockerFile failsShellcheck
-            assertOnBuildChecks dockerFile failsShellcheck
-
-    it "Does not complain on ash shell" $
-      let dockerFile =
-            Text.unlines
-              [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]",
-                "RUN echo hello"
-              ]
-       in do
-            assertChecks dockerFile passesShellcheck
-            assertOnBuildChecks dockerFile passesShellcheck
-
-    it "Does not complain on non-posix shells: pwsh" $
-      let dockerFile =
-            Text.unlines
-              [ "SHELL [\"pwsh\", \"-c\"]",
-                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-              ]
-       in do
-            assertChecks dockerFile passesShellcheck
-            assertOnBuildChecks dockerFile passesShellcheck
-    it "Does not complain on non-posix shells: cmd.exe" $
-      let dockerFile =
-            Text.unlines
-              [ "SHELL [\"cmd.exe\", \"/c\"]",
-                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-              ]
-       in do
-            assertChecks dockerFile passesShellcheck
-            assertOnBuildChecks dockerFile passesShellcheck
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,233 +1,1 @@
-import qualified ConfigSpec
-import qualified Data.Text as Text
-import qualified DL3000
-import qualified DL3001
-import qualified DL3002
-import qualified DL3003
-import qualified DL3004
-import qualified DL3005
-import qualified DL3006
-import qualified DL3007
-import qualified DL3008
-import qualified DL3009
-import qualified DL3010
-import qualified DL3011
-import qualified DL3012
-import qualified DL3013
-import qualified DL3014
-import qualified DL3015
-import qualified DL3016
-import qualified DL3018
-import qualified DL3019
-import qualified DL3020
-import qualified DL3021
-import qualified DL3022
-import qualified DL3023
-import qualified DL3024
-import qualified DL3025
-import qualified DL3026
-import qualified DL3027
-import qualified DL3028
-import qualified DL3029
-import qualified DL3030
-import qualified DL3032
-import qualified DL3033
-import qualified DL3034
-import qualified DL3035
-import qualified DL3036
-import qualified DL3037
-import qualified DL3038
-import qualified DL3040
-import qualified DL3041
-import qualified DL3042
-import qualified DL3043
-import qualified DL3044
-import qualified DL3045
-import qualified DL3046
-import qualified DL3047
-import qualified DL3048
-import qualified DL3049
-import qualified DL3050
-import qualified DL3051
-import qualified DL3052
-import qualified DL3053
-import qualified DL3054
-import qualified DL3055
-import qualified DL3056
-import qualified DL3057
-import qualified DL3058
-import qualified DL3059
-import qualified DL3060
-import qualified DL4001
-import qualified DL4003
-import qualified DL4004
-import qualified DL4005
-import qualified DL4006
-import Hadolint.Formatter.Format (errorMessage)
-import Hadolint.Formatter.TTY (formatError)
-import Helpers
-import Language.Docker.Parser
-import qualified Shellcheck
-import qualified ShellSpec
-import Test.HUnit hiding (Label)
-import Test.Hspec
-
-main :: IO ()
-main =
-  hspec $ do
-    let ?rulesConfig = mempty -- default implicit parameter running the checkers
-    describe "format error" $
-      it "display error after line pos" $ do
-        let ast = parseText "FOM debian:jessie"
-            expectedMsg =
-              "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY, ENTRYPOINT, "
-                <> "ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, "
-                <> "USER, VOLUME, WORKDIR, a pragma, end of input, or whitespaces "
-        case ast of
-          Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)
-          Right _ -> assertFailure "AST should fail parsing"
-    describe "errorMessage" $
-      it "display just the error message" $ do
-        let ast = parseText "RUNNN"
-            expectedMsg = "missing whitespace"
-        case ast of
-          Left err -> assertEqual "Unexpected error msg" expectedMsg (errorMessage err)
-          Right _ -> assertFailure "AST should fail parsing"
-    --
-    describe "Rules can be ignored with inline comments" $ do
-      it "ignores single rule" $
-        let dockerFile =
-              [ "FROM ubuntu",
-                "# hadolint ignore=DL3002",
-                "USER root"
-              ]
-         in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
-      it "ignores only the given rule" $
-        let dockerFile =
-              [ "FROM scratch",
-                "# hadolint ignore=DL3001",
-                "USER root"
-              ]
-         in ruleCatches "DL3002" $ Text.unlines dockerFile
-      it "ignores only the given rule, when multiple passed" $
-        let dockerFile =
-              [ "FROM scratch",
-                "# hadolint ignore=DL3001,DL3002",
-                "USER root"
-              ]
-         in ruleCatchesNot "DL3002" $ Text.unlines dockerFile
-      it "ignores the rule only if directly above the instruction" $
-        let dockerFile =
-              [ "# hadolint ignore=DL3001,DL3002",
-                "FROM ubuntu",
-                "USER root"
-              ]
-         in ruleCatches "DL3002" $ Text.unlines dockerFile
-      it "won't ignore the rule if passed invalid rule names" $
-        let dockerFile =
-              [ "FROM scratch",
-                "# hadolint ignore=crazy,DL3002",
-                "USER root"
-              ]
-         in ruleCatches "DL3002" $ Text.unlines dockerFile
-      it "ignores multiple rules correctly, even with some extra whitespace" $
-        let dockerFile =
-              [ "FROM node as foo",
-                "# hadolint ignore=DL3023, DL3021",
-                "COPY --from=foo bar baz ."
-              ]
-         in do
-              ruleCatchesNot "DL3023" $ Text.unlines dockerFile
-              ruleCatchesNot "DL3021" $ Text.unlines dockerFile
-    --
-    describe "Regression Tests" $ do
-      it "Comments with backslashes at the end are just comments" $
-        let dockerFile =
-              [ "FROM alpine:3.6",
-                "# The following comment makes hadolint still complain about DL4006",
-                "# \\",
-                "# should solve DL4006",
-                "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",
-                "# RUN with pipe. causes DL4006, but should be fixed by above SHELL",
-                "RUN echo \"kaka\" | sed 's/a/o/g' >> /root/afile"
-              ]
-         in ruleCatches "DL4006" $ Text.unlines dockerFile
-      it "`ARG` can correctly unset variables" $
-        let dockerFile =
-              [ "ARG A_WITHOUT_EQ",
-                "ARG A_WITH_EQ=",
-                "RUN echo bla"
-              ]
-         in assertChecks
-              (Text.unlines dockerFile)
-              (assertBool "No Warnings or Errors should be triggered" . null)
-
-    -- Run tests for the Config module
-    ConfigSpec.tests
-    -- Run tests for the Shell module
-    ShellSpec.tests
-    -- Run rule tests
-    Shellcheck.tests
-    DL3000.tests
-    DL3001.tests
-    DL3002.tests
-    DL3003.tests
-    DL3004.tests
-    DL3005.tests
-    DL3006.tests
-    DL3007.tests
-    DL3008.tests
-    DL3009.tests
-    DL3010.tests
-    DL3011.tests
-    DL3012.tests
-    DL3013.tests
-    DL3014.tests
-    DL3015.tests
-    DL3016.tests
-    DL3018.tests
-    DL3019.tests
-    DL3020.tests
-    DL3021.tests
-    DL3022.tests
-    DL3023.tests
-    DL3024.tests
-    DL3025.tests
-    DL3026.tests
-    DL3027.tests
-    DL3028.tests
-    DL3029.tests
-    DL3030.tests
-    DL3032.tests
-    DL3033.tests
-    DL3034.tests
-    DL3035.tests
-    DL3036.tests
-    DL3037.tests
-    DL3038.tests
-    DL3040.tests
-    DL3041.tests
-    DL3042.tests
-    DL3043.tests
-    DL3044.tests
-    DL3045.tests
-    DL3046.tests
-    DL3047.tests
-    DL3048.tests
-    DL3049.tests
-    DL3050.tests
-    DL3051.tests
-    DL3052.tests
-    DL3053.tests
-    DL3054.tests
-    DL3055.tests
-    DL3056.tests
-    DL3057.tests
-    DL3058.tests
-    DL3059.tests
-    DL3060.tests
-    DL4001.tests
-    DL4003.tests
-    DL4004.tests
-    DL4005.tests
-    DL4006.tests
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
