diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -292,6 +292,7 @@
 | [DL3055](https://github.com/hadolint/hadolint/wiki/DL3055)   | Label `<label>` is not a valid git hash.                                                                                                            |
 | [DL3056](https://github.com/hadolint/hadolint/wiki/DL3056)   | Label `<label>` does not conform to semantic versioning.                                                                                            |
 | [DL3057](https://github.com/hadolint/hadolint/wiki/DL3057)   | `HEALTHCHECK` instruction missing.                                                                                                                  |
+| [DL3058](https://github.com/hadolint/hadolint/wiki/DL3058)   | Label `<label>` is not a valid email format - must be conform to RFC5322.                                                                           |
 | [DL4000](https://github.com/hadolint/hadolint/wiki/DL4000)   | MAINTAINER is deprecated.                                                                                                                           |
 | [DL4001](https://github.com/hadolint/hadolint/wiki/DL4001)   | Either use Wget or Curl but not both.                                                                                                               |
 | [DL4003](https://github.com/hadolint/hadolint/wiki/DL4003)   | Multiple `CMD` instructions found.                                                                                                                  |
diff --git a/hadolint.cabal b/hadolint.cabal
--- a/hadolint.cabal
+++ b/hadolint.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 6dc5574db9b4edf093ae954fa31aa254d6a3b1a37aa6dc0bf793182d24daf375
+-- hash: 3a6af1aecfc32d960fd1df8de865bb60b8682f8877a9b3bb848a3f59c50c063d
 
 name:           hadolint
-version:        2.0.0
+version:        2.1.0
 synopsis:       Dockerfile Linter JavaScript API
 description:    A smarter Dockerfile linter that helps you build best practice Docker images.
 category:       Development
@@ -102,6 +102,7 @@
       Hadolint.Rule.DL3055
       Hadolint.Rule.DL3056
       Hadolint.Rule.DL3057
+      Hadolint.Rule.DL3058
       Hadolint.Rule.DL4000
       Hadolint.Rule.DL4001
       Hadolint.Rule.DL4003
@@ -130,6 +131,7 @@
     , cryptonite
     , deepseq ==1.4.4.*
     , directory >=1.3.0
+    , email-validate
     , filepath
     , foldl
     , ilist
@@ -174,11 +176,78 @@
   main-is: Spec.hs
   other-modules:
       ConfigSpec
+      DL3000
+      DL3001
+      DL3002
+      DL3003
+      DL3004
+      DL3005
+      DL3006
+      DL3007
+      DL3008
+      DL3009
+      DL3010
+      DL3011
+      DL3012
+      DL3013
+      DL3014
+      DL3015
+      DL3016
+      DL3017
+      DL3018
+      DL3019
+      DL3020
+      DL3021
+      DL3022
+      DL3023
+      DL3024
+      DL3025
+      DL3026
+      DL3027
+      DL3028
+      DL3029
+      DL3030
+      DL3031
+      DL3032
+      DL3033
+      DL3034
+      DL3035
+      DL3036
+      DL3037
+      DL3038
+      DL3039
+      DL3040
+      DL3041
+      DL3042
+      DL3043
+      DL3044
+      DL3045
+      DL3046
+      DL3047
+      DL3048
+      DL3049
+      DL3050
+      DL3051
+      DL3052
+      DL3053
+      DL3054
+      DL3055
+      DL3056
+      DL3057
+      DL3058
+      DL4000
+      DL4001
+      DL4003
+      DL4004
+      DL4005
+      DL4006
+      Helpers
+      Shellcheck
       ShellSpec
       Paths_hadolint
   hs-source-dirs:
       test
-  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms
+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms ImplicitParams OverloadedLists
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal
   build-depends:
       HUnit >=1.2
diff --git a/src/Hadolint/Formatter/Json.hs b/src/Hadolint/Formatter/Json.hs
--- a/src/Hadolint/Formatter/Json.hs
+++ b/src/Hadolint/Formatter/Json.hs
@@ -4,8 +4,10 @@
   )
 where
 
+import qualified Control.Foldl as Foldl
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy.Char8 as B
+import Data.Sequence (Seq)
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format (Result (..), errorPosition, severityText)
 import Hadolint.Rule (CheckFailure (..), DLSeverity (..), unRuleCode)
@@ -44,17 +46,13 @@
   (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>
   f (Result s e) ->
   IO ()
-printResults = mapM_ printResult
+printResults results = B.putStr . encode $ flattened
+  where
+    flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results
 
-formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Value
-formatResult (Result fileName errors checks) = toJSON allMessages
+formatResult :: Result s e -> Seq (JsonFormat s e)
+formatResult (Result fileName errors checks) = allMessages
   where
     allMessages = errorMessages <> checkMessages
     errorMessages = fmap JsonParseError errors
     checkMessages = fmap (JsonCheck fileName) checks
-
-printResult ::
-  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
-  Result s e ->
-  IO ()
-printResult result = B.putStrLn (encode (formatResult result))
diff --git a/src/Hadolint/Process.hs b/src/Hadolint/Process.hs
--- a/src/Hadolint/Process.hs
+++ b/src/Hadolint/Process.hs
@@ -66,6 +66,7 @@
 import qualified Hadolint.Rule.DL3055
 import qualified Hadolint.Rule.DL3056
 import qualified Hadolint.Rule.DL3057
+import qualified Hadolint.Rule.DL3058
 import qualified Hadolint.Rule.DL4000
 import qualified Hadolint.Rule.DL4001
 import qualified Hadolint.Rule.DL4003
@@ -193,6 +194,7 @@
     <> Hadolint.Rule.DL3054.rule labelSchema
     <> Hadolint.Rule.DL3055.rule labelSchema
     <> Hadolint.Rule.DL3056.rule labelSchema
+    <> Hadolint.Rule.DL3058.rule labelSchema
     <> Hadolint.Rule.DL3057.rule
     <> Hadolint.Rule.DL4000.rule
     <> Hadolint.Rule.DL4001.rule
diff --git a/src/Hadolint/Rule.hs b/src/Hadolint/Rule.hs
--- a/src/Hadolint/Rule.hs
+++ b/src/Hadolint/Rule.hs
@@ -57,6 +57,7 @@
   | GitHash
   | Rfc3339
   | SemVer
+  | Email
   deriving (Eq, Read, Show)
 
 read :: Text.Text -> Either Text.Text LabelType
@@ -65,6 +66,7 @@
 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)
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
@@ -1,5 +1,6 @@
 module Hadolint.Rule.DL3000 (rule) where
 
+import qualified Data.Char as Char
 import qualified Data.Text as Text
 import Hadolint.Rule
 import Language.Docker.Syntax (Instruction (..))
@@ -13,10 +14,18 @@
     check (Workdir loc)
       | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
       | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
+      | isWindowsAbsolute (Text.dropAround dropQuotes loc) = True
       | otherwise = False
     check _ = True
-    dropQuotes chr
-      | chr == '\"' = True
-      | chr == '\'' = True
-      | otherwise = False
 {-# INLINEABLE rule #-}
+
+dropQuotes :: Char -> Bool
+dropQuotes chr
+  | chr == '\"' = True
+  | chr == '\'' = True
+  | otherwise = False
+
+isWindowsAbsolute :: Text.Text -> Bool
+isWindowsAbsolute path
+  | Char.isLetter (Text.index path 0) && (':' == Text.index path 1) = True
+  | otherwise = False
diff --git a/src/Hadolint/Rule/DL3016.hs b/src/Hadolint/Rule/DL3016.hs
--- a/src/Hadolint/Rule/DL3016.hs
+++ b/src/Hadolint/Rule/DL3016.hs
@@ -26,36 +26,63 @@
 
     check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args
     check _ = True
+{-# INLINEABLE rule #-}
 
-    forgotToPinVersion cmd =
-      isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))
 
-    isNpmInstall = Shell.cmdHasArgs "npm" ["install"]
-    installIsFirst cmd = ["install"] `isPrefixOf` Shell.getArgsNoFlags cmd
-    packages cmd = stripInstallPrefix (Shell.getArgsNoFlags cmd)
+forgotToPinVersion :: Shell.Command -> Bool
+forgotToPinVersion cmd =
+  isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))
 
-    versionFixed package
-      | hasGitPrefix package = isVersionedGit package
-      | hasTarballSuffix package = True
-      | isFolder package = True
-      | otherwise = hasVersionSymbol package
+isNpmInstall :: Shell.Command -> Bool
+isNpmInstall = Shell.cmdHasArgs "npm" ["install"]
 
-    gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
-    pathPrefixes = ["/", "./", "../", "~/"]
-    tarballSuffixes = [".tar", ".tar.gz", ".tgz"]
+installIsFirst :: Shell.Command -> Bool
+installIsFirst cmd = ["install"] `isPrefixOf` Shell.getArgsNoFlags
+  (Shell.dropFlagArg ignoreFlags cmd)
 
-    hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]
-    hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]
-    isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]
-    isVersionedGit package = "#" `Text.isInfixOf` package
+packages :: Shell.Command -> [Text.Text]
+packages cmd = stripInstallPrefix
+  (Shell.getArgsNoFlags (Shell.dropFlagArg ignoreFlags cmd))
 
-    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
-      where
-        dropScope pkg =
-          if "@" `Text.isPrefixOf` pkg
-            then Text.dropWhile ('/' <) pkg
-            else pkg
-{-# INLINEABLE rule #-}
+versionFixed :: Text.Text -> Bool
+versionFixed package
+  | hasGitPrefix package = isVersionedGit package
+  | hasTarballSuffix package = True
+  | isFolder package = True
+  | otherwise = hasVersionSymbol package
 
+hasGitPrefix :: Text.Text -> Bool
+hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]
+
+hasTarballSuffix :: Text.Text -> Bool
+hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]
+
+isFolder :: Text.Text -> Bool
+isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]
+
+isVersionedGit :: Text.Text -> Bool
+isVersionedGit package = "#" `Text.isInfixOf` package
+
+hasVersionSymbol :: Text.Text -> Bool
+hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
+  where
+    dropScope pkg =
+      if "@" `Text.isPrefixOf` pkg
+        then Text.dropWhile ('/' <) pkg
+        else pkg
+
 stripInstallPrefix :: [Text.Text] -> [Text.Text]
 stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
+
+
+ignoreFlags :: [Text.Text]
+ignoreFlags = ["loglevel"]
+
+gitPrefixes :: [Text.Text]
+gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
+
+pathPrefixes :: [Text.Text]
+pathPrefixes = ["/", "./", "../", "~/"]
+
+tarballSuffixes :: [Text.Text]
+tarballSuffixes = [".tar", ".tar.gz", ".tgz"]
diff --git a/src/Hadolint/Rule/DL3045.hs b/src/Hadolint/Rule/DL3045.hs
--- a/src/Hadolint/Rule/DL3045.hs
+++ b/src/Hadolint/Rule/DL3045.hs
@@ -1,5 +1,6 @@
 module Hadolint.Rule.DL3045 (rule) where
 
+import qualified Data.Char as Char
 import Data.Map.Strict (Map)
 import qualified Data.Map.Strict as Map
 import Data.Maybe (fromMaybe)
@@ -34,7 +35,7 @@
     check line st (Copy (CopyArgs _ (TargetPath dest) _ _))
       | Acc s m <- state st, Just True <- Map.lookup s m = st -- workdir has been set
       | "/" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- absolute dest. normal
-      | ":\\" `Text.isPrefixOf` Text.drop 1 (Text.dropAround quotePredicate dest) = st -- absolute dest. windows
+      | isWindowsAbsolute (Text.dropAround quotePredicate dest) = st -- absolute dest. windows
       | "$" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- dest is a variable
       | otherwise = st |> addFail CheckFailure {..}
     check _ st _ = st
@@ -80,4 +81,9 @@
 quotePredicate c
   | c == '"' = True
   | c == '\'' = True
+  | otherwise = False
+
+isWindowsAbsolute :: Text.Text -> Bool
+isWindowsAbsolute path
+  | Char.isLetter (Text.index path 0) && (':' == Text.index path 1) = True
   | otherwise = False
diff --git a/src/Hadolint/Rule/DL3058.hs b/src/Hadolint/Rule/DL3058.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3058.hs
@@ -0,0 +1,26 @@
+module Hadolint.Rule.DL3058 (rule) where
+
+import qualified Data.Map as Map
+import Text.Email.Validate as Email
+import Hadolint.Rule
+import Language.Docker.Syntax
+import Data.Text.Encoding
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $
+  fmap labelIsNotEmailRule (Map.keys (Map.filter (== Email) labelschema))
+{-# INLINEABLE rule #-}
+
+
+labelIsNotEmailRule :: LabelName -> Rule args
+labelIsNotEmailRule label = simpleRule code severity message check
+  where
+    code = "DL3058"
+    severity = DLWarningC
+    message = "Label `" <> label <> "` is not a valid email format - must be conform to RFC5322."
+    check (Label ls) = null $ getBadEmailformatLabels label ls
+    check _ = True
+
+getBadEmailformatLabels :: LabelName -> Pairs -> Pairs
+getBadEmailformatLabels lbl pairs = [(l, v) | (l, v) <- pairs, l == lbl, not (Email.isValid (encodeUtf8 v))]
diff --git a/src/Hadolint/Shell.hs b/src/Hadolint/Shell.hs
--- a/src/Hadolint/Shell.hs
+++ b/src/Hadolint/Shell.hs
@@ -18,14 +18,14 @@
   { arg :: Text,
     partId :: Int
   }
-  deriving (Show)
+  deriving (Eq, Show)
 
 data Command = Command
   { name :: Text.Text,
     arguments :: [CmdPart],
     flags :: [CmdPart]
   }
-  deriving (Show)
+  deriving (Eq, Show)
 
 data ParsedShell = ParsedShell
   { original :: Text.Text,
@@ -202,8 +202,8 @@
 getFlagArg :: Text.Text -> Command -> [Text.Text]
 getFlagArg flag Command {arguments, flags} = extractArgs
   where
-    idsToGet = [getValueId fId arguments | CmdPart f fId <- flags, f == flag]
-    extractArgs = [arg | (CmdPart arg aId) <- arguments, aId `elem` idsToGet]
+    idsToGet = Set.fromList [getValueId fId arguments | CmdPart f fId <- flags, f == flag]
+    extractArgs = [arg | (CmdPart arg aId) <- arguments, aId `Set.member` idsToGet]
 
 getValueId :: Int -> [CmdPart] -> Int
 getValueId fId flags = foldl min (maxBound :: Int) $ filter (> fId) $ map partId flags
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
--- a/test/ConfigSpec.hs
+++ b/test/ConfigSpec.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module ConfigSpec where
 
 import Control.Monad (unless)
diff --git a/test/DL3000.hs b/test/DL3000.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3000.hs
@@ -0,0 +1,26 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3001.hs
@@ -0,0 +1,16 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3002.hs
@@ -0,0 +1,86 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3003.hs
@@ -0,0 +1,12 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3004.hs
@@ -0,0 +1,19 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3005.hs
@@ -0,0 +1,16 @@
+module DL3005 (tests) where
+
+import Helpers
+import Test.Hspec
+
+
+tests :: SpecWith ()
+tests = do
+  let ?rulesConfig = mempty
+  describe "DL3005" $ do
+    it "apt-get upgrade" $ do
+      ruleCatches "DL3005" "RUN apt-get update && apt-get upgrade"
+      onBuildRuleCatches "DL3005" "RUN apt-get update && apt-get upgrade"
+    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
new file mode 100644
--- /dev/null
+++ b/test/DL3006.hs
@@ -0,0 +1,42 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3007.hs
@@ -0,0 +1,20 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3008.hs
@@ -0,0 +1,61 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3009.hs
@@ -0,0 +1,77 @@
+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
diff --git a/test/DL3010.hs b/test/DL3010.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3010.hs
@@ -0,0 +1,12 @@
+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"
diff --git a/test/DL3011.hs b/test/DL3011.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3011.hs
@@ -0,0 +1,12 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3012.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3013.hs
@@ -0,0 +1,139 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3014.hs
@@ -0,0 +1,31 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3015.hs
@@ -0,0 +1,26 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3016.hs
@@ -0,0 +1,123 @@
+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/DL3017.hs b/test/DL3017.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3017.hs
@@ -0,0 +1,13 @@
+module DL3017 (tests) where
+
+import Helpers
+import Test.Hspec
+
+
+tests :: SpecWith ()
+tests = do
+  let ?rulesConfig = mempty
+  describe "DL3017 - Do not use `apk upgrade`." $ do
+    it "apk upgrade" $ do
+      ruleCatches "DL3017" "RUN apk update && apk upgrade"
+      onBuildRuleCatches "DL3017" "RUN apk update && apk upgrade"
diff --git a/test/DL3018.hs b/test/DL3018.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3018.hs
@@ -0,0 +1,86 @@
+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
diff --git a/test/DL3019.hs b/test/DL3019.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3019.hs
@@ -0,0 +1,16 @@
+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"
diff --git a/test/DL3020.hs b/test/DL3020.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3020.hs
@@ -0,0 +1,18 @@
+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 zip" $ ruleCatchesNot "DL3020" "ADD file.zip /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/"
diff --git a/test/DL3021.hs b/test/DL3021.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3021.hs
@@ -0,0 +1,13 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3022.hs
@@ -0,0 +1,29 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3023.hs
@@ -0,0 +1,26 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3024.hs
@@ -0,0 +1,27 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3025.hs
@@ -0,0 +1,36 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3026.hs
@@ -0,0 +1,56 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3027.hs
@@ -0,0 +1,19 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3028.hs
@@ -0,0 +1,42 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3029.hs
@@ -0,0 +1,12 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3030.hs
@@ -0,0 +1,18 @@
+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/DL3031.hs b/test/DL3031.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3031.hs
@@ -0,0 +1,18 @@
+module DL3031 (tests) where
+
+import Helpers
+import Test.Hspec
+
+
+tests :: SpecWith ()
+tests = do
+  let ?rulesConfig = mempty
+  describe "DL3031" $ do
+    it "yum update" $ do
+      ruleCatches "DL3031" "RUN yum update"
+      onBuildRuleCatches "DL3031" "RUN yum update"
+    it "not yum update" $ do
+      ruleCatchesNot "DL3031" "RUN yum install -y httpd-2.4.42 && yum clean all"
+      ruleCatchesNot "DL3031" "RUN bash -c `# not even a yum command`"
+      onBuildRuleCatchesNot "DL3031" "RUN yum install -y httpd-2.4.42 && yum clean all"
+      onBuildRuleCatchesNot "DL3031" "RUN bash -c `# not even a yum command`"
diff --git a/test/DL3032.hs b/test/DL3032.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3032.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3033.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3034.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3035.hs
@@ -0,0 +1,20 @@
+module DL3035 (tests) where
+
+import Helpers
+import Test.Hspec
+
+
+tests :: SpecWith ()
+tests = do
+  let ?rulesConfig = mempty
+  describe "DL3035" $ do
+    it "not ok: zypper update" $ do
+      ruleCatches "DL3035" "RUN zypper update"
+      ruleCatches "DL3035" "RUN zypper up"
+      onBuildRuleCatches "DL3035" "RUN zypper update"
+      onBuildRuleCatches "DL3035" "RUN zypper up"
+    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
new file mode 100644
--- /dev/null
+++ b/test/DL3036.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3037.hs
@@ -0,0 +1,28 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3038.hs
@@ -0,0 +1,18 @@
+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/DL3039.hs b/test/DL3039.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3039.hs
@@ -0,0 +1,18 @@
+module DL3039 (tests) where
+
+import Helpers
+import Test.Hspec
+
+
+tests :: SpecWith ()
+tests = do
+  let ?rulesConfig = mempty
+  describe "DL3039" $ do
+    it "dnf update" $ do
+      ruleCatches "DL3039" "RUN dnf upgrade"
+      ruleCatches "DL3039" "RUN dnf upgrade-minimal"
+      onBuildRuleCatches "DL3039" "RUN dnf upgrade"
+      onBuildRuleCatches "DL3039" "RUN dnf upgrade-minimal"
+    it "not dnf upgrade" $ do
+      ruleCatchesNot "DL3039" "RUN notdnf upgrade"
+      onBuildRuleCatchesNot "DL3039" "RUN notdnf upgrade"
diff --git a/test/DL3040.hs b/test/DL3040.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3040.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3041.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3042.hs
@@ -0,0 +1,34 @@
+module DL3042 (tests) where
+
+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"
diff --git a/test/DL3043.hs b/test/DL3043.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3043.hs
@@ -0,0 +1,43 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3044.hs
@@ -0,0 +1,42 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3045.hs
@@ -0,0 +1,152 @@
+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
diff --git a/test/DL3046.hs b/test/DL3046.hs
new file mode 100644
--- /dev/null
+++ b/test/DL3046.hs
@@ -0,0 +1,15 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3047.hs
@@ -0,0 +1,71 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3048.hs
@@ -0,0 +1,37 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3049.hs
@@ -0,0 +1,80 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3050.hs
@@ -0,0 +1,25 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3051.hs
@@ -0,0 +1,22 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3052.hs
@@ -0,0 +1,22 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3053.hs
@@ -0,0 +1,22 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3054.hs
@@ -0,0 +1,22 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3055.hs
@@ -0,0 +1,25 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3056.hs
@@ -0,0 +1,22 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3057.hs
@@ -0,0 +1,18 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL3058.hs
@@ -0,0 +1,22 @@
+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/DL4000.hs b/test/DL4000.hs
new file mode 100644
--- /dev/null
+++ b/test/DL4000.hs
@@ -0,0 +1,15 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL4001.hs
@@ -0,0 +1,69 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL4003.hs
@@ -0,0 +1,42 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL4004.hs
@@ -0,0 +1,45 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL4005.hs
@@ -0,0 +1,19 @@
+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
new file mode 100644
--- /dev/null
+++ b/test/DL4006.hs
@@ -0,0 +1,83 @@
+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
diff --git a/test/Helpers.hs b/test/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/test/Helpers.hs
@@ -0,0 +1,120 @@
+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.Formatter.TTY (formatCheck)
+import qualified Hadolint.Process
+import Hadolint.Rule (CheckFailure (..), Failures, RuleCode (..))
+import Language.Docker.Parser
+import Language.Docker.Syntax
+import Test.HUnit hiding (Label)
+import Test.Hspec
+
+
+assertChecks ::
+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  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
+
+assertOnBuildChecks ::
+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  Text.Text ->
+  (Failures -> IO a) ->
+  IO a
+assertOnBuildChecks dockerfile makeAssertions =
+  case parseText (dockerfile <> "\n") of
+    Left err -> assertFailure $ show err
+    Right dockerFile -> checkOnBuild dockerFile
+  where
+    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?rulesConfig (fmap wrapInOnBuild dockerFile)
+    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li
+    wrapInOnBuild i = i
+
+hasInvalidLines :: Failures -> Bool
+hasInvalidLines =
+  Foldl.fold
+    (Foldl.any (\CheckFailure {line} -> line <= 0))
+
+-- Assert a failed check exists for rule
+ruleCatches ::
+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  RuleCode ->
+  Text.Text ->
+  Assertion
+ruleCatches expectedCode dockerfile = assertChecks dockerfile f
+  where
+    f checks = do
+      failsWithSome expectedCode checks
+      assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks
+
+onBuildRuleCatches ::
+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  RuleCode ->
+  Text.Text ->
+  Assertion
+onBuildRuleCatches ruleCode dockerfile = assertOnBuildChecks dockerfile f
+  where
+    f checks = do
+      failsWith 1 ruleCode checks
+      assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks
+
+ruleCatchesNot ::
+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  RuleCode ->
+  Text.Text ->
+  Assertion
+ruleCatchesNot ruleCode dockerfile = assertChecks dockerfile f
+  where
+    f = failsWith 0 ruleCode
+
+onBuildRuleCatchesNot ::
+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
+  RuleCode ->
+  Text.Text ->
+  Assertion
+onBuildRuleCatchesNot ruleCode dockerfile = assertOnBuildChecks dockerfile f
+  where
+    f = failsWith 0 ruleCode
+
+formatChecksNoColor :: Foldable f => f CheckFailure -> Text.Text
+formatChecksNoColor = Foldl.fold (Foldl.premap (\c -> formatCheck True "line" c <> "\n") Foldl.mconcat)
+
+failsWithSome :: HasCallStack => RuleCode -> Failures -> Assertion
+failsWithSome expectedCode failures =
+  when (null matched) $
+    assertFailure $ "I was expecting to catch at least one error for " <> show (unRuleCode expectedCode)
+  where
+    matched = Seq.filter (\CheckFailure {code} -> expectedCode == code) failures
+
+failsWith :: HasCallStack => Int -> RuleCode -> Failures -> Assertion
+failsWith times expectedCode failures =
+  when (length matched /= times) $
+    assertFailure $
+      "I was expecting to catch exactly " <> show times <> " error(s) for " <> show (unRuleCode expectedCode) <> ". Found: \n"
+        <> (Text.unpack . formatChecksNoColor $ matched)
+  where
+    matched = Seq.filter (\CheckFailure {code} -> expectedCode == code) failures
+
+failsShellcheck :: HasCallStack => Failures -> Assertion
+failsShellcheck checks =
+  when (null matched) $
+    assertFailure
+      "I was expecting to catch at least one error with shellcheck"
+  where
+    matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks
+
+passesShellcheck :: HasCallStack => Failures -> Assertion
+passesShellcheck checks =
+  unless (null matched) $
+    assertFailure $
+      "I was expecting to catch no errors with shellcheck. Found: \n"
+        <> (Text.unpack . formatChecksNoColor $ matched)
+  where
+    matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks
diff --git a/test/ShellSpec.hs b/test/ShellSpec.hs
--- a/test/ShellSpec.hs
+++ b/test/ShellSpec.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module ShellSpec where
 
 import Data.Text as Text
@@ -11,32 +9,45 @@
   describe "Shell unit tests" $ do
   --
     it "getFlagArgs" $ do
-      let cmd = Command (Text.pack "useradd")
-                        [ CmdPart (Text.pack "12345") 2,
-                          CmdPart (Text.pack "67890") 4,
-                          CmdPart (Text.pack "luser") 5 ]
-                        [ CmdPart (Text.pack "u") 1,
-                          CmdPart (Text.pack "u") 3 ]
-       in getFlagArg (Text.pack "u") cmd `shouldBe` [ Text.pack "12345",
-                                                      Text.pack "67890" ]
-      let cmd = Command (Text.pack "useradd")
-                        [ CmdPart (Text.pack "12345") 2,
-                          CmdPart (Text.pack "luser") 3 ]
-                        [ CmdPart (Text.pack "u") 1 ]
-       in getFlagArg (Text.pack "u") cmd `shouldBe` [ Text.pack "12345" ]
+      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 (Text.pack "useradd")
-                        [ CmdPart (Text.pack "12345") 2,
-                          CmdPart (Text.pack "luser") 3 ]
-                        [ CmdPart (Text.pack "u") 1 ]
-       in getFlagArg (Text.pack "f") cmd `shouldBe` []
+      let cmd = Command "useradd"
+                        [ CmdPart "12345" 2,
+                          CmdPart "luser" 3 ]
+                        [ CmdPart "u" 1 ]
+       in getFlagArg "f" cmd `shouldBe` []
   --
     it "hasFlag" $ do
-      let cmd = Command (Text.pack "useradd")
-                        [ CmdPart (Text.pack "luser") 1 ]
-                        [ CmdPart (Text.pack "l") 0 ]
-       in hasFlag (Text.pack "l") cmd `shouldBe` True
-      let cmd = Command (Text.pack "useradd")
-                        [ CmdPart (Text.pack "luser") 1 ]
-                        [ CmdPart (Text.pack "l") 0 ]
-       in hasFlag (Text.pack "f") cmd `shouldBe` False
+      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
new file mode 100644
--- /dev/null
+++ b/test/Shellcheck.hs
@@ -0,0 +1,121 @@
+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 powershell" $
+      let dockerFile =
+            Text.unlines
+              [ "SHELL [\"pwsh\", \"-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,1998 +1,227 @@
-{-# LANGUAGE ImplicitParams #-}
-{-# LANGUAGE OverloadedLists #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-import qualified ConfigSpec
-import qualified Control.Foldl as Foldl
-import Control.Monad (unless, when)
-import qualified Data.Sequence as Seq
-import qualified Data.Map as Map
-import qualified Data.Text as Text
-import Hadolint.Formatter.TTY (formatCheck, formatError)
-import qualified Hadolint.Process
-import Hadolint.Rule (CheckFailure (..), Failures, RuleCode (..))
-import Hadolint.Rule as Rule
-import Language.Docker.Parser
-import Language.Docker.Syntax
-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 "FROM rules" $ do
-      it "no untagged" $ ruleCatches "DL3006" "FROM debian"
-      it "no untagged with name" $ ruleCatches "DL3006" "FROM debian AS builder"
-      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 platform flag" $ ruleCatches "DL3029" "FROM --platform=linux debian:jessie"
-      it "no platform flag" $ ruleCatchesNot "DL3029" "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"
-      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
-    --
-    describe "no root or sudo rules" $ do
-      it "sudo" $ do
-        ruleCatches "DL3004" "RUN sudo apt-get update"
-        onBuildRuleCatches "DL3004" "RUN sudo apt-get update"
-
-      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
-
-      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"
-    --
-    describe "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"
-    --
-    describe "gem" $
-      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"
-    --
-    describe "yum rules" $ do
-      it "yum update" $ do
-        ruleCatches "DL3031" "RUN yum update"
-        ruleCatchesNot "DL3031" "RUN yum install -y httpd-2.4.42 && yum clean all"
-        ruleCatchesNot "DL3031" "RUN bash -c `# not even a yum command`"
-        onBuildRuleCatches "DL3031" "RUN yum update"
-        onBuildRuleCatchesNot "DL3031" "RUN yum install -y httpd-2.4.42 && yum clean all"
-        onBuildRuleCatchesNot "DL3031" "RUN bash -c `# not even a yum command`"
-      it "yum version pinning" $ do
-        ruleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"
-        ruleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"
-        ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
-        onBuildRuleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"
-        onBuildRuleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"
-        onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
-      it "yum no clean all" $ do
-        ruleCatches "DL3032" "RUN yum install -y mariadb-10.4"
-        ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
-        ruleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
-        onBuildRuleCatches "DL3032" "RUN yum install -y mariadb-10.4"
-        onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
-        onBuildRuleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
-      it "yum non-interactive" $ do
-        ruleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"
-        ruleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"
-        ruleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
-        onBuildRuleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"
-        onBuildRuleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"
-        onBuildRuleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
-    --
-    describe "zypper rules" $ do
-      it "zupper update" $ do
-        ruleCatches "DL3035" "RUN zypper update"
-        ruleCatches "DL3035" "RUN zypper up"
-        ruleCatches "DL3035" "RUN zypper dist-upgrade"
-        ruleCatches "DL3035" "RUN zypper dup"
-        onBuildRuleCatches "DL3035" "RUN zypper update"
-        onBuildRuleCatches "DL3035" "RUN zypper up"
-        onBuildRuleCatches "DL3035" "RUN zypper dist-upgrade"
-        onBuildRuleCatches "DL3035" "RUN zypper dup"
-      it "zypper version pinning" $ do
-        -- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'
-        -- and '<' have to be escaped. Hence the double escaping.
-        ruleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"
-        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"
-        onBuildRuleCatches "DL3037" "RUN zypper install -y tomcat && 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"
-      it "zypper no clean all" $ do
-        ruleCatches "DL3036" "RUN zypper install -y mariadb=10.4"
-        ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
-        ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
-        onBuildRuleCatches "DL3036" "RUN zypper install -y mariadb=10.4"
-        onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
-        onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
-      it "zypper non-interactive" $ do
-        ruleCatches "DL3034" "RUN zypper install 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"
-        onBuildRuleCatches "DL3034" "RUN zypper install 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"
-    --
-    describe "dnf rules" $ do
-      it "dnf update" $ do
-        ruleCatches "DL3039" "RUN dnf upgrade"
-        onBuildRuleCatches "DL3039" "RUN dnf upgrade"
-      it "dnf version pinning" $ do
-        ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
-        ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-        onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
-        onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-      it "dnf no clean all" $ do
-        ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
-        ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
-        onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
-        onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
-      it "dnf non-interactive" $ do
-        ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
-        ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-    --
-    describe "dnf rules" $ do
-      it "dnf update" $ do
-        ruleCatches "DL3039" "RUN dnf upgrade"
-        ruleCatches "DL3039" "RUN dnf upgrade-minimal"
-        ruleCatchesNot "DL3039" "RUN notdnf upgrade"
-        onBuildRuleCatches "DL3039" "RUN dnf upgrade"
-        onBuildRuleCatches "DL3039" "RUN dnf upgrade-minimal"
-        onBuildRuleCatchesNot "DL3039" "RUN notdnf upgrade"
-      it "dnf version pinning" $ do
-        ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
-        ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-        ruleCatchesNot "DL3041" "RUN notdnf install tomcat"
-        onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"
-        onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-        onBuildRuleCatchesNot "DL3041" "RUN notdnf install tomcat"
-      it "dnf no clean all" $ do
-        ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
-        ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
-        ruleCatchesNot "DL3040" "RUN notdnf install mariadb"
-        onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
-        onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
-        onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"
-      it "dnf non-interactive" $ do
-        ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
-        ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-        ruleCatchesNot "DL3038" "RUN notdnf install httpd"
-        onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatchesNot "DL3038" "RUN notdnf install httpd"
-    --
-    describe "apt-get rules" $ do
-      it "apt" $
-        let dockerFile =
-              [ "FROM ubuntu",
-                "RUN apt install python"
-              ]
-         in do
-              ruleCatches "DL3027" $ Text.unlines dockerFile
-              onBuildRuleCatches "DL3027" $ Text.unlines dockerFile
-      it "apt-get upgrade" $ do
-        ruleCatches "DL3005" "RUN apt-get update && apt-get upgrade"
-        onBuildRuleCatches "DL3005" "RUN apt-get update && apt-get upgrade"
-      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"
-      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 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 "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 "has deprecated maintainer" $
-        ruleCatches "DL4000" "FROM busybox\nMAINTAINER hudu@mail.com"
-    --
-    describe "apk add rules" $ do
-      it "apk upgrade" $ do
-        ruleCatches "DL3017" "RUN apk update && apk upgrade"
-        onBuildRuleCatches "DL3017" "RUN apk update && apk upgrade"
-      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 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 "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
-    --
-    describe "EXPOSE rules" $ do
-      it "invalid port" $ ruleCatches "DL3011" "EXPOSE 80000"
-      it "valid port" $ ruleCatchesNot "DL3011" "EXPOSE 60000"
-    --
-    describe "pip pinning" $ 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"
-
-    --
-    describe "pip cache dir" $ 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"
-    --
-    describe "npm pinning" $ 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"
-    --
-    describe "use 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"
-    --
-    --
-    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 powershell" $
-        let dockerFile =
-              Text.unlines
-                [ "SHELL [\"pwsh\", \"-c\"]",
-                  "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-                ]
-         in do
-              assertChecks dockerFile passesShellcheck
-              assertOnBuildChecks dockerFile passesShellcheck
-    --
-    --
-    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"
-    --
-    describe "`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 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
-    --
-    describe "other rules" $ 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"
-      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"
-      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
-
-      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"
-      it "using add" $ ruleCatches "DL3020" "ADD file /usr/src/app/"
-
-      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"
-      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"
-      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 "scratch" $ ruleCatchesNot "DL3006" "FROM scratch"
-    --
-    describe "add files and archives" $ do
-      it "add for tar" $ ruleCatchesNot "DL3020" "ADD file.tar /usr/src/app/"
-      it "add for zip" $ ruleCatchesNot "DL3020" "ADD file.zip /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/"
-    --
-    describe "copy last argument" $ 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/"
-    --
-    describe "copy from existing 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
-    --
-    describe "copy from own FROM" $ 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
-    --
-    describe "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
-    --
-    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, or end of input "
-        case ast of
-          Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError 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 "JSON notation in ENTRYPOINT and CMD" $ 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
-
-    --
-    describe "Detects missing pipefail option" $ 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
-    --
-    describe "Allowed docker registries" $ 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
-    --
-    describe "Wget or Curl" $ 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")
-    --
-    describe "Wget no progress" $ 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
-    --
-    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"
-    --
-    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"
-    --
-    describe "ONBUILD" $ 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\""
-    --
-    describe "Selfreferencing `ENV`s" $ 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}\""
-    --
-    describe "warn when using `useradd` with long UID and without `-l`" $ 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"
-    --
-    describe "Missing label rule tests" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("foo", Rule.RawText)]) False
-       in 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")
-    describe "Strict Labels" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("required", Rule.RawText)]) True
-       in 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\""
-    describe "Label is not empty rule" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("emptylabel", Rule.RawText)]) False
-       in 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=\"\""
-    describe "Label is not URL rule" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("urllabel", Rule.Url)]) False
-       in 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\""
-    describe "Label is not RFC3339 date rule" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("datelabel", Rule.Rfc3339)]) False
-       in 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\""
-    describe "Label is not SPDX license identifier rule" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("spdxlabel", Rule.Spdx)]) False
-       in 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\""
-    describe "Label is not git hash rule" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("githash", Rule.GitHash)]) False
-       in 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\""
-    describe "Label is not semantic version rule" $
-      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("semver", Rule.SemVer)]) False
-       in 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\""
-    --
-    describe "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\""
-    --
-    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
-
-assertChecks ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
-  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
-
-assertOnBuildChecks ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
-  Text.Text ->
-  (Failures -> IO a) ->
-  IO a
-assertOnBuildChecks dockerfile makeAssertions =
-  case parseText (dockerfile <> "\n") of
-    Left err -> assertFailure $ show err
-    Right dockerFile -> checkOnBuild dockerFile
-  where
-    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?rulesConfig (fmap wrapInOnBuild dockerFile)
-    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li
-    wrapInOnBuild i = i
-
-hasInvalidLines :: Failures -> Bool
-hasInvalidLines =
-  Foldl.fold
-    (Foldl.any (\CheckFailure {line} -> line <= 0))
-
--- Assert a failed check exists for rule
-ruleCatches ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
-  RuleCode ->
-  Text.Text ->
-  Assertion
-ruleCatches expectedCode dockerfile = assertChecks dockerfile f
-  where
-    f checks = do
-      failsWithSome expectedCode checks
-      assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks
-
-onBuildRuleCatches ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
-  RuleCode ->
-  Text.Text ->
-  Assertion
-onBuildRuleCatches ruleCode dockerfile = assertOnBuildChecks dockerfile f
-  where
-    f checks = do
-      failsWith 1 ruleCode checks
-      assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks
-
-ruleCatchesNot ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
-  RuleCode ->
-  Text.Text ->
-  Assertion
-ruleCatchesNot ruleCode dockerfile = assertChecks dockerfile f
-  where
-    f = failsWith 0 ruleCode
-
-onBuildRuleCatchesNot ::
-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>
-  RuleCode ->
-  Text.Text ->
-  Assertion
-onBuildRuleCatchesNot ruleCode dockerfile = assertOnBuildChecks dockerfile f
-  where
-    f = failsWith 0 ruleCode
-
-formatChecksNoColor :: Foldable f => f CheckFailure -> Text.Text
-formatChecksNoColor = Foldl.fold (Foldl.premap (\c -> formatCheck True "line" c <> "\n") Foldl.mconcat)
-
-failsWithSome :: HasCallStack => RuleCode -> Failures -> Assertion
-failsWithSome expectedCode failures =
-  when (null matched) $
-    assertFailure $ "I was expecting to catch at least one error for " <> show (unRuleCode expectedCode)
-  where
-    matched = Seq.filter (\CheckFailure {code} -> expectedCode == code) failures
-
-failsWith :: HasCallStack => Int -> RuleCode -> Failures -> Assertion
-failsWith times expectedCode failures =
-  when (length matched /= times) $
-    assertFailure $
-      "I was expecting to catch exactly " <> show times <> " error(s) for " <> show (unRuleCode expectedCode) <> ". Found: \n"
-        <> (Text.unpack . formatChecksNoColor $ matched)
-  where
-    matched = Seq.filter (\CheckFailure {code} -> expectedCode == code) failures
-
-failsShellcheck :: HasCallStack => Failures -> Assertion
-failsShellcheck checks =
-  when (null matched) $
-    assertFailure
-      "I was expecting to catch at least one error with shellcheck"
-  where
-    matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks
-
-passesShellcheck :: HasCallStack => Failures -> Assertion
-passesShellcheck checks =
-  unless (null matched) $
-    assertFailure $
-      "I was expecting to catch no errors with shellcheck. Found: \n"
-        <> (Text.unpack . formatChecksNoColor $ matched)
-  where
-    matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks
+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 DL3017
+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 DL3031
+import qualified DL3032
+import qualified DL3033
+import qualified DL3034
+import qualified DL3035
+import qualified DL3036
+import qualified DL3037
+import qualified DL3038
+import qualified DL3039
+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 DL4001
+import qualified DL4003
+import qualified DL4004
+import qualified DL4005
+import qualified DL4006
+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, or end of input "
+        case ast of
+          Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError 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
+    DL3017.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
+    DL3031.tests
+    DL3032.tests
+    DL3033.tests
+    DL3034.tests
+    DL3035.tests
+    DL3036.tests
+    DL3037.tests
+    DL3038.tests
+    DL3039.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
+    DL4001.tests
+    DL4003.tests
+    DL4004.tests
+    DL4005.tests
+    DL4006.tests
