diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -152,6 +152,62 @@
 
 Inline ignores will only work if place directly above the instruction.
 
+## Linting Labels
+
+Hadolint has the ability to check that specific labels be present and conform
+to a predefined label schema.
+First a label schema must be defined either via commandline:
+```bash
+$ hadolint --require-label author:text --require-label version:semver Dockerfile
+```
+or via config file:
+
+```yaml
+label-schema:
+  author: text
+  created: rfc3339
+  version: semver
+  documentation: url
+  git-revision: hash
+  license: spdx
+```
+The value of a label can be either of `text`, `url`, `semver`, `hash` or
+`rfc3339`:
+| Schema  | Description                                        |
+|:--------|:---------------------------------------------------|
+| text    | Anything                                           |
+| rfc3339 | A time, formatted according to [RFC 3339][rfc3339] |
+| semver  | A [semantic version][semver]                       |
+| url     | A URI as described in [RFC 3986][rfc3986]          |
+| hash    | Either a short or a long [Git hash][githash]       |
+| spdx    | An [SPDX license identifier][spdxid]               |
+
+By default, Hadolint ignores any label not specified in the label schema. To
+warn on such additional labels, turn on strict labels:
+```bash
+$ hadolint --strict-labels --require-label version:semver Dockerfile
+```
+or in the config file:
+```yaml
+strict-labels: true
+```
+When strict labels is enabled, but no label schema has been specified, Hadolint
+will warn if any label is present.
+
+### Note on dealing with variables in labels
+It is a common pattern to fill the value of a label not statically, but rather
+dynamically at build time by using a variable:
+```dockerfile
+FROM debian:buster
+ARG VERSION="du-jour"
+LABEL version="${VERSION}"
+```
+To allow this, the label schema must specify `text` as value for that label:
+```yaml
+label-schema:
+  version: text
+```
+
 ## Integrations
 
 To get most of `hadolint` it is useful to integrate it as a check to your CI
@@ -190,7 +246,7 @@
 | [DL3009](https://github.com/hadolint/hadolint/wiki/DL3009)   | Delete the apt-get lists after installing something.                                                                                                |
 | [DL3010](https://github.com/hadolint/hadolint/wiki/DL3010)   | Use ADD for extracting archives into an image.                                                                                                      |
 | [DL3011](https://github.com/hadolint/hadolint/wiki/DL3011)   | Valid UNIX ports range from 0 to 65535.                                                                                                             |
-| [DL3012](https://github.com/hadolint/hadolint/wiki/DL3012)   | Provide an email address or URL as maintainer.                                                                                                      |
+| [DL3012](https://github.com/hadolint/hadolint/wiki/DL3012)   | Multiple `HEALTHCHECK` instructions.                                                                                                                |
 | [DL3013](https://github.com/hadolint/hadolint/wiki/DL3013)   | Pin versions in pip.                                                                                                                                |
 | [DL3014](https://github.com/hadolint/hadolint/wiki/DL3014)   | Use the `-y` switch.                                                                                                                                |
 | [DL3015](https://github.com/hadolint/hadolint/wiki/DL3015)   | Avoid additional packages by specifying --no-install-recommends.                                                                                    |
@@ -223,6 +279,19 @@
 | [DL3042](https://github.com/hadolint/hadolint/wiki/DL3042)   | Avoid cache directory with `pip install --no-cache-dir <package>`.                                                                                  |
 | [DL3043](https://github.com/hadolint/hadolint/wiki/DL3043)   | `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction.                                                                      |
 | [DL3044](https://github.com/hadolint/hadolint/wiki/DL3044)   | Do not refer to an environment variable within the same `ENV` statement where it is defined.                                                        |
+| [DL3045](https://github.com/hadolint/hadolint/wiki/DL3045)   | `COPY` to a relative destination without `WORKDIR` set.                                                                                             |
+| [DL3046](https://github.com/hadolint/hadolint/wiki/DL3046)   | `useradd` without flag `-l` and high UID will result in excessively large Image.                                                                    |
+| [DL3047](https://github.com/hadolint/hadolint/wiki/DL3047)   | `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files.                                       |
+| [DL3048](https://github.com/hadolint/hadolint/wiki/DL3048)   | Invalid Label Key                                                                                                                                   |
+| [DL3049](https://github.com/hadolint/hadolint/wiki/DL3049)   | Label `<label>` is missing.                                                                                                                         |
+| [DL3050](https://github.com/hadolint/hadolint/wiki/DL3050)   | Superfluous label(s) present.                                                                                                                       |
+| [DL3051](https://github.com/hadolint/hadolint/wiki/DL3051)   | Label `<label>` is empty.                                                                                                                           |
+| [DL3052](https://github.com/hadolint/hadolint/wiki/DL3052)   | Label `<label>` is not a valid URL.                                                                                                                 |
+| [DL3053](https://github.com/hadolint/hadolint/wiki/DL3053)   | Label `<label>` is not a valid time format - must be conform to RFC3339.                                                                            |
+| [DL3054](https://github.com/hadolint/hadolint/wiki/DL3054)   | Label `<label>` is not a valid SPDX license identifier.                                                                                             |
+| [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.                                                                                                                  |
 | [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.                                                                                                                  |
@@ -342,3 +411,8 @@
 [create an issue]: https://github.com/hadolint/hadolint/issues/new
 [dockerfile reference]: http://docs.docker.com/engine/reference/builder/
 [syntax.hs]: https://www.stackage.org/haddock/nightly-2018-01-07/language-docker-2.0.1/Language-Docker-Syntax.html
+[rfc3339]: https://www.ietf.org/rfc/rfc3339.txt
+[semver]: https://semver.org/
+[rfc3986]: https://www.ietf.org/rfc/rfc3986.txt
+[githash]: https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection
+[spdxid]: https://spdx.org/licenses/
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE NamedFieldPuns #-}
 {-# LANGUAGE TemplateHaskell #-}
@@ -5,19 +6,24 @@
 module Main where
 
 import Control.Applicative
-import Data.String (IsString (fromString))
+import qualified Data.Bifunctor as Bifunctor
 import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map as Map
+import Data.Maybe
 import qualified Data.Sequence as Seq
 import qualified Data.Set as Set
+import Data.String (IsString (fromString))
+import qualified Data.Text as Text
 import qualified Data.Version
 import qualified Development.GitRev
 import qualified Hadolint
-import Data.Maybe
+import qualified Hadolint.Rule as Rule
 import Options.Applicative
   ( Parser,
     action,
     argument,
     completeWith,
+    eitherReader,
     execParser,
     fullDesc,
     header,
@@ -29,6 +35,7 @@
     metavar,
     option,
     progDesc,
+    ReadM,
     short,
     showDefaultWith,
     str,
@@ -85,6 +92,9 @@
 
     nocolor = switch (long "no-color" <> help "Don't colorize output")
 
+    strictlabels = switch (long "strict-labels"
+        <> help "Do not permit labels other than specified in `label-schema`")
+
     configFile =
       optional
         ( strOption
@@ -108,37 +118,37 @@
     errorList =
       many
         ( strOption
-          ( long "error"
-              <> help "Make the rule `RULECODE` have the level `error`"
-              <> metavar "RULECODE"
-          )
+            ( long "error"
+                <> help "Make the rule `RULECODE` have the level `error`"
+                <> metavar "RULECODE"
+            )
         )
 
     warningList =
       many
         ( strOption
-          ( long "warning"
-              <> help "Make the rule `RULECODE` have the level `warning`"
-              <> metavar "RULECODE"
-          )
+            ( long "warning"
+                <> help "Make the rule `RULECODE` have the level `warning`"
+                <> metavar "RULECODE"
+            )
         )
 
     infoList =
       many
         ( strOption
-          ( long "info"
-              <> help "Make the rule `RULECODE` have the level `info`"
-              <> metavar "RULECODE"
-          )
+            ( long "info"
+                <> help "Make the rule `RULECODE` have the level `info`"
+                <> metavar "RULECODE"
+            )
         )
 
     styleList =
       many
         ( strOption
-          ( long "style"
-              <> help "Make the rule `RULECODE` have the level `style`"
-              <> metavar "RULECODE"
-          )
+            ( long "style"
+                <> help "Make the rule `RULECODE` have the level `style`"
+                <> metavar "RULECODE"
+            )
         )
 
     ignoreList =
@@ -161,8 +171,22 @@
         <*> ignoreList
         <*> parseRulesConfig
 
+    labels = Map.fromList
+        <$> many
+          ( option readSingleLabelSchema
+              ( long "require-label"
+                  <> help "The option --require-label=label:format makes Hadolint check that the label `label` conforms to format requirement `format`"
+                  <> metavar "LABELSCHEMA (e.g. maintainer:text)"
+              )
+          )
+
     parseRulesConfig =
-      Hadolint.RulesConfig . Set.fromList . fmap fromString
+      Hadolint.RulesConfig
+        <$> parseAllowedRegistries
+        <*> labels
+        <*> strictlabels
+
+    parseAllowedRegistries = Set.fromList . fmap fromString
         <$> many
           ( strOption
               ( long "trusted-registry"
@@ -171,20 +195,31 @@
               )
           )
 
+type SingleLabelSchema = (Rule.LabelName, Rule.LabelType)
+
+readSingleLabelSchema :: ReadM SingleLabelSchema
+readSingleLabelSchema = eitherReader $ \s -> labelParser (Text.pack s)
+
+labelParser :: Text.Text -> Either String (Rule.LabelName, Rule.LabelType)
+labelParser l =
+    case Bifunctor.second (Rule.read . Text.drop 1) $ Text.breakOn ":" l of
+      (ln, Right lt) -> Right (ln, lt)
+      (_, Left e) -> Left $ Text.unpack e
+
 noFailure :: Hadolint.Result s e -> Bool
-noFailure (Hadolint.Result Seq.Empty Seq.Empty) = True
+noFailure (Hadolint.Result _ Seq.Empty Seq.Empty) = True
 noFailure _ = False
 
-exitProgram :: CommandOptions -> Hadolint.Result s e -> IO()
+exitProgram :: Foldable f => CommandOptions -> f (Hadolint.Result s e) -> IO ()
 exitProgram cmd res
-  | noFail cmd                                 = exitSuccess
+  | noFail cmd = exitSuccess
   | Hadolint.shallSkipErrorStatus (format cmd) = exitSuccess
-  | noFailure res                              = exitSuccess
-  | otherwise                                  = exitFailure
+  | all noFailure res = exitSuccess
+  | otherwise = exitFailure
 
-runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO()
+runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO ()
 runLint cmd conf files = do
-  res <-  Hadolint.lint conf files
+  res <- Hadolint.lintIO conf files
   noColorEnv <- lookupEnv "NO_COLOR"
   let noColor = nocolor cmd || isJust noColorEnv
   Hadolint.printResults (format cmd) noColor res
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: 06df88cbceaf76da3599a8943568f912b17a63d1795839eb4da974832e1ae0d4
+-- hash: 6dc5574db9b4edf093ae954fa31aa254d6a3b1a37aa6dc0bf793182d24daf375
 
 name:           hadolint
-version:        1.23.0
+version:        2.0.0
 synopsis:       Dockerfile Linter JavaScript API
 description:    A smarter Dockerfile linter that helps you build best practice Docker images.
 category:       Development
@@ -40,8 +40,75 @@
       Hadolint.Formatter.Format
       Hadolint.Formatter.Json
       Hadolint.Formatter.TTY
+      Hadolint.Ignore
       Hadolint.Lint
-      Hadolint.Rules
+      Hadolint.Process
+      Hadolint.Rule
+      Hadolint.Rule.DL3000
+      Hadolint.Rule.DL3001
+      Hadolint.Rule.DL3002
+      Hadolint.Rule.DL3003
+      Hadolint.Rule.DL3004
+      Hadolint.Rule.DL3005
+      Hadolint.Rule.DL3006
+      Hadolint.Rule.DL3007
+      Hadolint.Rule.DL3008
+      Hadolint.Rule.DL3009
+      Hadolint.Rule.DL3010
+      Hadolint.Rule.DL3011
+      Hadolint.Rule.DL3012
+      Hadolint.Rule.DL3013
+      Hadolint.Rule.DL3014
+      Hadolint.Rule.DL3015
+      Hadolint.Rule.DL3016
+      Hadolint.Rule.DL3017
+      Hadolint.Rule.DL3018
+      Hadolint.Rule.DL3019
+      Hadolint.Rule.DL3020
+      Hadolint.Rule.DL3021
+      Hadolint.Rule.DL3022
+      Hadolint.Rule.DL3023
+      Hadolint.Rule.DL3024
+      Hadolint.Rule.DL3025
+      Hadolint.Rule.DL3026
+      Hadolint.Rule.DL3027
+      Hadolint.Rule.DL3028
+      Hadolint.Rule.DL3029
+      Hadolint.Rule.DL3030
+      Hadolint.Rule.DL3031
+      Hadolint.Rule.DL3032
+      Hadolint.Rule.DL3033
+      Hadolint.Rule.DL3034
+      Hadolint.Rule.DL3035
+      Hadolint.Rule.DL3036
+      Hadolint.Rule.DL3037
+      Hadolint.Rule.DL3038
+      Hadolint.Rule.DL3039
+      Hadolint.Rule.DL3040
+      Hadolint.Rule.DL3041
+      Hadolint.Rule.DL3042
+      Hadolint.Rule.DL3043
+      Hadolint.Rule.DL3044
+      Hadolint.Rule.DL3045
+      Hadolint.Rule.DL3046
+      Hadolint.Rule.DL3047
+      Hadolint.Rule.DL3048
+      Hadolint.Rule.DL3049
+      Hadolint.Rule.DL3050
+      Hadolint.Rule.DL3051
+      Hadolint.Rule.DL3052
+      Hadolint.Rule.DL3053
+      Hadolint.Rule.DL3054
+      Hadolint.Rule.DL3055
+      Hadolint.Rule.DL3056
+      Hadolint.Rule.DL3057
+      Hadolint.Rule.DL4000
+      Hadolint.Rule.DL4001
+      Hadolint.Rule.DL4003
+      Hadolint.Rule.DL4004
+      Hadolint.Rule.DL4005
+      Hadolint.Rule.DL4006
+      Hadolint.Rule.Shellcheck
       Hadolint.Shell
   other-modules:
       Paths_hadolint
@@ -49,12 +116,13 @@
       Paths_hadolint
   hs-source-dirs:
       src
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal
   build-depends:
-      HsYAML
+      Cabal
+    , HsYAML
     , ShellCheck >=0.7.1
     , aeson
-    , async
     , base >=4.8 && <5
     , bytestring
     , colourista
@@ -63,13 +131,20 @@
     , deepseq ==1.4.4.*
     , directory >=1.3.0
     , filepath
+    , foldl
     , ilist
     , language-docker >=9.1.3 && <10
     , megaparsec >=9.0.0
     , mtl
+    , network-uri
     , parallel
+    , parsec >=3.1.14
+    , semver
+    , spdx
     , split >=0.2
     , text
+    , time
+    , timerep >=2.0
     , void
   default-language: Haskell2010
 
@@ -79,7 +154,8 @@
       Paths_hadolint
   hs-source-dirs:
       app
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"
+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"
   build-depends:
       base >=4.8 && <5
     , containers
@@ -98,10 +174,12 @@
   main-is: Spec.hs
   other-modules:
       ConfigSpec
+      ShellSpec
       Paths_hadolint
   hs-source-dirs:
       test
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms
+  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
     , HsYAML
@@ -109,6 +187,8 @@
     , aeson
     , base >=4.8 && <5
     , bytestring >=0.10
+    , containers
+    , foldl
     , hadolint
     , hspec
     , language-docker >=9.1.3 && <10
diff --git a/src/Hadolint.hs b/src/Hadolint.hs
--- a/src/Hadolint.hs
+++ b/src/Hadolint.hs
@@ -1,12 +1,44 @@
 module Hadolint
   ( module Hadolint.Lint,
-    module Hadolint.Rules,
+    module Hadolint.Process,
     module Hadolint.Config,
-    Result (..)
+    Result (..),
+    OutputFormat (..),
+    shallSkipErrorStatus,
+    printResults,
   )
 where
 
+import Data.Text (Text)
 import Hadolint.Config
-import Hadolint.Lint
-import Hadolint.Rules
+import qualified Hadolint.Formatter.Checkstyle
+import qualified Hadolint.Formatter.Codacy
+import qualified Hadolint.Formatter.Codeclimate
 import Hadolint.Formatter.Format (Result (..))
+import qualified Hadolint.Formatter.Json
+import qualified Hadolint.Formatter.TTY
+import Hadolint.Lint
+import Hadolint.Process
+import Language.Docker.Parser (DockerfileError)
+
+data OutputFormat
+  = Json
+  | TTY
+  | CodeclimateJson
+  | GitlabCodeclimateJson
+  | Checkstyle
+  | Codacy
+  deriving (Show, Eq)
+
+shallSkipErrorStatus :: OutputFormat -> Bool
+shallSkipErrorStatus format = format `elem` [CodeclimateJson, Codacy]
+
+printResults :: Foldable f => OutputFormat -> Bool -> f (Result Text DockerfileError) -> IO ()
+printResults format nocolor allResults =
+  case format of
+    TTY -> Hadolint.Formatter.TTY.printResults allResults nocolor
+    Json -> Hadolint.Formatter.Json.printResults allResults
+    Checkstyle -> Hadolint.Formatter.Checkstyle.printResults allResults
+    CodeclimateJson -> Hadolint.Formatter.Codeclimate.printResults allResults
+    GitlabCodeclimateJson -> Hadolint.Formatter.Codeclimate.printGitlabResults allResults
+    Codacy -> Hadolint.Formatter.Codacy.printResults allResults
diff --git a/src/Hadolint/Config.hs b/src/Hadolint/Config.hs
--- a/src/Hadolint/Config.hs
+++ b/src/Hadolint/Config.hs
@@ -1,9 +1,12 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Hadolint.Config (applyConfig, ConfigFile (..), OverrideConfig (..)) where
+module Hadolint.Config
+  ( applyConfig,
+    ConfigFile (..),
+    OverrideConfig (..),
+  )
+where
 
+import Control.Applicative ((<|>))
+import qualified Control.Foldl.Text as Text
 import Control.Monad (filterM)
 import qualified Data.ByteString as Bytes
 import Data.Coerce (coerce)
@@ -13,7 +16,8 @@
 import qualified Data.YAML as Yaml
 import GHC.Generics (Generic)
 import qualified Hadolint.Lint as Lint
-import qualified Hadolint.Rules as Rules
+import qualified Hadolint.Process as Process
+import qualified Hadolint.Rule as Rule
 import qualified Language.Docker as Docker
 import System.Directory
   ( XdgDirectory (..),
@@ -23,7 +27,6 @@
   )
 import System.FilePath ((</>))
 
-
 data OverrideConfig = OverrideConfig
   { overrideErrorRules :: Maybe [Lint.ErrorRule],
     overrideWarningRules :: Maybe [Lint.WarningRule],
@@ -32,27 +35,39 @@
   }
   deriving (Show, Eq, Generic)
 
+instance Semigroup OverrideConfig where
+  OverrideConfig a1 a2 a3 a4 <> OverrideConfig b1 b2 b3 b4 =
+    OverrideConfig (a1 <> b1) (a2 <> b2) (a3 <> b3) (a4 <> b4)
+
+instance Monoid OverrideConfig where
+  mempty = OverrideConfig Nothing Nothing Nothing Nothing
+
 data ConfigFile = ConfigFile
   { overrideRules :: Maybe OverrideConfig,
     ignoredRules :: Maybe [Lint.IgnoreRule],
-    trustedRegistries :: Maybe [Lint.TrustedRegistry]
+    trustedRegistries :: Maybe [Lint.TrustedRegistry],
+    labelSchemaConfig :: Maybe Rule.LabelSchema,
+    strictLabelSchema :: Maybe Bool
   }
   deriving (Show, Eq, Generic)
 
-instance Yaml.FromYAML OverrideConfig
-  where
-    parseYAML = Yaml.withMap "OverrideConfig" $ \m -> OverrideConfig
-        <$> m .:? "error"
-        <*> m .:? "warning"
-        <*> m .:? "info"
-        <*> m .:? "style"
+instance Yaml.FromYAML OverrideConfig where
+  parseYAML = Yaml.withMap "OverrideConfig" $ \m ->
+    OverrideConfig
+      <$> m .:? "error"
+      <*> m .:? "warning"
+      <*> m .:? "info"
+      <*> m .:? "style"
 
 instance Yaml.FromYAML ConfigFile where
-  parseYAML = Yaml.withMap "ConfigFile" $ \m ->
-    ConfigFile
-      <$> m .:? "override"
-      <*> m .:? "ignored"
-      <*> m .:? "trustedRegistries"
+  parseYAML = Yaml.withMap "ConfigFile" $ \m -> do
+    overrideRules <- m .:? "override"
+    ignored <- m .:? "ignored"
+    let ignoredRules = coerce (ignored :: Maybe [Text.Text])
+    trustedRegistries <- m .:? "trustedRegistries"
+    labelSchemaConfig <- m .:? "label-schema"
+    strictLabelSchema <- m .:? "strict-labels"
+    return ConfigFile {..}
 
 -- | If both the ignoreRules and rulesConfig properties of Lint options are empty
 -- then this function will fill them with the default found in the passed config
@@ -60,7 +75,7 @@
 -- return the error string.
 applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions)
 applyConfig maybeConfig o
-  | not (null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)
+  | not (Prelude.null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)
   | otherwise = do
     theConfig <-
       case maybeConfig of
@@ -78,59 +93,45 @@
     parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)
     parseAndApply configFile = do
       contents <- Bytes.readFile configFile
-      case Yaml.decode1Strict contents of
-        Left (_, err) -> return $ Left (formatError err configFile)
-        Right (ConfigFile Nothing ignore trusted) ->
-          return $
-            Right (applyOverride Nothing Nothing Nothing Nothing ignore trusted)
-        Right (ConfigFile (Just (OverrideConfig errors warnings infos styles)) ignore trusted) ->
-          return $
-            Right (applyOverride errors warnings infos styles ignore trusted)
-
-    applyOverride errors warnings infos styles ignore trusted =
-      applyTrusted trusted
-        . applyIgnore ignore
-        . applyStyles styles
-        . applyInfos infos
-        . applyWarnings warnings
-        . applyErrors errors
-        $ o
-
-    applyErrors errors opts =
-      case Lint.errorRules opts of
-        [] -> opts {Lint.errorRules = fromMaybe [] errors}
-        _ -> opts
-
-    applyWarnings warnings opts =
-      case Lint.warningRules opts of
-        [] -> opts {Lint.warningRules = fromMaybe [] warnings}
-        _ -> opts
+      return $ case Yaml.decode1Strict contents of
+        Left (_, err) -> Left (formatError err configFile)
+        Right config -> Right $ fromMaybe o (applyOverride config)
 
-    applyInfos infos opts =
-      case Lint.infoRules opts of
-        [] -> opts {Lint.infoRules = fromMaybe [] infos}
-        _ -> opts
+    applyOverride ConfigFile {..} =
+      -- Maybe.do
+      do
+        OverrideConfig {..} <- overrideRules <|> Just mempty
+        overrideError <- overrideErrorRules <|> Just mempty
+        overrideWarning <- overrideWarningRules <|> Just mempty
+        overrideInfo <- overrideInfoRules <|> Just mempty
+        overrideStyle <- overrideStyleRules <|> Just mempty
+        overrideIgnored <- ignoredRules <|> Just mempty
 
-    applyStyles styles opts =
-      case Lint.styleRules opts of
-        [] -> opts {Lint.styleRules = fromMaybe [] styles}
-        _ -> opts
+        trusted <- Set.fromList . coerce <$> (trustedRegistries <|> Just mempty)
+        schema <- labelSchemaConfig <|> Just mempty
+        strictLabels <- strictLabelSchema <|> Just False
 
-    applyIgnore ignore opts =
-      case Lint.ignoreRules opts of
-        [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}
-        _ -> opts
+        let rulesConfig = Lint.rulesConfig o
 
-    applyTrusted trusted opts
-      | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =
-        opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}
-      | otherwise = opts
+        return $
+          Lint.LintOptions
+            { Lint.errorRules = Lint.errorRules o <|> overrideError,
+              Lint.warningRules = Lint.warningRules o <|> overrideWarning,
+              Lint.infoRules = Lint.infoRules o <|> overrideInfo,
+              Lint.styleRules = Lint.styleRules o <|> overrideStyle,
+              Lint.ignoreRules = Lint.ignoreRules o <|> overrideIgnored,
+              Lint.rulesConfig =
+                Process.RulesConfig
+                  { Process.allowedRegistries = Process.allowedRegistries rulesConfig `ifNull` trusted,
+                    Process.labelSchema = Process.labelSchema rulesConfig `ifNull` schema,
+                    Process.strictLabels = Process.strictLabels rulesConfig || strictLabels
+                  }
+            }
 
-    toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)
-    toRules _ = mempty
+    ifNull value override = if null value then override else value
 
     formatError err config =
-      unlines
+      Prelude.unlines
         [ "Error parsing your config file in  '" ++ config ++ "':",
           "It should contain one of the keys 'override', 'ignored'",
           "or 'trustedRegistries'. For example:\n",
diff --git a/src/Hadolint/Formatter/Checkstyle.hs b/src/Hadolint/Formatter/Checkstyle.hs
--- a/src/Hadolint/Formatter/Checkstyle.hs
+++ b/src/Hadolint/Formatter/Checkstyle.hs
@@ -1,97 +1,111 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
 module Hadolint.Formatter.Checkstyle
-  ( printResult,
+  ( printResults,
     formatResult,
   )
 where
 
+import qualified Control.Foldl as Foldl
 import qualified Data.ByteString.Builder as Builder
 import qualified Data.ByteString.Lazy.Char8 as B
-import Data.Char
-import Data.Foldable (toList)
-import Data.List (groupBy)
+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, ord)
 import qualified Data.Text as Text
+import Data.Text.Encoding (encodeUtf8Builder)
 import Hadolint.Formatter.Format
-import Hadolint.Rules (Metadata (..), RuleCheck (..), DLSeverity (..))
+  ( Result (..),
+    errorBundlePretty,
+    errorPosition,
+    severityText,
+  )
+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
+import System.IO (stdout)
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
-import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
+  ( ParseErrorBundle,
+    ShowErrorComponent,
+  )
+import Text.Megaparsec.Pos (sourceColumn, sourceLine, unPos)
 import Text.Megaparsec.Stream (VisualStream)
 
 data CheckStyle = CheckStyle
-  { file :: String,
-    line :: Int,
+  { line :: Int,
     column :: Int,
-    impact :: String,
-    msg :: String,
-    source :: String
+    impact :: Text.Text,
+    msg :: Text.Text,
+    source :: Text.Text
   }
 
 errorToCheckStyle :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle
 errorToCheckStyle err =
   CheckStyle
-    { file = sourceName pos,
-      line = unPos (sourceLine pos),
+    { line = unPos (sourceLine pos),
       column = unPos (sourceColumn pos),
       impact = severityText DLErrorC,
-      msg = errorBundlePretty err,
+      msg = Text.pack (errorBundlePretty err),
       source = "DL1000"
     }
   where
     pos = errorPosition err
 
-ruleToCheckStyle :: RuleCheck -> CheckStyle
-ruleToCheckStyle RuleCheck {..} =
+ruleToCheckStyle :: CheckFailure -> CheckStyle
+ruleToCheckStyle CheckFailure {..} =
   CheckStyle
-    { file = Text.unpack filename,
-      line = linenumber,
+    { line = line,
       column = 1,
-      impact = severityText (severity metadata),
-      msg = Text.unpack (message metadata),
-      source = Text.unpack (code metadata)
+      impact = severityText severity,
+      msg = message,
+      source = unRuleCode code
     }
 
-toXml :: [CheckStyle] -> Builder.Builder
-toXml checks = wrap fileName (foldMap convert checks)
-  where
-    wrap name innerNode = "<file " <> attr "name" name <> ">" <> innerNode <> "</file>"
-    convert CheckStyle {..} =
-      "<error "
-        <> attr "line" (show line) -- Beging the node construction
-        <> attr "column" (show column)
-        <> attr "severity" impact
-        <> attr "message" msg
-        <> attr "source" source
-        <> "/>"
-    fileName =
-      case checks of
-        [] -> ""
-        h : _ -> file h
+toXml :: CheckStyle -> Builder.Builder
+toXml CheckStyle {..} =
+  "<error "
+    <> attr "line" (Builder.intDec line)
+    <> attr "column" (Builder.intDec column)
+    <> attr "severity" (encode impact)
+    <> attr "message" (encode msg)
+    <> attr "source" (encode source)
+    <> "/>"
 
-attr :: String -> String -> Builder.Builder
-attr name value = Builder.string8 name <> "='" <> Builder.string8 (escape value) <> "' "
+encode :: Text.Text -> Builder.Builder
+encode = encodeUtf8Builder . escape
 
-escape :: String -> String
-escape = concatMap doEscape
+attr :: Text.Text -> Builder.Builder -> Builder.Builder
+attr name value = encodeUtf8Builder name <> "='" <> value <> "' "
+
+escape :: Text.Text -> Text.Text
+escape = Text.concatMap doEscape
   where
     doEscape c =
       if isOk c
-        then [c]
-        else "&#" ++ show (ord c) ++ ";"
+        then Text.singleton c
+        else "&#" <> Text.pack (show (ord c)) <> ";"
     isOk x = any (\check -> check x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])]
 
 formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Builder.Builder
-formatResult (Result errors checks) =
-  "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"
+formatResult (Result filename errors checks) = header <> xmlBody <> footer
   where
-    xmlBody = mconcat xmlChunks
-    xmlChunks = fmap toXml (groupBy sameFileName flatten)
-    flatten = toList $ checkstyleErrors <> checkstyleChecks
+    xmlBody = Foldl.fold (Foldl.premap toXml Foldl.mconcat) issues
+
+    issues = checkstyleErrors <> checkstyleChecks
     checkstyleErrors = fmap errorToCheckStyle errors
     checkstyleChecks = fmap ruleToCheckStyle checks
-    sameFileName CheckStyle {file = f1} CheckStyle {file = f2} = f1 == f2
 
-printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
-printResult result = B.putStr (Builder.toLazyByteString (formatResult result))
+    isEmpty = null checks && null errors
+    header =
+      if isEmpty
+        then ""
+        else "<file " <> attr "name" (encode filename) <> ">"
+    footer = if isEmpty then "" else "</file>"
+
+printResults ::
+  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  f (Result s e) ->
+  IO ()
+printResults results = do
+  B.putStr header
+  mapM_ put results
+  B.putStr footer
+  where
+    header = "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>"
+    footer = "</checkstyle>"
+    put result = Builder.hPutBuilder stdout (formatResult result)
diff --git a/src/Hadolint/Formatter/Codacy.hs b/src/Hadolint/Formatter/Codacy.hs
--- a/src/Hadolint/Formatter/Codacy.hs
+++ b/src/Hadolint/Formatter/Codacy.hs
@@ -1,27 +1,25 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
 module Hadolint.Formatter.Codacy
-  ( printResult,
+  ( printResults,
     formatResult,
   )
 where
 
+import qualified Control.Foldl as Foldl
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy.Char8 as B
 import Data.Sequence (Seq)
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format (Result (..), errorPosition)
-import Hadolint.Rules (Metadata (..), RuleCheck (..))
+import Hadolint.Rule (CheckFailure (..), RuleCode (..))
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceLine, sourceName, unPos)
 import Text.Megaparsec.Stream (VisualStream)
 
 data Issue = Issue
-  { filename :: String,
-    msg :: String,
-    patternId :: String,
+  { filename :: Text.Text,
+    msg :: Text.Text,
+    patternId :: Text.Text,
     line :: Int
   }
 
@@ -32,32 +30,36 @@
 errorToIssue :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue
 errorToIssue err =
   Issue
-    { filename = sourceName pos,
+    { filename = Text.pack $ sourceName pos,
       patternId = "DL1000",
-      msg = errorBundlePretty err,
+      msg = Text.pack $ errorBundlePretty err,
       line = linenumber
     }
   where
     pos = errorPosition err
     linenumber = unPos (sourceLine pos)
 
-checkToIssue :: RuleCheck -> Issue
-checkToIssue RuleCheck {..} =
+checkToIssue :: Text.Text -> CheckFailure -> Issue
+checkToIssue filename CheckFailure {..} =
   Issue
-    { filename = Text.unpack filename,
-      patternId = Text.unpack (code metadata),
-      msg = Text.unpack (message metadata),
-      line = linenumber
+    { filename = filename,
+      patternId = unRuleCode code,
+      msg = message,
+      line = line
     }
 
 formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue
-formatResult (Result errors checks) = allIssues
+formatResult (Result filename errors checks) = allIssues
   where
     allIssues = errorMessages <> checkMessages
     errorMessages = fmap errorToIssue errors
-    checkMessages = fmap checkToIssue checks
+    checkMessages = fmap (checkToIssue filename) checks
 
-printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
-printResult result = mapM_ output (formatResult result)
+printResults ::
+  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  f (Result s e) ->
+  IO ()
+printResults results = mapM_ output flattened
   where
+    flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results
     output value = B.putStrLn (encode value)
diff --git a/src/Hadolint/Formatter/Codeclimate.hs b/src/Hadolint/Formatter/Codeclimate.hs
--- a/src/Hadolint/Formatter/Codeclimate.hs
+++ b/src/Hadolint/Formatter/Codeclimate.hs
@@ -1,14 +1,12 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
 module Hadolint.Formatter.Codeclimate
-  ( printResult,
-    printGitlabResult,
+  ( printResults,
+    printGitlabResults,
     formatResult,
+    formatGitlabResult,
   )
 where
 
+import qualified Control.Foldl as Foldl
 import Crypto.Hash (Digest, SHA1 (..), hash)
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy as B
@@ -16,17 +14,17 @@
 import qualified Data.Text as Text
 import GHC.Generics
 import Hadolint.Formatter.Format (Result (..), errorPosition)
-import Hadolint.Rules (Metadata (..), RuleCheck (..), DLSeverity (..))
+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
 import Text.Megaparsec.Stream (VisualStream)
 
 data Issue = Issue
-  { checkName :: String,
-    description :: String,
+  { checkName :: Text.Text,
+    description :: Text.Text,
     location :: Location,
-    impact :: String
+    impact :: Text.Text
   }
 
 data FingerprintIssue = FingerprintIssue
@@ -36,10 +34,10 @@
 
 data Location
   = LocLine
-      String
+      Text.Text
       Int
   | LocPos
-      String
+      Text.Text
       Pos
 
 instance ToJSON Location where
@@ -58,10 +56,10 @@
 instance ToJSON Issue where
   toJSON Issue {..} =
     object
-      [ "type" .= ("issue" :: String),
+      [ "type" .= ("issue" :: Text.Text),
         "check_name" .= checkName,
         "description" .= description,
-        "categories" .= (["Bug Risk"] :: [String]),
+        "categories" .= (["Bug Risk"] :: [Text.Text]),
         "location" .= location,
         "severity" .= impact
       ]
@@ -69,11 +67,11 @@
 instance ToJSON FingerprintIssue where
   toJSON FingerprintIssue {..} =
     object
-      [ "type" .= ("issue" :: String),
+      [ "type" .= ("issue" :: Text.Text),
         "fingerprint" .= show fingerprint,
         "check_name" .= checkName issue,
         "description" .= description issue,
-        "categories" .= (["Bug Risk"] :: [String]),
+        "categories" .= (["Bug Risk"] :: [Text.Text]),
         "location" .= location issue,
         "severity" .= impact issue
       ]
@@ -82,8 +80,8 @@
 errorToIssue err =
   Issue
     { checkName = "DL1000",
-      description = errorBundlePretty err,
-      location = LocPos (sourceName pos) Pos {..},
+      description = Text.pack $ errorBundlePretty err,
+      location = LocPos (Text.pack $ sourceName pos) Pos {..},
       impact = severityText DLErrorC
     }
   where
@@ -91,16 +89,16 @@
     line = unPos (sourceLine pos)
     column = unPos (sourceColumn pos)
 
-checkToIssue :: RuleCheck -> Issue
-checkToIssue RuleCheck {..} =
+checkToIssue :: Text.Text -> CheckFailure -> Issue
+checkToIssue fileName CheckFailure {..} =
   Issue
-    { checkName = Text.unpack (code metadata),
-      description = Text.unpack (message metadata),
-      location = LocLine (Text.unpack filename) linenumber,
-      impact = severityText (severity metadata)
+    { checkName = unRuleCode code,
+      description = message,
+      location = LocLine fileName line,
+      impact = severityText severity
     }
 
-severityText :: DLSeverity -> String
+severityText :: DLSeverity -> Text.Text
 severityText severity =
   case severity of
     DLErrorC -> "blocker"
@@ -120,7 +118,7 @@
     }
 
 formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue
-formatResult (Result errors checks) = (errorToIssue <$> errors) <> (checkToIssue <$> checks)
+formatResult (Result filename errors checks) = (errorToIssue <$> errors) <> (checkToIssue filename <$> checks)
 
 formatGitlabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq FingerprintIssue
 formatGitlabResult result = issueToFingerprintIssue <$> formatResult result
@@ -132,5 +130,10 @@
       B.putStr (encode value)
       B.putStr (B.singleton 0x00)
 
-printGitlabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
-printGitlabResult = B.putStr . encode . formatGitlabResult
+printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> IO ()
+printResults = mapM_ printResult
+
+printGitlabResults :: (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) => f (Result s e) -> IO ()
+printGitlabResults results = B.putStr . encode $ flattened
+  where
+    flattened = Foldl.fold (Foldl.premap formatGitlabResult Foldl.mconcat) results
diff --git a/src/Hadolint/Formatter/Format.hs b/src/Hadolint/Formatter/Format.hs
--- a/src/Hadolint/Formatter/Format.hs
+++ b/src/Hadolint/Formatter/Format.hs
@@ -10,41 +10,35 @@
   )
 where
 
-import Data.List (sort)
 import qualified Data.List.NonEmpty as NE
 import Data.Sequence (Seq)
 import qualified Data.Sequence as Seq
-import Hadolint.Rules
+import qualified Data.Text as Text
+import qualified Hadolint.Rule
 import Text.Megaparsec (TraversableStream (..), pstateSourcePos)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (SourcePos, sourcePosPretty)
 import Text.Megaparsec.Stream (VisualStream)
 
 data Result s e = Result
-  { errors :: !(Seq (ParseErrorBundle s e)),
-    checks :: !(Seq RuleCheck)
+  { fileName :: Text.Text,
+    errors :: Seq (ParseErrorBundle s e),
+    checks :: Hadolint.Rule.Failures
   }
 
-instance Semigroup (Result s e) where
-  (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)
-
-instance Monoid (Result s e) where
-  mappend = (<>)
-  mempty = Result mempty mempty
-
-toResult :: Either (ParseErrorBundle s e) [RuleCheck] -> Result s e
-toResult res =
+toResult :: Text.Text -> Either (ParseErrorBundle s e) Hadolint.Rule.Failures -> Result s e
+toResult file res =
   case res of
-    Left err -> Result (Seq.singleton err) mempty
-    Right c -> Result mempty (Seq.fromList (sort c))
+    Left err -> Result file (Seq.singleton err) mempty
+    Right c -> Result file mempty (Seq.unstableSort c)
 
-severityText :: DLSeverity -> String
+severityText :: Hadolint.Rule.DLSeverity -> Text.Text
 severityText s =
   case s of
-    DLErrorC -> "error"
-    DLWarningC -> "warning"
-    DLInfoC -> "info"
-    DLStyleC -> "style"
+    Hadolint.Rule.DLErrorC -> "error"
+    Hadolint.Rule.DLWarningC -> "warning"
+    Hadolint.Rule.DLInfoC -> "info"
+    Hadolint.Rule.DLStyleC -> "style"
     _ -> ""
 
 stripNewlines :: String -> String
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
@@ -1,34 +1,32 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
 module Hadolint.Formatter.Json
-  ( printResult,
+  ( printResults,
     formatResult,
   )
 where
 
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy.Char8 as B
+import qualified Data.Text as Text
 import Hadolint.Formatter.Format (Result (..), errorPosition, severityText)
-import Hadolint.Rules (Metadata (..), RuleCheck (..), DLSeverity (..))
+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), unRuleCode)
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
 import Text.Megaparsec.Stream (VisualStream)
 
 data JsonFormat s e
-  = JsonCheck RuleCheck
+  = JsonCheck Text.Text CheckFailure
   | JsonParseError (ParseErrorBundle s e)
 
 instance (VisualStream s, TraversableStream s, ShowErrorComponent e) => ToJSON (JsonFormat s e) where
-  toJSON (JsonCheck RuleCheck {..}) =
+  toJSON (JsonCheck filename CheckFailure {..}) =
     object
       [ "file" .= filename,
-        "line" .= linenumber,
+        "line" .= line,
         "column" .= (1 :: Int),
-        "level" .= severityText (severity metadata),
-        "code" .= code metadata,
-        "message" .= message metadata
+        "level" .= severityText severity,
+        "code" .= unRuleCode code,
+        "message" .= message
       ]
   toJSON (JsonParseError err) =
     object
@@ -36,18 +34,27 @@
         "line" .= unPos (sourceLine pos),
         "column" .= unPos (sourceColumn pos),
         "level" .= severityText DLErrorC,
-        "code" .= ("DL1000" :: String),
+        "code" .= ("DL1000" :: Text.Text),
         "message" .= errorBundlePretty err
       ]
     where
       pos = errorPosition err
 
+printResults ::
+  (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>
+  f (Result s e) ->
+  IO ()
+printResults = mapM_ printResult
+
 formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Value
-formatResult (Result errors checks) = toJSON allMessages
+formatResult (Result fileName errors checks) = toJSON allMessages
   where
     allMessages = errorMessages <> checkMessages
     errorMessages = fmap JsonParseError errors
-    checkMessages = fmap JsonCheck checks
+    checkMessages = fmap (JsonCheck fileName) checks
 
-printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
+printResult ::
+  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  Result s e ->
+  IO ()
 printResult result = B.putStrLn (encode (formatResult result))
diff --git a/src/Hadolint/Formatter/TTY.hs b/src/Hadolint/Formatter/TTY.hs
--- a/src/Hadolint/Formatter/TTY.hs
+++ b/src/Hadolint/Formatter/TTY.hs
@@ -2,53 +2,56 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 module Hadolint.Formatter.TTY
-  ( printResult,
+  ( printResults,
+    formatCheck,
     formatError,
-    formatChecks,
   )
 where
 
 import Colourista
+import qualified Control.Foldl as Foldl
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format
-import Hadolint.Rules
+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
 import Language.Docker.Syntax
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Stream (VisualStream)
 
-formatErrors :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Functor f) => f (ParseErrorBundle s e) -> f String
-formatErrors = fmap formatError
-
 formatError :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> String
 formatError err = stripNewlines (errorMessageLine err)
 
-formatChecks :: Functor f => f RuleCheck -> Bool -> f Text.Text
-formatChecks rc nocolor = fmap formatCheck rc
-  where
-    formatCheck (RuleCheck meta source line _) =
-      formatPos source line
-          <> code meta
-          <> " "
-          <> (if nocolor then Text.pack (severityText (severity meta))
-                         else colorizedSeverity (severity meta))
-          <> ": "
-          <> message meta
+formatCheck :: Bool -> Text.Text -> CheckFailure -> Text.Text
+formatCheck nocolor source CheckFailure {code, severity, line, message} =
+  formatPos source line
+    <> unRuleCode code
+    <> " "
+    <> ( if nocolor
+           then severityText severity
+           else colorizedSeverity severity
+       )
+    <> ": "
+    <> message
 
 formatPos :: Filename -> Linenumber -> Text.Text
 formatPos source line = source <> ":" <> Text.pack (show line) <> " "
 
-printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Bool -> IO ()
-printResult Result {errors, checks} color = printErrors >> printChecks
+printResults ::
+  (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldl.Foldable f) =>
+  f (Result s e) ->
+  Bool ->
+  IO ()
+printResults results color = mapM_ printResult results
   where
-    printErrors = mapM_ putStrLn (formatErrors errors)
-    printChecks = mapM_ (putStrLn . Text.unpack) (formatChecks checks color)
+    printResult Result {fileName, errors, checks} = printErrors errors >> printChecks fileName checks
+    printErrors errors = mapM_ (putStrLn . formatError) errors
+    printChecks fileName checks = mapM_ (putStrLn . Text.unpack . formatCheck color fileName) checks
 
 colorizedSeverity :: DLSeverity -> Text.Text
 colorizedSeverity s =
   case s of
-    DLErrorC -> Text.pack $ formatWith [bold, red] $ severityText s
-    DLWarningC -> Text.pack $ formatWith [bold, yellow] $ severityText s
-    DLInfoC -> Text.pack $ formatWith [green] $ severityText s
-    DLStyleC -> Text.pack $ formatWith [cyan] $ severityText s
-    _ -> Text.pack $ severityText s
+    DLErrorC -> formatWith [bold, red] $ severityText s
+    DLWarningC -> formatWith [bold, yellow] $ severityText s
+    DLInfoC -> formatWith [green] $ severityText s
+    DLStyleC -> formatWith [cyan] $ severityText s
+    _ -> severityText s
diff --git a/src/Hadolint/Ignore.hs b/src/Hadolint/Ignore.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Ignore.hs
@@ -0,0 +1,40 @@
+module Hadolint.Ignore (ignored) where
+
+import qualified Control.Foldl as Foldl
+import qualified Data.IntMap.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import Data.Void (Void)
+import Hadolint.Rule (RuleCode (RuleCode))
+import Language.Docker.Syntax
+import qualified Text.Megaparsec as Megaparsec
+import qualified Text.Megaparsec.Char as Megaparsec
+
+ignored :: Foldl.Fold (InstructionPos Text.Text) (Map.IntMap (Set.Set RuleCode))
+ignored = Foldl.Fold parse mempty id
+  where
+    parse acc InstructionPos {instruction = Comment comment, lineNumber = line} =
+      case parseComment comment of
+        Just ignores@(_ : _) -> Map.insert (line + 1) (Set.fromList . fmap RuleCode $ ignores) acc
+        _ -> acc
+    parse acc _ = acc
+
+    parseComment :: Text.Text -> Maybe [Text.Text]
+    parseComment =
+      Megaparsec.parseMaybe commentParser
+
+    commentParser :: Megaparsec.Parsec Void Text.Text [Text.Text]
+    commentParser =
+      do
+        spaces
+        >> string "hadolint"
+        >> spaces1
+        >> string "ignore="
+        >> spaces
+        >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)
+
+    ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` Set.fromList "DLSC0123456789")
+    string = Megaparsec.string
+    spaces = Megaparsec.takeWhileP Nothing space
+    spaces1 = Megaparsec.takeWhile1P Nothing space
+    space c = c == ' ' || c == '\t'
diff --git a/src/Hadolint/Lint.hs b/src/Hadolint/Lint.hs
--- a/src/Hadolint/Lint.hs
+++ b/src/Hadolint/Lint.hs
@@ -1,30 +1,39 @@
-{-# LANGUAGE NamedFieldPuns #-}
-
-module Hadolint.Lint where
+module Hadolint.Lint
+  ( lintIO,
+    lint,
+    analyze,
+    LintOptions (..),
+    ErrorRule,
+    WarningRule,
+    InfoRule,
+    StyleRule,
+    IgnoreRule,
+    TrustedRegistry,
+  )
+where
 
-import qualified Control.Concurrent.Async as Async
-import Control.Parallel.Strategies (parListChunk, rseq, using)
+import qualified Control.Parallel.Strategies as Parallel
 import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
 import Data.Text (Text)
-import GHC.Conc (numCapabilities)
-import qualified Hadolint.Formatter.Checkstyle as Checkstyle
-import qualified Hadolint.Formatter.Codacy as Codacy
-import qualified Hadolint.Formatter.Codeclimate as Codeclimate
+import qualified Data.Text as Text
 import qualified Hadolint.Formatter.Format as Format
-import qualified Hadolint.Formatter.Json as Json
-import qualified Hadolint.Formatter.TTY as TTY
-import qualified Hadolint.Rules as Rules
+import qualified Hadolint.Process
+import qualified Hadolint.Rule
 import qualified Language.Docker as Docker
 import Language.Docker.Parser (DockerfileError, Error)
 import Language.Docker.Syntax (Dockerfile)
 
-
 type ErrorRule = Text
+
 type WarningRule = Text
+
 type InfoRule = Text
+
 type StyleRule = Text
-type IgnoreRule = Text
 
+type IgnoreRule = Hadolint.Rule.RuleCode
+
 type TrustedRegistry = Text
 
 data LintOptions = LintOptions
@@ -33,85 +42,75 @@
     infoRules :: [InfoRule],
     styleRules :: [StyleRule],
     ignoreRules :: [IgnoreRule],
-    rulesConfig :: Rules.RulesConfig
+    rulesConfig :: Hadolint.Process.RulesConfig
   }
   deriving (Show)
 
-data OutputFormat
-  = Json
-  | TTY
-  | CodeclimateJson
-  | GitlabCodeclimateJson
-  | Checkstyle
-  | Codacy
-  deriving (Show, Eq)
-
-printResults :: OutputFormat -> Bool -> Format.Result Text DockerfileError -> IO ()
-printResults format nocolor allResults =
-  case format of
-    TTY -> TTY.printResult allResults nocolor
-    Json -> Json.printResult allResults
-    Checkstyle -> Checkstyle.printResult allResults
-    CodeclimateJson -> Codeclimate.printResult allResults
-    GitlabCodeclimateJson -> Codeclimate.printGitlabResult allResults
-    Codacy -> Codacy.printResult allResults
+instance Semigroup LintOptions where
+  LintOptions a1 a2 a3 a4 a5 a6 <> LintOptions b1 b2 b3 b4 b5 b6 =
+    LintOptions
+      (a1 <> b1)
+      (a2 <> b2)
+      (a3 <> b3)
+      (a4 <> b4)
+      (a5 <> b5)
+      (a6 <> b6)
 
-shallSkipErrorStatus:: OutputFormat -> Bool
-shallSkipErrorStatus format  = format `elem` [CodeclimateJson, Codacy]
+instance Monoid LintOptions where
+  mempty = LintOptions mempty mempty mempty mempty mempty mempty
 
 -- | Performs the process of parsing the dockerfile and analyzing it with all the applicable
 -- rules, depending on the list of ignored rules.
--- Depending on the preferred printing format, it will output the results to stdout
-lint :: LintOptions -> NonEmpty.NonEmpty String -> IO (Format.Result Text DockerfileError)
-lint
-  LintOptions
-    { errorRules = errorList,
-      warningRules = warningList,
-      infoRules = infoList,
-      styleRules = styleList,
-      ignoreRules = ignoreList,
-      rulesConfig
-    }
-  dFiles = do
-    parsedFiles <- Async.mapConcurrently parseFile (NonEmpty.toList dFiles)
-    let results = lintAll parsedFiles `using` parListChunk (div numCapabilities 2) rseq
-    return $ mconcat results
-    where
-      parseFile :: String -> IO (Either Error Dockerfile)
-      parseFile "-" = Docker.parseStdin
-      parseFile s = Docker.parseFile s
-
-      lintAll = fmap lintDockerfile
+lintIO :: LintOptions -> NonEmpty.NonEmpty FilePath -> IO (NonEmpty.NonEmpty (Format.Result Text DockerfileError))
+lintIO options dFiles = do
+  parsedFiles <- mapM parseFile (NonEmpty.toList dFiles)
+  return $ NonEmpty.fromList (lint options parsedFiles)
+  where
+    parseFile :: String -> IO (Text, Either Error Dockerfile)
+    parseFile "-" = do
+      res <- Docker.parseStdin
+      return (Text.pack "-", res)
+    parseFile s = do
+      res <- Docker.parseFile s
+      return (Text.pack s, res)
 
-      lintDockerfile = processedFile
-        where
-          processedFile = Format.toResult . fmap processRules
-          processRules fileLines =
-            filter ignoredRules $
-              map
-                ( makeSeverity Rules.DLErrorC errorList
-                . makeSeverity Rules.DLWarningC warningList
-                . makeSeverity Rules.DLInfoC infoList
-                . makeSeverity Rules.DLStyleC styleList
-                )
-                $ analyzeAll rulesConfig fileLines
+lint ::
+  LintOptions ->
+  [(Text, Either Error Dockerfile)] ->
+  [Format.Result Text DockerfileError]
+lint options parsedFiles = gather results `Parallel.using` parallelRun
+  where
+    gather = fmap (uncurry Format.toResult)
+    results =
+      [ ( name,
+          fmap (analyze options) parseResult
+        )
+        | (name, parseResult) <- parsedFiles
+      ]
+    parallelRun = Parallel.parList Parallel.rseq
 
-          ignoredRules = ignoreFilter ignoreList
+analyze :: LintOptions -> Dockerfile -> Seq.Seq Hadolint.Rule.CheckFailure
+analyze options dockerfile = fixer process
+  where
+    fixer = fixSeverity options
+    process = Hadolint.Process.run (rulesConfig options) dockerfile
 
-          makeSeverity s rules rule@(Rules.RuleCheck (Rules.Metadata code _ message) filename linenumber success) =
-            if code `elem` rules
-              then Rules.RuleCheck (Rules.Metadata code s message) filename linenumber success
-              else rule
+fixSeverity :: LintOptions -> Seq.Seq Hadolint.Rule.CheckFailure -> Seq.Seq Hadolint.Rule.CheckFailure
+fixSeverity LintOptions {..} = Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)
+  where
+    correctSeverity =
+      makeSeverity Hadolint.Rule.DLErrorC (fmap Hadolint.Rule.RuleCode errorRules)
+        . makeSeverity Hadolint.Rule.DLWarningC (fmap Hadolint.Rule.RuleCode warningRules)
+        . makeSeverity Hadolint.Rule.DLInfoC (fmap Hadolint.Rule.RuleCode infoRules)
+        . makeSeverity Hadolint.Rule.DLStyleC (fmap Hadolint.Rule.RuleCode styleRules)
 
-          ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool
-          ignoreFilter rules (Rules.RuleCheck (Rules.Metadata code severity _) _ _ _) =
-            code `notElem` rules && severity /= Rules.DLIgnoreC
+    ignoredRules = ignoreFilter ignoreRules
 
--- | Returns the result of applying all the rules to the given dockerfile
-analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]
-analyzeAll config = Rules.analyze (Rules.rules ++ Rules.optionalRules config)
+    makeSeverity s rules rule@Hadolint.Rule.CheckFailure {code} =
+      if code `elem` rules
+        then rule {Hadolint.Rule.severity = s}
+        else rule
 
--- | Helper to analyze AST quickly in GHCI
-analyzeEither :: Rules.RulesConfig -> Either t Dockerfile -> [Rules.RuleCheck]
-analyzeEither _ (Left _) = []
-analyzeEither config (Right dockerFile) = analyzeAll config dockerFile
+    ignoreFilter :: [IgnoreRule] -> Hadolint.Rule.CheckFailure -> Bool
+    ignoreFilter ignored Hadolint.Rule.CheckFailure {code, severity} =
+      code `notElem` ignored && severity /= Hadolint.Rule.DLIgnoreC
diff --git a/src/Hadolint/Process.hs b/src/Hadolint/Process.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Process.hs
@@ -0,0 +1,203 @@
+module Hadolint.Process (run, RulesConfig (..)) where
+
+import qualified Control.Foldl as Foldl
+import qualified Data.IntMap.Strict as SMap
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import qualified Hadolint.Ignore
+import Hadolint.Rule (CheckFailure (..), Failures, Rule, RuleCode)
+import qualified Hadolint.Rule as Rule
+import qualified Hadolint.Rule.DL3000
+import qualified Hadolint.Rule.DL3001
+import qualified Hadolint.Rule.DL3002
+import qualified Hadolint.Rule.DL3003
+import qualified Hadolint.Rule.DL3004
+import qualified Hadolint.Rule.DL3005
+import qualified Hadolint.Rule.DL3006
+import qualified Hadolint.Rule.DL3007
+import qualified Hadolint.Rule.DL3008
+import qualified Hadolint.Rule.DL3009
+import qualified Hadolint.Rule.DL3010
+import qualified Hadolint.Rule.DL3011
+import qualified Hadolint.Rule.DL3012
+import qualified Hadolint.Rule.DL3013
+import qualified Hadolint.Rule.DL3014
+import qualified Hadolint.Rule.DL3015
+import qualified Hadolint.Rule.DL3016
+import qualified Hadolint.Rule.DL3017
+import qualified Hadolint.Rule.DL3018
+import qualified Hadolint.Rule.DL3019
+import qualified Hadolint.Rule.DL3020
+import qualified Hadolint.Rule.DL3021
+import qualified Hadolint.Rule.DL3022
+import qualified Hadolint.Rule.DL3023
+import qualified Hadolint.Rule.DL3024
+import qualified Hadolint.Rule.DL3025
+import qualified Hadolint.Rule.DL3026
+import qualified Hadolint.Rule.DL3027
+import qualified Hadolint.Rule.DL3028
+import qualified Hadolint.Rule.DL3029
+import qualified Hadolint.Rule.DL3030
+import qualified Hadolint.Rule.DL3031
+import qualified Hadolint.Rule.DL3032
+import qualified Hadolint.Rule.DL3033
+import qualified Hadolint.Rule.DL3034
+import qualified Hadolint.Rule.DL3035
+import qualified Hadolint.Rule.DL3036
+import qualified Hadolint.Rule.DL3037
+import qualified Hadolint.Rule.DL3038
+import qualified Hadolint.Rule.DL3039
+import qualified Hadolint.Rule.DL3040
+import qualified Hadolint.Rule.DL3041
+import qualified Hadolint.Rule.DL3042
+import qualified Hadolint.Rule.DL3043
+import qualified Hadolint.Rule.DL3044
+import qualified Hadolint.Rule.DL3045
+import qualified Hadolint.Rule.DL3046
+import qualified Hadolint.Rule.DL3047
+import qualified Hadolint.Rule.DL3048
+import qualified Hadolint.Rule.DL3049
+import qualified Hadolint.Rule.DL3050
+import qualified Hadolint.Rule.DL3051
+import qualified Hadolint.Rule.DL3052
+import qualified Hadolint.Rule.DL3053
+import qualified Hadolint.Rule.DL3054
+import qualified Hadolint.Rule.DL3055
+import qualified Hadolint.Rule.DL3056
+import qualified Hadolint.Rule.DL3057
+import qualified Hadolint.Rule.DL4000
+import qualified Hadolint.Rule.DL4001
+import qualified Hadolint.Rule.DL4003
+import qualified Hadolint.Rule.DL4004
+import qualified Hadolint.Rule.DL4005
+import qualified Hadolint.Rule.DL4006
+import qualified Hadolint.Rule.Shellcheck
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+
+-- | Contains the required parameters for optional rules
+data RulesConfig = RulesConfig
+  { -- | The docker registries that are allowed in FROM
+    allowedRegistries :: Set.Set Registry,
+    labelSchema :: Rule.LabelSchema,
+    strictLabels :: Bool
+  }
+  deriving (Show, Eq)
+
+instance Semigroup RulesConfig where
+  RulesConfig a1 a2 a3 <> RulesConfig b1 b2 b3 =
+    RulesConfig
+      (a1 <> b1)
+      (a2 <> b2)
+      (a3 || b3)
+
+instance Monoid RulesConfig where
+  mempty = RulesConfig mempty mempty False
+
+data AnalisisResult = AnalisisResult
+  { -- | The set of ignored rules per line
+    ignored :: SMap.IntMap (Set.Set RuleCode),
+    -- | A set of failures collected for reach rule
+    failed :: Failures
+  }
+
+run :: RulesConfig -> [InstructionPos Text.Text] -> Failures
+run config dockerfile = Seq.filter shouldKeep failed
+  where
+    AnalisisResult {..} = Foldl.fold (analyze config) dockerfile
+
+    shouldKeep CheckFailure {line, code} =
+      Just True /= do
+        ignoreList <- SMap.lookup line ignored
+        return $ code `Set.member` ignoreList
+
+analyze :: RulesConfig -> Foldl.Fold (InstructionPos Text.Text) AnalisisResult
+analyze config =
+  AnalisisResult
+    <$> Hadolint.Ignore.ignored
+    <*> Foldl.premap parseShell (failures config <> onBuildFailures config)
+
+parseShell :: InstructionPos Text.Text -> InstructionPos Shell.ParsedShell
+parseShell = fmap Shell.parseShell
+
+onBuildFailures :: RulesConfig -> Rule Shell.ParsedShell
+onBuildFailures config =
+  Foldl.prefilter
+    isOnBuild
+    (Foldl.premap unwrapOnbuild (failures config))
+  where
+    isOnBuild InstructionPos {instruction = OnBuild {}} = True
+    isOnBuild _ = False
+
+    unwrapOnbuild inst@InstructionPos {instruction = OnBuild i} = inst {instruction = i}
+    unwrapOnbuild inst = inst
+
+failures :: RulesConfig -> Rule Shell.ParsedShell
+failures RulesConfig {allowedRegistries, labelSchema, strictLabels} =
+  Hadolint.Rule.DL3000.rule
+    <> Hadolint.Rule.DL3001.rule
+    <> Hadolint.Rule.DL3002.rule
+    <> Hadolint.Rule.DL3003.rule
+    <> Hadolint.Rule.DL3004.rule
+    <> Hadolint.Rule.DL3005.rule
+    <> Hadolint.Rule.DL3006.rule
+    <> Hadolint.Rule.DL3007.rule
+    <> Hadolint.Rule.DL3008.rule
+    <> Hadolint.Rule.DL3009.rule
+    <> Hadolint.Rule.DL3010.rule
+    <> Hadolint.Rule.DL3011.rule
+    <> Hadolint.Rule.DL3012.rule
+    <> Hadolint.Rule.DL3013.rule
+    <> Hadolint.Rule.DL3014.rule
+    <> Hadolint.Rule.DL3015.rule
+    <> Hadolint.Rule.DL3016.rule
+    <> Hadolint.Rule.DL3017.rule
+    <> Hadolint.Rule.DL3018.rule
+    <> Hadolint.Rule.DL3019.rule
+    <> Hadolint.Rule.DL3020.rule
+    <> Hadolint.Rule.DL3021.rule
+    <> Hadolint.Rule.DL3022.rule
+    <> Hadolint.Rule.DL3023.rule
+    <> Hadolint.Rule.DL3024.rule
+    <> Hadolint.Rule.DL3025.rule
+    <> Hadolint.Rule.DL3026.rule allowedRegistries
+    <> Hadolint.Rule.DL3027.rule
+    <> Hadolint.Rule.DL3028.rule
+    <> Hadolint.Rule.DL3029.rule
+    <> Hadolint.Rule.DL3030.rule
+    <> Hadolint.Rule.DL3031.rule
+    <> Hadolint.Rule.DL3032.rule
+    <> Hadolint.Rule.DL3033.rule
+    <> Hadolint.Rule.DL3034.rule
+    <> Hadolint.Rule.DL3035.rule
+    <> Hadolint.Rule.DL3036.rule
+    <> Hadolint.Rule.DL3037.rule
+    <> Hadolint.Rule.DL3038.rule
+    <> Hadolint.Rule.DL3039.rule
+    <> Hadolint.Rule.DL3040.rule
+    <> Hadolint.Rule.DL3041.rule
+    <> Hadolint.Rule.DL3042.rule
+    <> Hadolint.Rule.DL3043.rule
+    <> Hadolint.Rule.DL3044.rule
+    <> Hadolint.Rule.DL3045.rule
+    <> Hadolint.Rule.DL3046.rule
+    <> Hadolint.Rule.DL3047.rule
+    <> Hadolint.Rule.DL3048.rule
+    <> Hadolint.Rule.DL3049.rule labelSchema
+    <> Hadolint.Rule.DL3050.rule labelSchema strictLabels
+    <> Hadolint.Rule.DL3051.rule labelSchema
+    <> Hadolint.Rule.DL3052.rule labelSchema
+    <> Hadolint.Rule.DL3053.rule labelSchema
+    <> Hadolint.Rule.DL3054.rule labelSchema
+    <> Hadolint.Rule.DL3055.rule labelSchema
+    <> Hadolint.Rule.DL3056.rule labelSchema
+    <> Hadolint.Rule.DL3057.rule
+    <> Hadolint.Rule.DL4000.rule
+    <> Hadolint.Rule.DL4001.rule
+    <> Hadolint.Rule.DL4003.rule
+    <> Hadolint.Rule.DL4004.rule
+    <> Hadolint.Rule.DL4005.rule
+    <> Hadolint.Rule.DL4006.rule
+    <> Hadolint.Rule.Shellcheck.rule
diff --git a/src/Hadolint/Rule.hs b/src/Hadolint/Rule.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule.hs
@@ -0,0 +1,172 @@
+module Hadolint.Rule where
+
+import Control.DeepSeq (NFData)
+import qualified Control.Foldl as Foldl
+import qualified Data.Map as Map
+import qualified Data.Sequence as Seq
+import Data.String (IsString (..))
+import qualified Data.Text as Text
+import qualified Data.YAML as Yaml
+import GHC.Generics (Generic)
+import Language.Docker.Syntax
+
+infixl 0 |>
+
+(|>) :: a -> (a -> b) -> b
+x |> f = f x
+
+data DLSeverity
+  = DLErrorC
+  | DLWarningC
+  | DLInfoC
+  | DLStyleC
+  | DLIgnoreC
+  deriving (Show, Eq, Ord, Generic, NFData)
+
+newtype RuleCode = RuleCode {unRuleCode :: Text.Text}
+  deriving (Show, Eq, Ord)
+
+instance IsString RuleCode where
+  fromString = RuleCode . Text.pack
+
+data CheckFailure = CheckFailure
+  { code :: RuleCode,
+    severity :: DLSeverity,
+    message :: Text.Text,
+    line :: Linenumber
+  }
+  deriving (Show, Eq)
+
+instance Ord CheckFailure where
+  a `compare` b = line a `compare` line b
+
+type Failures = Seq.Seq CheckFailure
+
+data State a = State
+  { failures :: Failures,
+    state :: a
+  }
+  deriving (Show)
+
+type LabelName = Text.Text
+
+data LabelType
+  = RawText
+  | Url
+  | Spdx
+  | GitHash
+  | Rfc3339
+  | SemVer
+  deriving (Eq, Read, Show)
+
+read :: Text.Text -> Either Text.Text LabelType
+read "url"     = Right Url
+read "spdx"    = Right Spdx
+read "hash"    = Right GitHash
+read "rfc3339" = Right Rfc3339
+read "semver"  = Right SemVer
+read "text"    = Right RawText
+read ""        = Right RawText
+read t         = Left ("Invalid label type: " <> t)
+
+instance Yaml.FromYAML LabelType where
+  parseYAML = withLabelType pure
+
+withLabelType :: (LabelType -> Yaml.Parser a) -> Yaml.Node Yaml.Pos -> Yaml.Parser a
+withLabelType f v@(Yaml.Scalar _ (Yaml.SStr b)) =
+    case Hadolint.Rule.read b of
+      Right lt -> f lt
+      Left _ -> Yaml.typeMismatch "labeltype" v
+withLabelType _ v = Yaml.typeMismatch "labeltype" v
+
+type LabelSchema = Map.Map LabelName LabelType
+
+
+withLineNumber ::
+  (Linenumber -> t1 -> Instruction args -> t2) ->
+  t1 ->
+  InstructionPos args ->
+  t2
+withLineNumber f state InstructionPos {instruction, lineNumber} =
+  f lineNumber state instruction
+
+addFail :: CheckFailure -> State a -> State a
+addFail failure state@(State fails _) =
+  state
+    { failures =
+        fails
+          Seq.|> failure
+    }
+
+emptyState :: a -> State a
+emptyState = State Seq.empty
+
+simpleState :: State ()
+simpleState = State Seq.empty ()
+
+modify :: (a -> a) -> State a -> State a
+modify f s@(State _ st) = s {state = f st}
+
+replaceWith :: a -> State a -> State a
+replaceWith newState s = s {state = newState}
+
+type Rule args = Foldl.Fold (InstructionPos args) Failures
+
+-- | A simple rule that can be implemented in terms of returning True or False for each instruction
+-- If you need to calculate some state to decide upon past information, use 'customRule'
+simpleRule ::
+  -- | rule code
+  RuleCode ->
+  -- | severity for the rule
+  DLSeverity ->
+  -- | failure message for the rule
+  Text.Text ->
+  -- | step calculation for the rule. Returns True or False for each line in the dockerfile depending on its validity.
+  (Instruction args -> Bool) ->
+  Rule args
+simpleRule code severity message checker = customRule step simpleState
+  where
+    step line s instr
+      | checker instr = s
+      | otherwise = s |> addFail (CheckFailure code severity message line)
+
+-- | A rule that accumulates a State a. The state contains the collection of failed lines and a custom data
+-- type that can be used to track properties for the rule. Each step always returns the new State, which offers
+-- the ability to both accumulate properties and mark failures for every given instruction.
+customRule ::
+  (Linenumber -> State a -> Instruction args -> State a) ->
+  State a ->
+  Rule args
+customRule step initial = veryCustomRule step initial failures
+
+-- | Similarly to 'customRule', it returns a State a for each step, but it has the ability to run a
+-- done callback as the last step of the rule. The done callback can be used to transform the state
+-- and mark failures for any arbitrary line in the input. This helper is meant for rules that need
+-- to do lookahead. Instead of looking ahead, the state should store the facts and make a decision about
+-- them once the input is finished.
+veryCustomRule ::
+  -- | step calculation for the rule. Called for each instruction in the docker file
+  -- it must return the state after being modified by the rule
+  (Linenumber -> State a -> Instruction args -> State a) ->
+  -- | initial state
+  State a ->
+  -- | done callaback. It is passed the final accumulated state and it should return all failures
+  -- found by the rule
+  (State a -> Failures) ->
+  Rule args
+veryCustomRule step = Foldl.Fold (withLineNumber step)
+
+foldArguments :: (a -> b) -> Arguments a -> b
+foldArguments applyRule args =
+  case args of
+    ArgumentsText as -> applyRule as
+    ArgumentsList as -> applyRule as
+
+-- | Returns the result of running the check function on the image alias
+--   name, if the passed instruction is a FROM instruction with a stage alias.
+--   Otherwise, returns True.
+aliasMustBe :: (Text.Text -> Bool) -> Instruction a -> Bool
+aliasMustBe predicate fromInstr =
+  case fromInstr of
+    From BaseImage {alias = Just (ImageAlias as)} -> predicate as
+    _ -> True
diff --git a/src/Hadolint/Rule/DL3000.hs b/src/Hadolint/Rule/DL3000.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3000.hs
@@ -0,0 +1,22 @@
+module Hadolint.Rule.DL3000 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax (Instruction (..))
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3000"
+    severity = DLErrorC
+    message = "Use absolute WORKDIR"
+    check (Workdir loc)
+      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
+      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
+      | otherwise = False
+    check _ = True
+    dropQuotes chr
+      | chr == '\"' = True
+      | chr == '\'' = True
+      | otherwise = False
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3001.hs b/src/Hadolint/Rule/DL3001.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3001.hs
@@ -0,0 +1,33 @@
+module Hadolint.Rule.DL3001 (rule) where
+
+import qualified Data.Set as Set
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3001"
+    severity = DLInfoC
+    message =
+      "For some bash commands it makes no sense running them in a Docker container like `ssh`, \
+      \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"
+
+    check (Run (RunArgs args _)) = foldArguments hasInvalid args
+    check _ = True
+
+    hasInvalid args = null [arg | arg <- Shell.findCommandNames args, Set.member arg invalidCmds]
+    invalidCmds =
+      Set.fromList
+        [ "free",
+          "kill",
+          "mount",
+          "ps",
+          "service",
+          "shutdown",
+          "ssh",
+          "top",
+          "vim"
+        ]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3002.hs b/src/Hadolint/Rule/DL3002.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3002.hs
@@ -0,0 +1,49 @@
+module Hadolint.Rule.DL3002 (rule) where
+
+import qualified Data.IntMap.Strict as Map
+import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax (Instruction (..), Linenumber)
+
+type StageLine = Linenumber
+
+type UserLine = Linenumber
+
+data Acc
+  = Acc StageLine (Map.IntMap UserLine)
+  | Empty
+  deriving (Show)
+
+rule :: Rule args
+rule = veryCustomRule check (emptyState Empty) markFailures
+  where
+    code = "DL3002"
+    severity = DLWarningC
+    message = "Last USER should not be root"
+
+    check line st (From _) = st |> modify (rememberStage line)
+    check line st (User user)
+      | not (isRoot user) = st |> modify forgetStage
+      | otherwise = st |> modify (rememberLine line)
+    check _ st _ = st
+
+    isRoot user =
+      Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"
+
+    markFailures (State fails (Acc _ st)) = Map.foldl' (Seq.|>) fails (fmap makeFail st)
+    markFailures st = failures st
+    makeFail line = CheckFailure {..}
+{-# INLINEABLE rule #-}
+
+rememberStage :: StageLine -> Acc -> Acc
+rememberStage from (Acc _ m) = Acc from m
+rememberStage from Empty = Acc from Map.empty
+
+forgetStage :: Acc -> Acc
+forgetStage (Acc from m) = Acc from (m |> Map.delete from)
+forgetStage Empty = Empty
+
+rememberLine :: StageLine -> Acc -> Acc
+rememberLine line (Acc from m) = Acc from (m |> Map.insert from line)
+rememberLine _ Empty = Empty
diff --git a/src/Hadolint/Rule/DL3003.hs b/src/Hadolint/Rule/DL3003.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3003.hs
@@ -0,0 +1,15 @@
+module Hadolint.Rule.DL3003 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell, usingProgram)
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3003"
+    severity = DLWarningC
+    message = "Use WORKDIR to switch to a directory"
+    check (Run (RunArgs args _)) = foldArguments (not . usingProgram "cd") args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3004.hs b/src/Hadolint/Rule/DL3004.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3004.hs
@@ -0,0 +1,17 @@
+module Hadolint.Rule.DL3004 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell, usingProgram)
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3004"
+    severity = DLErrorC
+    message =
+      "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \
+      \root"
+    check (Run (RunArgs args _)) = foldArguments (not . usingProgram "sudo") args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3005.hs b/src/Hadolint/Rule/DL3005.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3005.hs
@@ -0,0 +1,17 @@
+module Hadolint.Rule.DL3005 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3005"
+    severity = DLErrorC
+    message = "Do not use apt-get upgrade or dist-upgrade"
+    check (Run (RunArgs args _)) =
+      foldArguments (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade", "dist-upgrade"])) args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3006.hs b/src/Hadolint/Rule/DL3006.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3006.hs
@@ -0,0 +1,33 @@
+module Hadolint.Rule.DL3006 (rule) where
+
+import qualified Data.Set as Set
+import Data.Text (Text)
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = customRule check (emptyState Set.empty)
+  where
+    code = "DL3006"
+    severity = DLWarningC
+    message = "Always tag the version of an image explicitly"
+
+    check line st (From from) =
+      let newState = st |> modify (insertFromAlias from)
+       in case from of
+            BaseImage {image = (Image _ "scratch")} -> newState
+            BaseImage {digest = Just _} -> newState
+            BaseImage {image = (Image _ i), tag = Nothing} ->
+              -- When the image being used is a previously defined FROM alias,
+              -- then we can safely ignore that the image is not tagged. Otherwise
+              -- we marked it as a failure
+              if Set.member i (state st)
+                then newState
+                else newState |> addFail (CheckFailure {..})
+            _ -> newState
+    check _ st _ = st
+{-# INLINEABLE rule #-}
+
+insertFromAlias :: BaseImage -> Set.Set Text -> Set.Set Text
+insertFromAlias BaseImage {alias = Just a} st = st |> Set.insert (unImageAlias a)
+insertFromAlias _ st = st
diff --git a/src/Hadolint/Rule/DL3007.hs b/src/Hadolint/Rule/DL3007.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3007.hs
@@ -0,0 +1,16 @@
+module Hadolint.Rule.DL3007 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3007"
+    severity = DLWarningC
+    message =
+      "Using latest is prone to errors if the image will ever update. Pin the version explicitly \
+      \to a release tag"
+    check (From BaseImage {tag = Just t}) = t /= "latest"
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3008.hs b/src/Hadolint/Rule/DL3008.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3008.hs
@@ -0,0 +1,31 @@
+module Hadolint.Rule.DL3008 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3008"
+    severity = DLWarningC
+    message =
+      "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \
+      \install <package>=<version>`"
+    check (Run (RunArgs args _)) = foldArguments (all versionFixed . aptGetPackages) args
+    check _ = True
+    versionFixed package = "=" `Text.isInfixOf` package || ("/" `Text.isInfixOf` package || ".deb" `Text.isSuffixOf` package)
+{-# INLINEABLE rule #-}
+
+aptGetPackages :: ParsedShell -> [Text.Text]
+aptGetPackages args =
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdHasArgs "apt-get" ["install"] cmd,
+      arg <- Shell.getArgsNoFlags (dropTarget cmd),
+      arg /= "install"
+  ]
+  where
+    dropTarget = Shell.dropFlagArg ["t", "target-release"]
diff --git a/src/Hadolint/Rule/DL3009.hs b/src/Hadolint/Rule/DL3009.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3009.hs
@@ -0,0 +1,65 @@
+module Hadolint.Rule.DL3009 (rule) where
+
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as Text
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+data Acc
+  = Acc
+      { lastFrom :: BaseImage,
+        stages :: Map.Map Text.Text Linenumber,
+        forgets :: Map.Map Linenumber BaseImage
+      }
+  | Empty
+  deriving (Show)
+
+rule :: Rule Shell.ParsedShell
+rule = veryCustomRule check (emptyState Empty) markFailures
+  where
+    code = "DL3009"
+    severity = DLInfoC
+    message = "Delete the apt-get lists after installing something"
+
+    check line st (From from) = st |> modify (rememberStage line from)
+    check line st (Run (RunArgs args _))
+      | foldArguments forgotToCleanup args = st |> modify (rememberLine line)
+      | otherwise = st
+    check _ st _ = st
+
+    -- Convert the final state into failures
+    markFailures (State fails Empty) = fails
+    markFailures (State _ Acc {..}) = forgets |> Map.foldMapWithKey mapFail
+      where
+        mapFail line from
+          | from == lastFrom = pure CheckFailure {..}
+          | BaseImage {alias = Just (ImageAlias als)} <- from, -- Check if this stage is used later
+            Just _ <- Map.lookup als stages =
+            -- If the same alias is used in another stage, fail
+            pure CheckFailure {..}
+          | otherwise = mempty
+{-# INLINEABLE rule #-}
+
+rememberStage :: Linenumber -> BaseImage -> Acc -> Acc
+rememberStage line from@BaseImage {image = Image _ als} (Acc _ stages o) = Acc from (Map.insert als line stages) o
+rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from (Map.singleton als line) Map.empty
+
+rememberLine :: Linenumber -> Acc -> Acc
+rememberLine line (Acc from stages o) = Acc from stages (Map.insert line from o)
+rememberLine line Empty = Acc emptyImage mempty (Map.singleton line emptyImage)
+
+forgotToCleanup :: Shell.ParsedShell -> Bool
+forgotToCleanup args
+  | hasUpdate && not hasCleanup = True
+  | otherwise = False
+  where
+    hasCleanup =
+      any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)
+
+    hasUpdate = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)
+
+-- | Even though dockerfiles without a FROM are not valid, we still want to provide some feedback for this rule
+-- so we pretend there is a base image at the start of the file if there is none
+emptyImage :: BaseImage
+emptyImage = (BaseImage {image = "scratch", tag = Nothing, digest = Nothing, alias = Nothing, platform = Nothing})
diff --git a/src/Hadolint/Rule/DL3010.hs b/src/Hadolint/Rule/DL3010.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3010.hs
@@ -0,0 +1,38 @@
+module Hadolint.Rule.DL3010 (rule) where
+
+import Data.Foldable (toList)
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3010"
+    severity = DLInfoC
+    message = "Use ADD for extracting archives into an image"
+    check (Copy (CopyArgs srcs _ _ _)) =
+      and
+        [ not (format `Text.isSuffixOf` src)
+          | SourcePath src <- toList srcs,
+            format <- archiveFormats
+        ]
+    check _ = True
+    archiveFormats =
+      [ ".tar",
+        ".tar.bz2",
+        ".tb2",
+        ".tbz",
+        ".tbz2",
+        ".tar.gz",
+        ".tgz",
+        ".tpz",
+        ".tar.lz",
+        ".tar.lzma",
+        ".tlz",
+        ".tar.xz",
+        ".txz",
+        ".tar.Z",
+        ".tZ"
+      ]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3011.hs b/src/Hadolint/Rule/DL3011.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3011.hs
@@ -0,0 +1,16 @@
+module Hadolint.Rule.DL3011 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3011"
+    severity = DLErrorC
+    message = "Valid UNIX ports range from 0 to 65535"
+    check (Expose (Ports ports)) =
+      and [p <= 65535 | Port p _ <- ports]
+        && and [l <= 65535 && m <= 65535 | PortRange l m _ <- ports]
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3012.hs b/src/Hadolint/Rule/DL3012.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3012.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3012 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: Rule args
+rule = customRule check (emptyState False)
+  where
+    code = "DL3012"
+    severity = DLErrorC
+    message = "Multiple `HEALTHCHECK` instructions"
+    check _ st From {} = st |> replaceWith False
+    check line st Healthcheck {}
+        | not (state st) = st |> replaceWith True
+        | otherwise = st |> addFail CheckFailure {..}
+    check _ st _ = st
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3013.hs b/src/Hadolint/Rule/DL3013.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3013.hs
@@ -0,0 +1,83 @@
+module Hadolint.Rule.DL3013 (rule) where
+
+import Data.List (isInfixOf)
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3013"
+    severity = DLWarningC
+    message =
+      "Pin versions in pip. Instead of `pip install <package>` use `pip install \
+      \<package>==<version>` or `pip install --requirement <requirements file>`"
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args
+    check _ = True
+
+    forgotToPinVersion cmd =
+      isPipInstall cmd
+        && not (hasBuildConstraint cmd)
+        && not (all versionFixed (packages cmd))
+
+    -- Check if the command is a pip* install command, and that specific packages are being listed
+    isPipInstall cmd =
+      ( Shell.isPipInstall cmd
+          && not (hasBuildConstraint cmd)
+          && not (all versionFixed (packages cmd))
+      )
+        && not (requirementInstall cmd)
+
+    -- If the user is installing requirements from a file or just the local module, then we are not interested
+    -- in running this rule
+    requirementInstall cmd =
+      ["--requirement"] `isInfixOf` Shell.getArgs cmd
+        || ["-r"] `isInfixOf` Shell.getArgs cmd
+        || ["."] `isInfixOf` Shell.getArgs cmd
+
+    hasBuildConstraint cmd = Shell.hasFlag "constraint" cmd || Shell.hasFlag "c" cmd
+    versionFixed package = hasVersionSymbol package || isVersionedGit package || isLocalPackage package
+    isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package
+    versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]
+    hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols]
+    localPackageFileExtensions = [".whl", ".tar.gz"]
+    isLocalPackage package = or [s `Text.isSuffixOf` package | s <- localPackageFileExtensions]
+{-# INLINEABLE rule #-}
+
+packages :: Shell.Command -> [Text.Text]
+packages cmd =
+  stripInstallPrefix $
+    Shell.getArgsNoFlags $
+      Shell.dropFlagArg
+        [ "abi",
+          "b",
+          "build",
+          "e",
+          "editable",
+          "extra-index-url",
+          "f",
+          "find-links",
+          "i",
+          "index-url",
+          "implementation",
+          "no-binary",
+          "only-binary",
+          "platform",
+          "prefix",
+          "progress-bar",
+          "proxy",
+          "python-version",
+          "root",
+          "src",
+          "t",
+          "target",
+          "trusted-host",
+          "upgrade-strategy"
+        ]
+        cmd
+
+stripInstallPrefix :: [Text.Text] -> [Text.Text]
+stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
diff --git a/src/Hadolint/Rule/DL3014.hs b/src/Hadolint/Rule/DL3014.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3014.hs
@@ -0,0 +1,21 @@
+module Hadolint.Rule.DL3014 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3014"
+    severity = DLWarningC
+    message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotAptYesOption) args
+    check _ = True
+
+    forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)
+    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
+    hasYesOption = Shell.hasAnyFlag ["y", "yes", "q", "assume-yes"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3015.hs b/src/Hadolint/Rule/DL3015.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3015.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3015 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3015"
+    severity = DLInfoC
+    message = "Avoid additional packages by specifying `--no-install-recommends`"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotNoInstallRecommends) args
+    check _ = True
+
+    forgotNoInstallRecommends cmd = isAptGetInstall cmd && not (disablesRecommendOption cmd)
+    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
+    disablesRecommendOption cmd =
+      Shell.hasFlag "no-install-recommends" cmd
+        || Shell.hasArg "APT::Install-Recommends=false" cmd
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3016.hs b/src/Hadolint/Rule/DL3016.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3016.hs
@@ -0,0 +1,61 @@
+module Hadolint.Rule.DL3016 (rule) where
+
+import Data.List (isPrefixOf)
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+-- | Rule for pinning NPM packages to version, tag, or commit
+--  supported formats by Hadolint
+--    npm install (with no args, in package dir)
+--    npm install [<@scope>/]<name>
+--    npm install [<@scope>/]<name>@<tag>
+--    npm install [<@scope>/]<name>@<version>
+--    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]
+--    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>]
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3016"
+    severity = DLWarningC
+    message =
+      "Pin versions in npm. Instead of `npm install <package>` use `npm install \
+      \<package>@<version>`"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args
+    check _ = True
+
+    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)
+
+    versionFixed package
+      | hasGitPrefix package = isVersionedGit package
+      | hasTarballSuffix package = True
+      | isFolder package = True
+      | otherwise = hasVersionSymbol package
+
+    gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
+    pathPrefixes = ["/", "./", "../", "~/"]
+    tarballSuffixes = [".tar", ".tar.gz", ".tgz"]
+
+    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
+
+    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
+      where
+        dropScope pkg =
+          if "@" `Text.isPrefixOf` pkg
+            then Text.dropWhile ('/' <) pkg
+            else pkg
+{-# INLINEABLE rule #-}
+
+stripInstallPrefix :: [Text.Text] -> [Text.Text]
+stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
diff --git a/src/Hadolint/Rule/DL3017.hs b/src/Hadolint/Rule/DL3017.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3017.hs
@@ -0,0 +1,16 @@
+module Hadolint.Rule.DL3017 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3017"
+    severity = DLErrorC
+    message = "Do not use apk upgrade"
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands (Shell.cmdHasArgs "apk" ["upgrade"])) args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3018.hs b/src/Hadolint/Rule/DL3018.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3018.hs
@@ -0,0 +1,30 @@
+module Hadolint.Rule.DL3018 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3018"
+    severity = DLWarningC
+    message =
+      "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
+    check (Run (RunArgs args _)) = foldArguments (\as -> and [versionFixed p | p <- apkAddPackages as]) args
+    check _ = True
+    versionFixed package = "=" `Text.isInfixOf` package
+{-# INLINEABLE rule #-}
+
+apkAddPackages :: ParsedShell -> [Text.Text]
+apkAddPackages args =
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdHasArgs "apk" ["add"] cmd,
+      arg <- Shell.getArgsNoFlags (dropTarget cmd),
+      arg /= "add"
+  ]
+  where
+    dropTarget = Shell.dropFlagArg ["t", "virtual", "repository", "X"]
diff --git a/src/Hadolint/Rule/DL3019.hs b/src/Hadolint/Rule/DL3019.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3019.hs
@@ -0,0 +1,19 @@
+module Hadolint.Rule.DL3019 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3019"
+    severity = DLInfoC
+    message =
+      "Use the `--no-cache` switch to avoid the need to use `--update` and remove \
+      \`/var/cache/apk/*` when done installing packages"
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotCacheOption) args
+    check _ = True
+    forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd && not (Shell.hasFlag "no-cache" cmd)
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3020.hs b/src/Hadolint/Rule/DL3020.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3020.hs
@@ -0,0 +1,45 @@
+module Hadolint.Rule.DL3020 (rule) where
+
+import Data.Foldable (toList)
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3020"
+    severity = DLErrorC
+    message = "Use COPY instead of ADD for files and folders"
+
+    check (Add (AddArgs srcs _ _)) =
+      and [isArchive src || isUrl src | SourcePath src <- toList srcs]
+    check _ = True
+{-# INLINEABLE rule #-}
+
+isArchive :: Text.Text -> Bool
+isArchive path =
+  or
+    ( [ ftype `Text.isSuffixOf` path
+        | ftype <-
+            [ ".tar",
+              ".gz",
+              ".bz2",
+              ".xz",
+              ".zip",
+              ".tgz",
+              ".tb2",
+              ".tbz",
+              ".tbz2",
+              ".lz",
+              ".lzma",
+              ".tlz",
+              ".txz",
+              ".Z",
+              ".tZ"
+            ]
+      ]
+    )
+
+isUrl :: Text.Text -> Bool
+isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])
diff --git a/src/Hadolint/Rule/DL3021.hs b/src/Hadolint/Rule/DL3021.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3021.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3021 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3021"
+    severity = DLErrorC
+    message = "COPY with more than 2 arguments requires the last argument to end with /"
+
+    check (Copy (CopyArgs sources t _ _))
+      | length sources > 1 = endsWithSlash t
+      | otherwise = True
+    check _ = True
+
+    endsWithSlash (TargetPath t) = not (Text.null t) && Text.last t == '/'
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3022.hs b/src/Hadolint/Rule/DL3022.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3022.hs
@@ -0,0 +1,19 @@
+module Hadolint.Rule.DL3022 (rule) where
+
+import qualified Data.Set as Set
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = customRule check (emptyState Set.empty)
+  where
+    code = "DL3022"
+    severity = DLWarningC
+    message = "COPY --from should reference a previously defined FROM alias"
+
+    check _ st (From BaseImage {alias = Just (ImageAlias als)}) = st |> modify (Set.insert als)
+    check line st (Copy (CopyArgs _ _ _ (CopySource s)))
+      | Set.member s (state st) = st
+      | otherwise = st |> addFail CheckFailure {..}
+    check _ st _ = st
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3023.hs b/src/Hadolint/Rule/DL3023.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3023.hs
@@ -0,0 +1,19 @@
+module Hadolint.Rule.DL3023 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = customRule check (emptyState Nothing)
+  where
+    code = "DL3023"
+    severity = DLErrorC
+    message = "COPY --from should reference a previously defined FROM alias"
+
+    check _ st f@(From _) = st |> replaceWith (Just f) -- Remember the last FROM instruction found
+    check line st@(State _ (Just fromInstr)) (Copy (CopyArgs _ _ _ (CopySource stageName)))
+      | aliasMustBe (/= stageName) fromInstr = st
+      | otherwise = st |> addFail CheckFailure {..}
+    -- cannot copy from the same stage!
+    check _ st _ = st
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3024.hs b/src/Hadolint/Rule/DL3024.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3024.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3024 (rule) where
+
+import qualified Data.Set as Set
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = customRule check (emptyState Set.empty)
+  where
+    code = "DL3024"
+    severity = DLErrorC
+    message = "FROM aliases (stage names) must be unique"
+
+    check line st (From BaseImage {alias = Just (ImageAlias als)}) =
+      let newState = st |> modify (Set.insert als)
+       in if Set.member als (state st)
+            then newState |> addFail CheckFailure {..}
+            else newState
+    check _ st _ = st
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3025.hs b/src/Hadolint/Rule/DL3025.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3025.hs
@@ -0,0 +1,16 @@
+module Hadolint.Rule.DL3025 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3025"
+    severity = DLWarningC
+    message = "Use arguments JSON notation for CMD and ENTRYPOINT arguments"
+
+    check (Cmd (ArgumentsText _)) = False
+    check (Entrypoint (ArgumentsText _)) = False
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3026.hs b/src/Hadolint/Rule/DL3026.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3026.hs
@@ -0,0 +1,29 @@
+module Hadolint.Rule.DL3026 (rule) where
+
+import qualified Data.Set as Set
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Set.Set Registry -> Rule args
+rule allowed = customRule check (emptyState Set.empty)
+  where
+    code = "DL3026"
+    severity = DLErrorC
+    message = "Use only an allowed registry in the FROM image"
+
+    check line st (From BaseImage {image, alias}) =
+      let newState = st |> modify (Set.insert alias)
+       in if doCheck (state st) image
+            then newState
+            else newState |> addFail CheckFailure {..}
+    check _ st _ = st
+
+    doCheck st img = Set.member (toImageAlias img) st || Set.null allowed || isAllowed img
+
+    toImageAlias = Just . ImageAlias . imageName
+    isAllowed Image {registryName = Just registry} = Set.member registry allowed
+    isAllowed Image {registryName = Nothing, imageName} =
+      imageName == "scratch"
+        || Set.member "docker.io" allowed
+        || Set.member "hub.docker.com" allowed
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3027.hs b/src/Hadolint/Rule/DL3027.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3027.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3027 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3027"
+    severity = DLWarningC
+    message =
+      "Do not use apt as it is meant to be a end-user tool, use apt-get or apt-cache instead"
+
+    check (Run (RunArgs args _)) = foldArguments (not . Shell.usingProgram "apt") args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3028.hs b/src/Hadolint/Rule/DL3028.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3028.hs
@@ -0,0 +1,36 @@
+module Hadolint.Rule.DL3028 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3028"
+    severity = DLWarningC
+    message =
+      "Pin versions in gem install. Instead of `gem install <gem>` use `gem \
+      \install <gem>:<version>`"
+
+    check (Run (RunArgs args _)) = foldArguments (all versionFixed . gems) args
+    check _ = True
+
+    versionFixed package = ":" `Text.isInfixOf` package
+{-# INLINEABLE rule #-}
+
+gems :: Shell.ParsedShell -> [Text.Text]
+gems shell =
+  [ arg
+    | cmd <- Shell.presentCommands shell,
+      Shell.cmdHasArgs "gem" ["install", "i"] cmd,
+      not (Shell.cmdHasArgs "gem" ["-v"] cmd),
+      not (Shell.cmdHasArgs "gem" ["--version"] cmd),
+      not (Shell.cmdHasPrefixArg "gem" "--version=" cmd),
+      arg <- Shell.getArgsNoFlags cmd,
+      arg /= "install",
+      arg /= "i",
+      arg /= "--"
+  ]
diff --git a/src/Hadolint/Rule/DL3029.hs b/src/Hadolint/Rule/DL3029.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3029.hs
@@ -0,0 +1,15 @@
+module Hadolint.Rule.DL3029 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3029"
+    severity = DLWarningC
+    message = "Do not use --platform flag with FROM"
+
+    check (From BaseImage {platform = Just p}) = p == ""
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3030.hs b/src/Hadolint/Rule/DL3030.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3030.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3030 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3030"
+    severity = DLWarningC
+    message = "Use the -y switch to avoid manual input `yum install -y <package`"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotYumYesOption) args
+    check _ = True
+
+    forgotYumYesOption cmd = isYumInstall cmd && not (hasYesOption cmd)
+    isYumInstall = Shell.cmdHasArgs "yum" ["install", "groupinstall", "localinstall"]
+    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3031.hs b/src/Hadolint/Rule/DL3031.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3031.hs
@@ -0,0 +1,27 @@
+module Hadolint.Rule.DL3031 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3031"
+    severity = DLErrorC
+    message = "Do not use yum update."
+    check (Run (RunArgs args _)) =
+      foldArguments
+        ( Shell.noCommands
+            ( Shell.cmdHasArgs
+                "yum"
+                [ "update",
+                  "update-to",
+                  "upgrade",
+                  "upgrade-to"
+                ]
+            )
+        )
+        args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3032.hs b/src/Hadolint/Rule/DL3032.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3032.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3032 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3032"
+    severity = DLWarningC
+    message = "`yum clean all` missing after yum command."
+
+    check (Run (RunArgs args _)) =
+      foldArguments (Shell.noCommands yumInstall) args
+        || ( foldArguments (Shell.anyCommands yumInstall) args
+               && foldArguments (Shell.anyCommands yumClean) args
+           )
+    check _ = True
+
+    yumInstall = Shell.cmdHasArgs "yum" ["install"]
+    yumClean = Shell.cmdHasArgs "yum" ["clean", "all"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3033.hs b/src/Hadolint/Rule/DL3033.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3033.hs
@@ -0,0 +1,26 @@
+module Hadolint.Rule.DL3033 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3033"
+    severity = DLWarningC
+    message = "Specify version with `yum install -y <package>-<version>`."
+
+    check (Run (RunArgs args _)) = foldArguments (all versionFixed . yumPackages) args
+    check _ = True
+
+    versionFixed package =
+      "-" `Text.isInfixOf` package
+        || ".rpm" `Text.isSuffixOf` package
+{-# INLINEABLE rule #-}
+
+yumPackages :: Shell.ParsedShell -> [Text.Text]
+yumPackages args =
+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
+  ]
diff --git a/src/Hadolint/Rule/DL3034.hs b/src/Hadolint/Rule/DL3034.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3034.hs
@@ -0,0 +1,30 @@
+module Hadolint.Rule.DL3034 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3034"
+    severity = DLWarningC
+    message = "Non-interactive switch missing from `zypper` command: `zypper install -y`"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotZypperYesOption) args
+    check _ = True
+
+    forgotZypperYesOption cmd = isZypperInstall cmd && not (hasYesOption cmd)
+    isZypperInstall =
+      Shell.cmdHasArgs
+        "zypper"
+        [ "install",
+          "in",
+          "remove",
+          "rm",
+          "source-install",
+          "si",
+          "patch"
+        ]
+    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3035.hs b/src/Hadolint/Rule/DL3035.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3035.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3035 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3035"
+    severity = DLWarningC
+    message = "Do not use `zypper update`."
+
+    check (Run (RunArgs args _)) =
+      foldArguments
+        ( Shell.noCommands
+            ( Shell.cmdHasArgs
+                "zypper"
+                [ "update",
+                  "up",
+                  "dist-upgrade",
+                  "dup"
+                ]
+            )
+        )
+        args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3036.hs b/src/Hadolint/Rule/DL3036.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3036.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3036 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3036"
+    severity = DLWarningC
+    message = "`zypper clean` missing after zypper use."
+
+    check (Run (RunArgs args _)) =
+      foldArguments (Shell.noCommands zypperInstall) args
+        || ( foldArguments (Shell.anyCommands zypperInstall) args
+               && foldArguments (Shell.anyCommands zypperClean) args
+           )
+    check _ = True
+
+    zypperInstall = Shell.cmdHasArgs "zypper" ["install", "in"]
+    zypperClean = Shell.cmdHasArgs "zypper" ["clean", "cc"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3037.hs b/src/Hadolint/Rule/DL3037.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3037.hs
@@ -0,0 +1,30 @@
+module Hadolint.Rule.DL3037 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3037"
+    severity = DLWarningC
+    message = "Specify version with `zypper install -y <package>=<version>`."
+
+    check (Run (RunArgs args _)) = foldArguments (all versionFixed . zypperPackages) args
+    check _ = True
+
+    versionFixed package =
+      "=" `Text.isInfixOf` package
+        || ">=" `Text.isInfixOf` package
+        || ">" `Text.isInfixOf` package
+        || "<=" `Text.isInfixOf` package
+        || "<" `Text.isInfixOf` package
+        || ".rpm" `Text.isSuffixOf` package
+{-# INLINEABLE rule #-}
+
+zypperPackages :: Shell.ParsedShell -> [Text.Text]
+zypperPackages args =
+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "zypper" ["install", "in"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install", arg /= "in"
+  ]
diff --git a/src/Hadolint/Rule/DL3038.hs b/src/Hadolint/Rule/DL3038.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3038.hs
@@ -0,0 +1,20 @@
+module Hadolint.Rule.DL3038 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3038"
+    severity = DLWarningC
+    message = "Use the -y switch to avoid manual input `dnf install -y <package`"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotDnfYesOption) args
+    check _ = True
+
+    forgotDnfYesOption cmd = isDnfInstall cmd && not (hasYesOption cmd)
+    isDnfInstall = Shell.cmdHasArgs "dnf" ["install", "groupinstall", "localinstall"]
+    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3039.hs b/src/Hadolint/Rule/DL3039.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3039.hs
@@ -0,0 +1,26 @@
+module Hadolint.Rule.DL3039 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3039"
+    severity = DLErrorC
+    message = "Do not use dnf update."
+
+    check (Run (RunArgs args _)) =
+      foldArguments
+        ( Shell.noCommands
+            ( Shell.cmdHasArgs
+                "dnf"
+                [ "upgrade",
+                  "upgrade-minimal"
+                ]
+            )
+        )
+        args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3040.hs b/src/Hadolint/Rule/DL3040.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3040.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3040 (rule) where
+
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3040"
+    severity = DLWarningC
+    message = "`dnf clean all` missing after dnf command."
+
+    check (Run (RunArgs args _)) =
+      foldArguments (Shell.noCommands dnfInstall) args
+        || ( foldArguments (Shell.anyCommands dnfInstall) args
+               && foldArguments (Shell.anyCommands dnfClean) args
+           )
+    check _ = True
+
+    dnfInstall = Shell.cmdHasArgs "dnf" ["install"]
+    dnfClean = Shell.cmdHasArgs "dnf" ["clean", "all"]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3041.hs b/src/Hadolint/Rule/DL3041.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3041.hs
@@ -0,0 +1,26 @@
+module Hadolint.Rule.DL3041 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3041"
+    severity = DLWarningC
+    message = "Specify version with `dnf install -y <package>-<version>`."
+
+    check (Run (RunArgs args _)) = foldArguments (all versionFixed . dnfPackages) args
+    check _ = True
+
+    versionFixed package =
+      "-" `Text.isInfixOf` package
+        || ".rpm" `Text.isSuffixOf` package
+{-# INLINEABLE rule #-}
+
+dnfPackages :: Shell.ParsedShell -> [Text.Text]
+dnfPackages args =
+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "dnf" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
+  ]
diff --git a/src/Hadolint/Rule/DL3042.hs b/src/Hadolint/Rule/DL3042.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3042.hs
@@ -0,0 +1,29 @@
+module Hadolint.Rule.DL3042 (rule) where
+
+import Data.List (isInfixOf)
+import qualified Data.Text as Text
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule Shell.ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3042"
+    severity = DLWarningC
+    message =
+      "Avoid use of cache directory with pip. Use `pip install --no-cache-dir <package>`"
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotNoCacheDir) args
+    check _ = True
+    forgotNoCacheDir cmd =
+      Shell.isPipInstall cmd && not (usesNoCacheDir cmd) && not (isPipWrapper cmd)
+    usesNoCacheDir cmd = "--no-cache-dir" `elem` Shell.getArgs cmd
+{-# INLINEABLE rule #-}
+
+isPipWrapper :: Shell.Command -> Bool
+isPipWrapper cmd@(Shell.Command name _ _) = isWrapper "pipx" || isWrapper "pipenv"
+  where
+    isWrapper :: Text.Text -> Bool
+    isWrapper w =
+      w `Text.isInfixOf` name
+        || ("python" `Text.isPrefixOf` name && ["-m", w] `isInfixOf` Shell.getArgs cmd)
diff --git a/src/Hadolint/Rule/DL3043.hs b/src/Hadolint/Rule/DL3043.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3043.hs
@@ -0,0 +1,17 @@
+module Hadolint.Rule.DL3043 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3043"
+    severity = DLErrorC
+    message = "`ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction."
+
+    check (OnBuild (OnBuild _)) = False
+    check (OnBuild (From _)) = False
+    check (OnBuild (Maintainer _)) = False
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3044.hs b/src/Hadolint/Rule/DL3044.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3044.hs
@@ -0,0 +1,69 @@
+module Hadolint.Rule.DL3044 (rule) where
+
+import Data.List.Index (indexed)
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = customRule check (emptyState Set.empty)
+  where
+    code = "DL3044"
+    severity = DLErrorC
+    message = "Do not refer to an environment variable within the same `ENV` statement where it is defined."
+
+    check line st (Env pairs) =
+      let newState = st |> modify (Set.union (Set.fromList (map fst pairs)))
+       in if null [env | env <- listOfReferences pairs, env `Set.notMember` state st]
+            then newState
+            else newState |> addFail CheckFailure {..}
+    check _ st (Arg arg _) = st |> modify (Set.insert arg)
+    check _ st _ = st
+{-# INLINEABLE rule #-}
+
+-- | generates a list of references to variable names referenced on the right
+-- hand side of a variable definition, except when the variable is
+-- referenced on its own right hand side.
+listOfReferences :: Pairs -> [Text.Text]
+listOfReferences prs =
+  [ var
+    | (idx, (var, _)) <- indexed prs,
+      var `isSubstringOfAny` map (snd . snd) (filter ((/= idx) . fst) (indexed prs))
+  ]
+
+-- | is a reference of a variable substring of any text?
+-- matches ${var_name} and $var_name, but not $var_nameblafoo
+isSubstringOfAny :: Text.Text -> [Text.Text] -> Bool
+isSubstringOfAny t l =
+  not $
+    null
+      [ v
+        | v <- l,
+          (Text.pack "${" <> t <> Text.pack "}") `Text.isInfixOf` v
+            || (t `bareVariableInText` v)
+      ]
+
+-- | we find a 'bare' variable with name v in a text, if
+-- '$v' is in the text at any place and any text following after that
+-- occurence would terminate a variable name. To determine that, the text t
+-- is split at every occurence of var, check if '$v' is in the text and if
+-- any part of the split text would terminate a variable name.
+bareVariableInText :: Text.Text -> Text.Text -> Bool
+bareVariableInText v t =
+  let var = "$" <> v
+      rest = drop 1 $ Text.splitOn var t
+   in var `Text.isInfixOf` t && any terminatesVarName rest
+  where
+    -- x would terminate a variable name if it was appended directly to
+    -- that name
+    terminatesVarName :: Text.Text -> Bool
+    terminatesVarName x = Text.null x || not (beginsWithAnyOf x varChar)
+
+    -- txt begins with any character of String
+    beginsWithAnyOf :: Text.Text -> Set.Set Char -> Bool
+    beginsWithAnyOf txt str = any (`Text.isPrefixOf` txt) (Set.map Text.singleton str)
+
+    -- all characters valid in the inner of a shell variable name
+    varChar :: Set.Set Char
+    varChar = Set.fromList (['0' .. '9'] ++ ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['_'])
diff --git a/src/Hadolint/Rule/DL3045.hs b/src/Hadolint/Rule/DL3045.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3045.hs
@@ -0,0 +1,83 @@
+module Hadolint.Rule.DL3045 (rule) where
+
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+-- This data encapsulates the name of a build stage. It may be None withing an
+-- `ONBUILD` context.
+data Stage
+  = Stage {stage :: Text}
+  | None
+  deriving (Eq, Ord)
+
+-- | The state here keeps the image name/alias of the current build stage
+-- and a map from image names/aliases to a Bool, saving whether or
+-- not a `WORKDIR` has been set in a build stage.
+data Acc
+  = Acc {current :: Stage, workdirSet :: Map Stage Bool}
+  | Empty
+
+rule :: Rule args
+rule = customRule check (emptyState Empty)
+  where
+    code = "DL3045"
+    severity = DLWarningC
+    message = "`COPY` to a relative destination without `WORKDIR` set."
+
+    check _ st (From from) = st |> modify (rememberStage from)
+    check _ st (Workdir _) = st |> modify rememberWorkdir
+    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
+      | "$" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- dest is a variable
+      | otherwise = st |> addFail CheckFailure {..}
+    check _ st _ = st
+{-# INLINEABLE rule #-}
+
+rememberStage :: BaseImage -> Acc -> Acc
+rememberStage BaseImage {alias = Just als} Empty =
+  Acc
+    { current = Stage {stage = unImageAlias als},
+      workdirSet = mempty
+    }
+rememberStage BaseImage {alias = Nothing, image} Empty =
+  Acc
+    { current = Stage {stage = imageName image},
+      workdirSet = mempty
+    }
+rememberStage BaseImage {alias = Just als, image} Acc {..} =
+  Acc
+    { current = Stage {stage = unImageAlias als},
+      workdirSet =
+        let parentValue =
+              Map.lookup (Stage { stage = imageName image}) workdirSet
+                |> fromMaybe False
+         in workdirSet
+              |> Map.insert (Stage {stage = unImageAlias als}) parentValue
+    }
+rememberStage BaseImage {alias = Nothing, image} Acc {..} =
+  Acc
+    { current = Stage {stage = imageName image},
+      workdirSet =
+        let parentValue =
+              Map.lookup (Stage {stage = imageName image}) workdirSet
+                |> fromMaybe False
+         in workdirSet
+              |> Map.insert (Stage {stage = imageName image}) parentValue
+    }
+
+rememberWorkdir :: Acc -> Acc
+rememberWorkdir Empty = Acc {current = None, workdirSet = Map.insert None True Map.empty}
+rememberWorkdir Acc {..} = Acc {current, workdirSet = Map.insert current True workdirSet}
+
+quotePredicate :: Char -> Bool
+quotePredicate c
+  | c == '"' = True
+  | c == '\'' = True
+  | otherwise = False
diff --git a/src/Hadolint/Rule/DL3046.hs b/src/Hadolint/Rule/DL3046.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3046.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL3046 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3046"
+    severity = DLWarningC
+    message = "`useradd` without flag `-l` and high UID will result in excessively large Image."
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotFlagL) args
+    check _ = True
+
+    forgotFlagL cmd = isUseradd cmd && (not (hasLFlag cmd) && hasUFlag cmd && hasLongUID cmd)
+    isUseradd (Shell.Command name _ _) = name == "useradd"
+    hasLFlag = Shell.hasAnyFlag ["l", "no-log-init"]
+    hasUFlag = Shell.hasAnyFlag ["u", "uid"]
+    hasLongUID cmd = any ((> 5) . Text.length) (Shell.getFlagArg "u" cmd)
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3047.hs b/src/Hadolint/Rule/DL3047.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3047.hs
@@ -0,0 +1,36 @@
+module Hadolint.Rule.DL3047 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL3047"
+    severity = DLWarningC
+    message =
+      "Avoid use of wget without progress bar. Use `wget --progress=dot:giga <url>`.\
+      \Or consider using `-q` or `-nv` (shorthands for `--quiet` or `--no-verbose`)."
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotProgress) args
+    check _ = True
+
+    forgotProgress cmd = isWget cmd && (not (hasProgressOption cmd) && not (hasSpecialFlags cmd))
+    isWget (Shell.Command name _ _) = name == "wget"
+    hasProgressOption cmd = Shell.hasFlag "progress" cmd
+
+    hasSpecialFlags cmd =
+      hasQuietFlag cmd
+        || hasOutputFlag cmd
+        || hasAppendOutputFlag cmd
+        || hasNoVerboseFlag cmd
+
+    hasQuietFlag cmd = Shell.hasAnyFlag ["q", "quiet"] cmd
+    hasOutputFlag cmd = Shell.hasAnyFlag ["o", "output-file"] cmd
+    hasAppendOutputFlag cmd = Shell.hasAnyFlag ["a", "append-output"] cmd
+    hasNoVerboseFlag cmd =
+      Shell.hasAnyFlag ["no-verbose"] cmd
+        || Shell.cmdHasArgs "wget" ["-nv"] cmd
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3048.hs b/src/Hadolint/Rule/DL3048.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3048.hs
@@ -0,0 +1,39 @@
+module Hadolint.Rule.DL3048 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3048"
+    severity = DLStyleC
+    message = "Invalid label key."
+    check (Label pairs) = hasNoInvalidKey pairs
+    check _ = True
+
+    hasNoInvalidKey prs = null $ [(l, v) | (l, v) <- prs,
+                                          Text.take 1 l `notElem`
+                                              fmap Text.singleton ['a'..'z'] ||
+                                          Text.takeEnd 1 l `notElem`
+                                              fmap Text.singleton (['a'..'z'] ++ ['0'..'9']) ||
+                                          containsIllegalChar l ||
+                                          hasReservedNamespace l ||
+                                          hasConsecutiveSeparators l]
+{-# INLINEABLE rule #-}
+
+containsIllegalChar :: Text.Text -> Bool
+containsIllegalChar = Text.any (`notElem` validChars)
+
+hasReservedNamespace :: Text.Text -> Bool
+hasReservedNamespace l = "com.docker." `Text.isPrefixOf` l
+  || "io.docker." `Text.isPrefixOf` l
+  || "org.dockerproject." `Text.isPrefixOf` l
+
+hasConsecutiveSeparators :: Text.Text -> Bool
+hasConsecutiveSeparators l = ".." `Text.isInfixOf` l || "--" `Text.isInfixOf` l
+
+validChars :: String
+validChars = ['0'..'9'] ++ ['a' .. 'z'] ++ ['.', '-']
diff --git a/src/Hadolint/Rule/DL3049.hs b/src/Hadolint/Rule/DL3049.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3049.hs
@@ -0,0 +1,60 @@
+ module Hadolint.Rule.DL3049 (rule) where
+
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $ fmap missingLabelRule (Map.keys labelschema)
+{-# INLINEABLE rule #-}
+
+data StageID = StageID
+  { name :: Text.Text,
+    line :: Linenumber
+  } deriving (Eq, Ord, Show)
+
+data Acc
+  = Acc StageID (Set.Set StageID) (Set.Set StageID)
+  | Empty
+  deriving (Show)
+
+-- missingLabelRule
+--
+-- triggers on a `FROM` instruction when label `label` is not defined within
+-- that stage. Tracks defined labels through multi stage builds
+missingLabelRule :: LabelName -> Rule args
+missingLabelRule label = veryCustomRule check (emptyState Empty) markFailure
+  where
+    code = "DL3049"
+    severity = DLInfoC
+    message = "Label `" <> label <> "` is missing."
+    check line state (From BaseImage {image, alias = Just als}) =
+        state |> modify (currentStage (imageName image) (StageID (unImageAlias als) line))
+    check line state (From BaseImage {image, alias = Nothing}) =
+        state |> modify (currentStage (imageName image) (StageID (imageName image) line))
+    check _ state (Label pairs)
+        | label `elem` fmap fst pairs = state |> modify goodStage
+        | otherwise = state
+    check _ state _ = state
+
+    markFailure :: State Acc -> Failures
+    markFailure (State fails (Acc _ _ b)) = Set.foldl' (Seq.|>) fails (Set.map markFail b)
+    markFailure st = failures st
+
+    markFail (StageID _ line) = CheckFailure {..}
+
+currentStage :: Text.Text -> StageID -> Acc -> Acc
+currentStage src stageid (Acc _ g b)
+    | not $ Set.null (Set.filter (predicate src) g) = Acc stageid (g |> Set.insert stageid) b
+    | otherwise = Acc stageid g (b |> Set.insert stageid)
+  where
+    predicate n0 StageID {name = n1} = n1 == n0
+currentStage _ stageid Empty = Acc stageid Set.empty (Set.singleton stageid)
+
+goodStage :: Acc -> Acc
+goodStage (Acc stageid g b) = Acc stageid (g |> Set.insert stageid) (b |> Set.delete stageid)
+goodStage Empty = Empty
diff --git a/src/Hadolint/Rule/DL3050.hs b/src/Hadolint/Rule/DL3050.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3050.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3050 (rule) where
+
+import qualified Data.Map as Map
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Bool -> Rule args
+rule labelschema strictlabels = simpleRule code severity message check
+  where
+    code = "DL3050"
+    severity = DLInfoC
+    message = "Superfluous label(s) present."
+    check (Label pairs)
+        | strictlabels = all ((`elem` Map.keys labelschema) . fst) pairs
+        | otherwise = True
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3051.hs b/src/Hadolint/Rule/DL3051.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3051.hs
@@ -0,0 +1,23 @@
+module Hadolint.Rule.DL3051 (rule) where
+
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $ fmap labelIsNotEmptyRule (Map.keys labelschema)
+{-# INLINEABLE rule #-}
+
+labelIsNotEmptyRule :: LabelName -> Rule args
+labelIsNotEmptyRule label = simpleRule code severity message check
+  where
+    code = "DL3051"
+    severity = DLWarningC
+    message = "label `" <> label <> "` is empty."
+    check (Label pairs) = null $ getEmptyLabels label pairs
+    check _ = True
+
+getEmptyLabels :: LabelName -> Pairs -> Pairs
+getEmptyLabels lbl prs = [(l, v) | (l, v) <- prs, l == lbl, Text.null v]
diff --git a/src/Hadolint/Rule/DL3052.hs b/src/Hadolint/Rule/DL3052.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3052.hs
@@ -0,0 +1,27 @@
+module Hadolint.Rule.DL3052 (rule) where
+
+import qualified Data.Map as Map
+import Data.Maybe
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+import Network.URI as Uri
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $
+  fmap labelIsNotUrlRule (Map.keys (Map.filter (== Url) labelschema))
+{-# INLINEABLE rule #-}
+
+labelIsNotUrlRule :: LabelName -> Rule args
+labelIsNotUrlRule label = simpleRule code severity message check
+  where
+    code = "DL3052"
+    severity = DLWarningC
+    message = "Label `" <> label <> "` is not a valid URL."
+    check (Label ls) = null $ getBadURLLabels label ls
+    check _ = True
+
+getBadURLLabels :: LabelName -> Pairs -> Pairs
+getBadURLLabels lbl prs = [(l, u) | (l, u) <- prs, l == lbl,
+                                    (isNothing . Uri.parseURI) (Text.unpack u)]
diff --git a/src/Hadolint/Rule/DL3053.hs b/src/Hadolint/Rule/DL3053.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3053.hs
@@ -0,0 +1,28 @@
+module Hadolint.Rule.DL3053 (rule) where
+
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Time.RFC3339
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $
+  fmap labelIsNotRFC3339Rule (Map.keys (Map.filter (== Rfc3339) labelschema))
+{-# INLINEABLE rule #-}
+
+
+labelIsNotRFC3339Rule :: LabelName -> Rule args
+labelIsNotRFC3339Rule label = simpleRule code severity message check
+  where
+    code = "DL3053"
+    severity = DLWarningC
+    message = "Label `" <> label <> "` is not a valid time format - must be conform to RFC3339."
+    check (Label ls) = null $ getBadTimeformatLabels label ls
+    check _ = True
+
+getBadTimeformatLabels :: LabelName -> Pairs -> Pairs
+getBadTimeformatLabels lbl pairs = [(l, v) | (l, v) <- pairs,
+                                             l == lbl,
+                                             isNothing $ parseTimeRFC3339 v]
diff --git a/src/Hadolint/Rule/DL3054.hs b/src/Hadolint/Rule/DL3054.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3054.hs
@@ -0,0 +1,32 @@
+module Hadolint.Rule.DL3054 (rule) where
+
+import Data.Either
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+import Distribution.Parsec
+import Distribution.SPDX.Extra
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $
+  fmap labelIsNotSPDXRule (Map.keys (Map.filter (== Spdx) labelschema))
+{-# INLINEABLE rule #-}
+
+labelIsNotSPDXRule :: LabelName -> Rule args
+labelIsNotSPDXRule label = simpleRule code severity message check
+  where
+    code = "DL3054"
+    severity = DLWarningC
+    message = "Label `" <> label <> "` is not a valid SPDX identifier."
+    check (Label ls) = null $ getBadLicenseLabels label ls
+    check _ = True
+
+
+getBadLicenseLabels :: LabelName -> Pairs -> Pairs
+getBadLicenseLabels lbl pairs =
+    [ (l, v) | (l, v) <- pairs,
+               l == lbl,
+               isLeft (eitherParsec (Text.unpack v) :: Either String LicenseId)
+    ]
diff --git a/src/Hadolint/Rule/DL3055.hs b/src/Hadolint/Rule/DL3055.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3055.hs
@@ -0,0 +1,31 @@
+module Hadolint.Rule.DL3055 (rule) where
+
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $
+  fmap labelIsNotGitHashRule (Map.keys (Map.filter (== GitHash) labelschema))
+{-# INLINEABLE rule #-}
+
+labelIsNotGitHashRule :: LabelName -> Rule args
+labelIsNotGitHashRule label = simpleRule code severity message check
+  where
+    code = "DL3055"
+    severity = DLWarningC
+    message = "Label `" <> label <> "` is not a valid git hash."
+    check (Label ls) = null $ getBadHashLabels label ls
+    check _ = True
+
+getBadHashLabels :: LabelName -> Pairs -> Pairs
+getBadHashLabels lbl prs = [(l, v) | (l, v) <- prs, l == lbl, isBadHash v]
+
+isBadHash :: Text.Text -> Bool
+isBadHash h = Text.any (`notElem` validHash) h
+            || (Text.length h /= 40 && Text.length h /= 7)
+
+validHash :: String
+validHash = ['0'..'9'] ++ ['a'..'f']
diff --git a/src/Hadolint/Rule/DL3056.hs b/src/Hadolint/Rule/DL3056.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3056.hs
@@ -0,0 +1,27 @@
+module Hadolint.Rule.DL3056 (rule) where
+
+import Data.Either
+import qualified Data.Map as Map
+import qualified Data.SemVer as SemVer
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+rule :: LabelSchema -> Rule args
+rule labelschema = mconcat $
+  fmap labelIsNotSemVerRule (Map.keys (Map.filter (== SemVer) labelschema))
+{-# INLINEABLE rule #-}
+
+labelIsNotSemVerRule :: LabelName -> Rule args
+labelIsNotSemVerRule label = simpleRule code severity message check
+  where
+    code = "DL3056"
+    severity = DLWarningC
+    message = Text.pack "Label `" <> label <> Text.pack "` does not conform to semantic versioning."
+    check (Label pairs) = hasNoBadVersioning label pairs
+    check _ = True
+
+hasNoBadVersioning :: LabelName -> Pairs -> Bool
+hasNoBadVersioning lbl prs = null [(l, v) | (l, v) <- prs, l == lbl,
+                                            isLeft $ SemVer.fromText v]
diff --git a/src/Hadolint/Rule/DL3057.hs b/src/Hadolint/Rule/DL3057.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3057.hs
@@ -0,0 +1,50 @@
+module Hadolint.Rule.DL3057 (rule) where
+
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+
+data StageID = StageID
+  { name :: Text.Text,
+    line :: Linenumber
+  } deriving (Show, Eq, Ord)
+
+data Acc
+  = Acc StageID (Set.Set StageID) (Set.Set StageID)
+  | Empty
+  deriving (Show)
+
+rule :: Rule args
+rule = veryCustomRule check (emptyState Empty) markFailures
+  where
+    code = "DL3057"
+    severity = DLIgnoreC
+    message = "`HEALTHCHECK` instruction missing."
+
+    check line state (From BaseImage {image, alias = Just als}) =
+      state |> modify (currentStage (imageName image) (StageID (unImageAlias als) line))
+    check line state (From BaseImage {image, alias = Nothing}) =
+      state |> modify (currentStage (imageName image) (StageID (imageName image) line))
+    check _ state (Healthcheck _) = state |> modify goodStage
+    check _ state _ = state
+
+    markFailures :: State Acc -> Failures
+    markFailures (State fails (Acc _ _ b)) = Set.foldl' (Seq.|>) fails (Set.map makeFail b)
+    markFailures st = failures st
+    makeFail (StageID _ line) = CheckFailure {..}
+{-# INLINEABLE rule #-}
+
+currentStage :: Text.Text -> StageID -> Acc -> Acc
+currentStage src stageid (Acc _ g b)
+    | not $ Set.null (Set.filter (predicate src) g) = Acc stageid (g |> Set.insert stageid) b
+    | otherwise = Acc stageid g (b |> Set.insert stageid)
+  where
+    predicate n0 StageID {name = n1} = n1 == n0
+currentStage _ stageid Empty = Acc stageid Set.empty (Set.singleton stageid)
+
+goodStage :: Acc -> Acc
+goodStage (Acc stageid g b) = Acc stageid (g |> Set.insert stageid) (b |> Set.delete stageid)
+goodStage Empty = Empty
diff --git a/src/Hadolint/Rule/DL4000.hs b/src/Hadolint/Rule/DL4000.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL4000.hs
@@ -0,0 +1,14 @@
+module Hadolint.Rule.DL4000 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax (Instruction (..))
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL4000"
+    severity = DLErrorC
+    message = "MAINTAINER is deprecated"
+    check (Maintainer _) = False
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL4001.hs b/src/Hadolint/Rule/DL4001.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL4001.hs
@@ -0,0 +1,32 @@
+module Hadolint.Rule.DL4001 (rule) where
+
+import qualified Data.IntSet as Set
+import Hadolint.Rule
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax (Instruction (..), RunArgs (..))
+
+rule :: Rule Shell.ParsedShell
+rule = customRule check (emptyState Set.empty)
+  where
+    code = "DL4001"
+    severity = DLWarningC
+    message = "Either use Wget or Curl but not both"
+
+    check line st (Run (RunArgs args _)) =
+      let newArgs = foldArguments extractCommands args
+          newState = st |> modify (Set.union newArgs)
+       in if Set.size newArgs > 0 && Set.size (state newState) >= 2
+            then newState |> addFail (CheckFailure {..})
+            else newState
+    -- Reset the state for each stage
+    check _ st From {} = st |> replaceWith Set.empty
+    check _ st _ = st
+{-# INLINEABLE rule #-}
+
+extractCommands :: Shell.ParsedShell -> Set.IntSet
+extractCommands args =
+  Set.fromList
+    [ if w == "curl" then 0 else 1
+      | w <- Shell.findCommandNames args,
+        w == "curl" || w == "wget"
+    ]
diff --git a/src/Hadolint/Rule/DL4003.hs b/src/Hadolint/Rule/DL4003.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL4003.hs
@@ -0,0 +1,25 @@
+module Hadolint.Rule.DL4003 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax (Instruction (..))
+
+data HasCmd = HasCmd | NoCmd
+
+rule :: Rule args
+rule = customRule check (emptyState NoCmd)
+  where
+    code = "DL4003"
+    severity = DLWarningC
+    message =
+      "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \
+      \`CMD` will take effect"
+
+    -- Reset the state each time we find a FROM
+    check _ st From {} = st |> replaceWith NoCmd
+    -- Remember we found a CMD, fail if we found a CMD before
+    check _ st@(State _ NoCmd) Cmd {} = st |> replaceWith HasCmd
+    -- If we already saw a CMD, add an error for the line
+    check line st@(State _ HasCmd) Cmd {} = st |> addFail (CheckFailure {..})
+    -- otherwise return the accumulator
+    check _ st _ = st
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL4004.hs b/src/Hadolint/Rule/DL4004.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL4004.hs
@@ -0,0 +1,24 @@
+module Hadolint.Rule.DL4004 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax (Instruction (..))
+
+data HasEntry = HasEntry | NoEntry
+
+rule :: Rule args
+rule = customRule check (emptyState NoEntry)
+  where
+    code = "DL4004"
+    severity = DLErrorC
+    message =
+      "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \
+      \only the last `ENTRYPOINT` will take effect"
+
+    -- Reset the state each time we find a FROM
+    check _ st From {} = st |> replaceWith NoEntry
+    -- Remember we found an ENTRYPOINT
+    check _ st@(State _ NoEntry) Entrypoint {} = st |> replaceWith HasEntry
+    -- Add a failure if we found another entrypoint in the same stage
+    check line st@(State _ HasEntry) Entrypoint {} = st |> addFail (CheckFailure {..})
+    check _ st _ = st
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL4005.hs b/src/Hadolint/Rule/DL4005.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL4005.hs
@@ -0,0 +1,17 @@
+module Hadolint.Rule.DL4005 (rule) where
+
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = simpleRule code severity message check
+  where
+    code = "DL4005"
+    severity = DLWarningC
+    message = "Use SHELL to change the default shell"
+
+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands (Shell.cmdHasArgs "ln" ["/bin/sh"])) args
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL4006.hs b/src/Hadolint/Rule/DL4006.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL4006.hs
@@ -0,0 +1,41 @@
+module Hadolint.Rule.DL4006 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+
+rule :: Rule ParsedShell
+rule = customRule check (emptyState False)
+  where
+    code = "DL4006"
+    severity = DLWarningC
+    message =
+      "Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using \
+      \/bin/sh in an alpine image or if your shell is symlinked to busybox then consider \
+      \explicitly setting your SHELL to /bin/ash, or disable this check"
+
+    check _ st From {} = st |> replaceWith False -- Reset the state each time we find a new FROM
+    check _ st (Shell args)
+      | foldArguments isPowerShell args = st |> replaceWith True
+      | otherwise = st |> replaceWith (foldArguments hasPipefailOption args)
+    check line st@(State _ False) (Run (RunArgs args _))
+      | foldArguments hasPipes args = st |> addFail CheckFailure {..}
+      | otherwise = st
+    check _ st _ = st
+
+    isPowerShell (Shell.ParsedShell orig _ _) = "pwsh" `Text.isPrefixOf` orig
+    hasPipes script = Shell.hasPipes script
+    hasPipefailOption script =
+      not $
+        null
+          [ True
+            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script,
+              validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"],
+              name == validShell,
+              Shell.hasFlag "o" cmd,
+              arg <- Shell.arg <$> arguments,
+              arg == "pipefail"
+          ]
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/Shellcheck.hs b/src/Hadolint/Rule/Shellcheck.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/Shellcheck.hs
@@ -0,0 +1,44 @@
+module Hadolint.Rule.Shellcheck (rule) where
+
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import qualified Hadolint.Shell as Shell
+import Language.Docker.Syntax
+import qualified ShellCheck.Interface
+
+rule :: Rule ParsedShell
+rule = customRule check (emptyState Shell.defaultShellOpts)
+  where
+    check _ st (From _) = st |> replaceWith Shell.defaultShellOpts -- Reset the state
+    check _ st (Arg name _) = st |> modify (Shell.addVars [name])
+    check _ st (Env pairs) = st |> modify (Shell.addVars (map fst pairs))
+    check _ st (Shell args) = st |> modify (Shell.setShell (foldArguments Shell.original args))
+    check line st (Run (RunArgs args _)) = getFailures |> foldr addFail st
+      where
+        getFailures = foldArguments (runShellCheck (state st)) args
+        runShellCheck opts script = Set.fromList [toFailure line c | c <- Shell.shellcheck opts script]
+    check _ st _ = st
+{-# INLINEABLE rule #-}
+
+-- | Converts ShellCheck errors into our own errors type
+toFailure :: Linenumber -> ShellCheck.Interface.PositionedComment -> CheckFailure
+toFailure line c =
+  CheckFailure
+    { code = RuleCode $ Text.pack ("SC" ++ show (code c)),
+      severity = getDLSeverity $ severity c,
+      message = Text.pack (message c),
+      line = line
+    }
+  where
+    getDLSeverity :: ShellCheck.Interface.Severity -> DLSeverity
+    getDLSeverity s =
+      case s of
+        ShellCheck.Interface.WarningC -> DLWarningC
+        ShellCheck.Interface.InfoC -> DLInfoC
+        ShellCheck.Interface.StyleC -> DLStyleC
+        _ -> DLErrorC
+    severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc
+    code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc
+    message pc = ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc
diff --git a/src/Hadolint/Rules.hs b/src/Hadolint/Rules.hs
deleted file mode 100644
--- a/src/Hadolint/Rules.hs
+++ /dev/null
@@ -1,1236 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Hadolint.Rules where
-
-import Control.Arrow ((&&&))
-import Control.DeepSeq (NFData)
-import Data.List (foldl', isInfixOf, isPrefixOf, mapAccumL, nub)
-import Data.List.NonEmpty (toList)
-import Data.List.Index
-import qualified Data.Map as Map
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-import Data.Void (Void)
-import GHC.Generics (Generic)
-import qualified Hadolint.Shell as Shell
-import Language.Docker.Syntax
-import ShellCheck.Interface (Severity (..))
-import qualified ShellCheck.Interface
-import qualified Text.Megaparsec as Megaparsec
-import qualified Text.Megaparsec.Char as Megaparsec
-
-data DLSeverity
-  = DLErrorC
-  | DLWarningC
-  | DLInfoC
-  | DLStyleC
-  | DLIgnoreC
-  deriving (Show, Eq, Ord, Generic, NFData)
-
-data Metadata = Metadata
-  { code :: Text.Text,
-    severity :: DLSeverity,
-    message :: Text.Text
-  }
-  deriving (Eq, Show)
-
--- a check is the application of a rule on a specific part of code
--- the enforced result and the affected position
--- position only records the linenumber at the moment to keep it easy
--- and simple to develop new rules
--- line numbers in the negative range are meant for the global context
-data RuleCheck = RuleCheck
-  { metadata :: Metadata,
-    filename :: Filename,
-    linenumber :: Linenumber,
-    success :: Bool
-  }
-  deriving (Eq, Show)
-
--- | Contains the required parameters for optional rules
-newtype RulesConfig = RulesConfig
-  { -- | The docker registries that are allowed in FROM
-    allowedRegistries :: Set.Set Registry
-  }
-  deriving (Show, Eq)
-
-instance Ord RuleCheck where
-  a `compare` b = linenumber a `compare` linenumber b
-
-instance Semigroup RulesConfig where
-  RulesConfig a <> RulesConfig b = RulesConfig (a <> b)
-
-instance Monoid RulesConfig where
-  mempty = RulesConfig mempty
-  mappend = (<>)
-
-type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text
-
-type ParsedFile = [InstructionPos Shell.ParsedShell]
-
--- | A function to check individual dockerfile instructions.
--- It gets the current state and a line number.
--- It should return the new state and whether or not the check passes for the given instruction.
-type SimpleCheckerWithState state =
-  state -> Linenumber -> Instruction Shell.ParsedShell -> (state, Bool)
-
--- | A function to check individual dockerfile instructions.
--- It gets the current line number.
--- It should return True if the check passes for the given instruction.
-type SimpleCheckerWithLine = (Linenumber -> Instruction Shell.ParsedShell -> Bool)
-
--- | A function to check individual dockerfile instructions.
--- It should return the new state and a list of Metadata records.
--- Each Metadata record signifies a failing check for the given instruction.
-type CheckerWithState state =
-  state -> Linenumber -> Instruction Shell.ParsedShell -> (state, [Metadata])
-
-link :: Metadata -> Text.Text
-link (Metadata code _ _)
-  | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code
-  | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code
-  | otherwise = "https://github.com/hadolint/hadolint"
-
--- a Rule takes a Dockerfile with parsed shell and returns the executed checks
-type Rule = ParsedFile -> [RuleCheck]
-
--- Apply a function on each instruction and create a check
--- for the according line number
-mapInstructions :: CheckerWithState state -> state -> Rule
-mapInstructions f initialState dockerfile =
-  let (_, results) = mapAccumL applyRule initialState dockerfile
-   in concat results
-  where
-    applyRule state (InstructionPos onbuild@(OnBuild i) source linenumber) =
-      -- All rules applying to instructions also apply to ONBUILD,
-      -- so we unwrap the OnBuild constructor and check directly the inner
-      -- instruction. Then we also check the instruction itself and append the
-      -- result to avoid losing out on detecting problems with `ONBUILD`
-      let (innerState, innerResults) = applyWithState state source linenumber i
-          (finalState, outerResults) = applyWithState innerState source linenumber onbuild
-       in (finalState, innerResults <> outerResults)
-    applyRule state (InstructionPos i source linenumber) =
-      applyWithState state source linenumber i -- Otherwise, normal instructions are not unwrapped
-    applyWithState state source linenumber instruction =
-      let (newState, res) = f state linenumber instruction
-       in (newState, [RuleCheck m source linenumber False | m <- res])
-
-instructionRule ::
-  Text.Text -> DLSeverity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule
-instructionRule code severity message check =
-  instructionRuleLine code severity message (const check)
-
-instructionRuleLine :: Text.Text -> DLSeverity -> Text.Text -> SimpleCheckerWithLine -> Rule
-instructionRuleLine code severity message check =
-  instructionRuleState code severity message checkAndDropState ()
-  where
-    checkAndDropState state line instr = (state, check line instr)
-
-instructionRuleState ::
-  Text.Text -> DLSeverity -> Text.Text -> SimpleCheckerWithState state -> state -> Rule
-instructionRuleState code severity message f = mapInstructions constMetadataCheck
-  where
-    meta = Metadata code severity message
-    constMetadataCheck st ln instr =
-      let (newSt, success) = f st ln instr
-       in if not success
-            then (newSt, [meta])
-            else (newSt, [])
-
-withState :: a -> b -> (a, b)
-withState st res = (st, res)
-
-argumentsRule :: (Shell.ParsedShell -> a) -> Arguments Shell.ParsedShell -> a
-argumentsRule applyRule args =
-  case args of
-    ArgumentsText as -> applyRule as
-    ArgumentsList as -> applyRule as
-
--- Enforce rules on a dockerfile and return failed checks
-analyze :: [Rule] -> Dockerfile -> [RuleCheck]
-analyze list dockerfile =
-  [ result -- Keep the result
-    | rule <- list, -- for each rule in the list
-      result <- rule parsedFile, -- after applying the rule to the file
-      notIgnored result -- and only keep failures that were not ignored
-  ]
-  where
-    notIgnored RuleCheck {metadata = Metadata {code}, linenumber} = not (wasIgnored code linenumber)
-    wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes]
-    allIgnores = ignored dockerfile
-    parsedFile = map (fmap Shell.parseShell) dockerfile
-
-ignored :: Dockerfile -> [(Linenumber, [Text.Text])]
-ignored dockerfile =
-  [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]
-  where
-    extractIgnored = ignoreFromInstruction . instruction
-    ignoreFromInstruction (Comment comment) = parseComment comment
-    ignoreFromInstruction _ = Nothing
-    parseComment :: Text.Text -> Maybe [Text.Text]
-    parseComment = Megaparsec.parseMaybe commentParser
-    commentParser :: IgnoreRuleParser [Text.Text]
-    commentParser =
-      spaces
-        >> string "hadolint" -- The parser for the ignored rules
-        >> spaces1
-        >> string "ignore="
-        >> spaces
-        >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)
-    string = Megaparsec.string
-    spaces = Megaparsec.takeWhileP Nothing space
-    spaces1 = Megaparsec.takeWhile1P Nothing space
-    space c = c == ' ' || c == '\t'
-    ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` ("DLSC0123456789" :: String))
-
-rules :: [Rule]
-rules =
-  [ absoluteWorkdir,
-    shellcheck,
-    invalidCmd,
-    copyInsteadAdd,
-    copyEndingSlash,
-    copyFromExists,
-    copyFromAnother,
-    fromAliasUnique,
-    noRootUser,
-    noCd,
-    noSudo,
-    noAptGetUpgrade,
-    noApkUpgrade,
-    noLatestTag,
-    noUntagged,
-    noPlatformFlag,
-    aptGetVersionPinned,
-    aptGetCleanup,
-    apkAddVersionPinned,
-    apkAddNoCache,
-    useAdd,
-    pipVersionPinned,
-    npmVersionPinned,
-    invalidPort,
-    aptGetNoRecommends,
-    aptGetYes,
-    wgetOrCurl,
-    hasNoMaintainer,
-    multipleCmds,
-    multipleEntrypoints,
-    useShell,
-    useJsonArgs,
-    usePipefail,
-    noApt,
-    gemVersionPinned,
-    yumYes,
-    noYumUpdate,
-    yumCleanup,
-    yumVersionPinned,
-    zypperYes,
-    noZypperUpdate,
-    zypperCleanup,
-    zypperVersionPinned,
-    dnfYes,
-    noDnfUpdate,
-    dnfCleanup,
-    dnfVersionPinned,
-    pipNoCacheDir,
-    noIllegalInstructionInOnbuild,
-    noSelfreferencingEnv
-  ]
-
-optionalRules :: RulesConfig -> [Rule]
-optionalRules RulesConfig {allowedRegistries} = [registryIsAllowed allowedRegistries]
-
-allFromImages :: ParsedFile -> [(Linenumber, BaseImage)]
-allFromImages dockerfile = [(l, f) | (l, From f) <- instr]
-  where
-    instr = fmap (lineNumber &&& instruction) dockerfile
-
-allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)]
-allAliasedImages dockerfile =
-  [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)]
-  where
-    extractAlias (l, f) = (l, fromAlias f)
-
-allImageNames :: ParsedFile -> [(Linenumber, Text.Text)]
-allImageNames dockerfile = [(l, fromName baseImage) | (l, baseImage) <- allFromImages dockerfile]
-
--- | Returns a list of all image aliases in FROM instructions that
---  are defined before the given line number.
-previouslyDefinedAliases :: Linenumber -> ParsedFile -> [Text.Text]
-previouslyDefinedAliases line dockerfile =
-  [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line]
-
--- | Returns the result of running the check function on the image alias
---   name, if the passed instruction is a FROM instruction with a stage alias.
---   Otherwise, returns True.
-aliasMustBe :: (Text.Text -> Bool) -> Instruction a -> Bool
-aliasMustBe predicate fromInstr =
-  case fromInstr of
-    From BaseImage {alias = Just (ImageAlias as)} -> predicate as
-    _ -> True
-
-fromName :: BaseImage -> Text.Text
-fromName BaseImage {image = Image {imageName}} = imageName
-
-fromAlias :: BaseImage -> Maybe ImageAlias
-fromAlias BaseImage {alias} = alias
-
--------------
---  RULES  --
--------------
-shellcheck :: Rule
-shellcheck = mapInstructions check Shell.defaultShellOpts
-  where
-    check :: CheckerWithState Shell.ShellOpts
-    check _ _ (From _) = (Shell.defaultShellOpts, []) -- Reset the state
-    check st _ (Arg name _) = (Shell.addVars [name] st, [])
-    check st _ (Env pairs) = (Shell.addVars (map fst pairs) st, [])
-    check st _ (Shell (ArgumentsList script)) = (Shell.setShell (Shell.original script) st, [])
-    check st _ (Shell (ArgumentsText script)) = (Shell.setShell (Shell.original script) st, [])
-    check st _ (Run (RunArgs (ArgumentsList script) _)) = (st, doCheck st script)
-    check st _ (Run (RunArgs (ArgumentsText script) _)) = (st, doCheck st script)
-    check st _ _ = (st, [])
-    doCheck opts script = nub [commentMetadata c | c <- Shell.shellcheck opts script]
-
--- | Converts ShellCheck errors into our own errors type
-commentMetadata :: ShellCheck.Interface.PositionedComment -> Metadata
-commentMetadata c =
-  Metadata (Text.pack ("SC" ++ show (code c))) (getDLSeverity $ severity c) (Text.pack (message c))
-  where
-    getDLSeverity :: Severity -> DLSeverity
-    getDLSeverity s =
-      case s of
-        WarningC -> DLWarningC
-        InfoC -> DLInfoC
-        StyleC -> DLStyleC
-        _ -> DLErrorC
-    severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc
-    code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc
-    message pc = ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc
-
-absoluteWorkdir :: Rule
-absoluteWorkdir = instructionRule code severity message check
-  where
-    code = "DL3000"
-    severity = DLErrorC
-    message = "Use absolute WORKDIR"
-    check (Workdir loc)
-      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
-      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True
-      | otherwise = False
-    check _ = True
-    dropQuotes chr
-      | chr == '\"' = True
-      | chr == '\'' = True
-      | otherwise = False
-
-hasNoMaintainer :: Rule
-hasNoMaintainer = instructionRule code severity message check
-  where
-    code = "DL4000"
-    severity = DLErrorC
-    message = "MAINTAINER is deprecated"
-    check (Maintainer _) = False
-    check _ = True
-
--- Check if a command contains a program call in the Run instruction
-usingProgram :: Text.Text -> Shell.ParsedShell -> Bool
-usingProgram prog args = not $ null [cmd | cmd <- Shell.findCommandNames args, cmd == prog]
-
-multipleCmds :: Rule
-multipleCmds = instructionRuleState code severity message check False
-  where
-    code = "DL4003"
-    severity = DLWarningC
-    message =
-      "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \
-      \`CMD` will take effect"
-    check _ _ From {} = withState False True -- Reset the state each time we find a FROM
-    check st _ Cmd {} = withState True (not st) -- Remember we found a CMD, fail if we found a CMD before
-    check st _ _ = withState st True
-
-multipleEntrypoints :: Rule
-multipleEntrypoints = instructionRuleState code severity message check False
-  where
-    code = "DL4004"
-    severity = DLErrorC
-    message =
-      "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \
-      \only the last `ENTRYPOINT` will take effect"
-    check _ _ From {} = withState False True -- Reset the state each time we find a FROM
-    check st _ Entrypoint {} = withState True (not st) -- Remember we found an ENTRYPOINT
-    -- and fail if we found another one before
-    check st _ _ = withState st True
-
-wgetOrCurl :: Rule
-wgetOrCurl = instructionRuleState code severity message check Set.empty
-  where
-    code = "DL4001"
-    severity = DLWarningC
-    message = "Either use Wget or Curl but not both"
-    check state _ (Run (RunArgs args _)) = argumentsRule (detectDoubleUsage state) args
-    check _ _ (From _) = withState Set.empty True -- Reset the state for each stage
-    check state _ _ = withState state True
-    detectDoubleUsage state args =
-      let newArgs = extractCommands args
-          newState = Set.union state newArgs
-       in withState newState (Set.null newArgs || Set.size newState < 2)
-    extractCommands args =
-      Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]
-
-invalidCmd :: Rule
-invalidCmd = instructionRule code severity message check
-  where
-    code = "DL3001"
-    severity = DLInfoC
-    message =
-      "For some bash commands it makes no sense running them in a Docker container like `ssh`, \
-      \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"
-    check (Run (RunArgs args _)) = argumentsRule detectInvalid args
-    check _ = True
-    detectInvalid args = null [arg | arg <- Shell.findCommandNames args, arg `elem` invalidCmds]
-    invalidCmds = ["ssh", "vim", "shutdown", "service", "ps", "free", "top", "kill", "mount"]
-
-noRootUser :: Rule
-noRootUser dockerfile = instructionRuleState code severity message check Nothing dockerfile
-  where
-    code = "DL3002"
-    severity = DLWarningC
-    message = "Last USER should not be root"
-    check _ _ (From from) = withState (Just from) True -- Remember the last FROM instruction found
-    check st@(Just from) line (User user)
-      | isRoot user && lastUserIsRoot from line = withState st False
-      | otherwise = withState st True
-    check st _ _ = withState st True
-    --
-    --
-    lastUserIsRoot from line = Map.lookup from rootStages == Just line
-    --
-    --
-    rootStages :: Map.Map BaseImage Linenumber
-    rootStages =
-      let indexedInstructions = map (instruction &&& lineNumber) dockerfile
-          (_, usersMap) = foldl' buildMap (Nothing, Map.empty) indexedInstructions
-       in usersMap
-    --
-    --
-    buildMap (_, st) (From from, _) = (Just from, st) -- Remember the FROM we are currently inspecting
-    buildMap (Just from, st) (User user, line)
-      | isRoot user = (Just from, Map.insert from line st) -- Remember the line with a root user
-      | otherwise = (Just from, Map.delete from st) -- Forget there was a root used for this FROM
-    buildMap st _ = st
-    --
-    --
-    isRoot user =
-      Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"
-
-noCd :: Rule
-noCd = instructionRule code severity message check
-  where
-    code = "DL3003"
-    severity = DLWarningC
-    message = "Use WORKDIR to switch to a directory"
-    check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "cd") args
-    check _ = True
-
-noSudo :: Rule
-noSudo = instructionRule code severity message check
-  where
-    code = "DL3004"
-    severity = DLErrorC
-    message =
-      "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \
-      \root"
-    check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "sudo") args
-    check _ = True
-
-noAptGetUpgrade :: Rule
-noAptGetUpgrade = instructionRule code severity message check
-  where
-    code = "DL3005"
-    severity = DLErrorC
-    message = "Do not use apt-get upgrade or dist-upgrade"
-    check (Run (RunArgs args _)) =
-      argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade", "dist-upgrade"])) args
-    check _ = True
-
-noUntagged :: Rule
-noUntagged dockerfile = instructionRuleLine code severity message check dockerfile
-  where
-    code = "DL3006"
-    severity = DLWarningC
-    message = "Always tag the version of an image explicitly"
-    check _ (From BaseImage {image = (Image _ "scratch")}) = True
-    check _ (From BaseImage {digest = Just _}) = True
-    check line (From BaseImage {image = (Image _ i), tag = Nothing}) =
-      i `elem` previouslyDefinedAliases line dockerfile
-    check _ _ = True
-
-noLatestTag :: Rule
-noLatestTag = instructionRule code severity message check
-  where
-    code = "DL3007"
-    severity = DLWarningC
-    message =
-      "Using latest is prone to errors if the image will ever update. Pin the version explicitly \
-      \to a release tag"
-    check (From BaseImage {tag = Just t}) = t /= "latest"
-    check _ = True
-
-aptGetVersionPinned :: Rule
-aptGetVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3008"
-    severity = DLWarningC
-    message =
-      "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \
-      \install <package>=<version>`"
-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . aptGetPackages) args
-    check _ = True
-    versionFixed package = "=" `Text.isInfixOf` package || ("/" `Text.isInfixOf` package || ".deb" `Text.isSuffixOf` package)
-
-aptGetPackages :: Shell.ParsedShell -> [Text.Text]
-aptGetPackages args =
-  [ arg
-    | cmd <- Shell.presentCommands args,
-      Shell.cmdHasArgs "apt-get" ["install"] cmd,
-      arg <- Shell.getArgsNoFlags (dropTarget cmd),
-      arg /= "install"
-  ]
-  where
-    dropTarget = Shell.dropFlagArg ["t", "target-release"]
-
-aptGetCleanup :: Rule
-aptGetCleanup dockerfile = instructionRuleState code severity message check Nothing dockerfile
-  where
-    code = "DL3009"
-    severity = DLInfoC
-    message = "Delete the apt-get lists after installing something"
-
-    check _ line f@(From _) = withState (Just (line, f)) True -- Remember the last FROM instruction found
-    check st@(Just (line, From baseimage)) _ (Run (RunArgs args _)) =
-      withState st (argumentsRule (didNotForgetToCleanup line baseimage) args)
-    check st _ _ = withState st True
-    -- Check all commands in the script for the presence of apt-get update
-    -- If the command is there, then we need to verify that the user is also removing the lists folder
-    didNotForgetToCleanup line baseimage args
-      | not (hasUpdate args) || not (imageIsUsed line baseimage) = True
-      | otherwise = hasCleanup args
-    hasCleanup args =
-      any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)
-    hasUpdate args = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)
-    imageIsUsed line baseimage = isLastImage line baseimage || imageIsUsedLater line baseimage
-    isLastImage line baseimage =
-      case reverse (allFromImages dockerfile) of
-        lst : _ -> (line, baseimage) == lst
-        _ -> True
-    imageIsUsedLater line baseimage =
-      case fromAlias baseimage of
-        Nothing -> True
-        Just (ImageAlias alias) ->
-          alias `elem` [i | (l, i) <- allImageNames dockerfile, l > line]
-
-noApkUpgrade :: Rule
-noApkUpgrade = instructionRule code severity message check
-  where
-    code = "DL3017"
-    severity = DLErrorC
-    message = "Do not use apk upgrade"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apk" ["upgrade"])) args
-    check _ = True
-
-apkAddVersionPinned :: Rule
-apkAddVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3018"
-    severity = DLWarningC
-    message =
-      "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
-    check (Run (RunArgs args _)) = argumentsRule (\as -> and [versionFixed p | p <- apkAddPackages as]) args
-    check _ = True
-    versionFixed package = "=" `Text.isInfixOf` package
-
-apkAddPackages :: Shell.ParsedShell -> [Text.Text]
-apkAddPackages args =
-  [ arg
-    | cmd <- Shell.presentCommands args,
-      Shell.cmdHasArgs "apk" ["add"] cmd,
-      arg <- Shell.getArgsNoFlags (dropTarget cmd),
-      arg /= "add"
-  ]
-  where
-    dropTarget = Shell.dropFlagArg ["t", "virtual", "repository", "X"]
-
-apkAddNoCache :: Rule
-apkAddNoCache = instructionRule code severity message check
-  where
-    code = "DL3019"
-    severity = DLInfoC
-    message =
-      "Use the `--no-cache` switch to avoid the need to use `--update` and remove \
-      \`/var/cache/apk/*` when done installing packages"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotCacheOption) args
-    check _ = True
-    forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd && not (Shell.hasFlag "no-cache" cmd)
-
-useAdd :: Rule
-useAdd = instructionRule code severity message check
-  where
-    code = "DL3010"
-    severity = DLInfoC
-    message = "Use ADD for extracting archives into an image"
-    check (Copy (CopyArgs srcs _ _ _)) =
-      and
-        [ not (format `Text.isSuffixOf` src)
-          | SourcePath src <- toList srcs,
-            format <- archiveFormats
-        ]
-    check _ = True
-    archiveFormats =
-      [ ".tar",
-        ".tar.bz2",
-        ".tb2",
-        ".tbz",
-        ".tbz2",
-        ".tar.gz",
-        ".tgz",
-        ".tpz",
-        ".tar.lz",
-        ".tar.lzma",
-        ".tlz",
-        ".tar.xz",
-        ".txz",
-        ".tar.Z",
-        ".tZ"
-      ]
-
-invalidPort :: Rule
-invalidPort = instructionRule code severity message check
-  where
-    code = "DL3011"
-    severity = DLErrorC
-    message = "Valid UNIX ports range from 0 to 65535"
-    check (Expose (Ports ports)) =
-      and [p <= 65535 | Port p _ <- ports]
-        && and [l <= 65535 && m <= 65535 | PortRange l m _ <- ports]
-    check _ = True
-
-pipVersionPinned :: Rule
-pipVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3013"
-    severity = DLWarningC
-    message =
-      "Pin versions in pip. Instead of `pip install <package>` use `pip install \
-      \<package>==<version>` or `pip install --requirement <requirements file>`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args
-    check _ = True
-    forgotToPinVersion cmd =
-      isPipInstall' cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))
-    -- Check if the command is a pip* install command, and that specific packages are being listed
-    isPipInstall' cmd =
-      (isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))) && not (requirementInstall cmd)
-    -- If the user is installing requirements from a file or just the local module, then we are not interested
-    -- in running this rule
-    requirementInstall cmd =
-      ["--requirement"] `isInfixOf` Shell.getArgs cmd
-        || ["-r"] `isInfixOf` Shell.getArgs cmd
-        || ["."] `isInfixOf` Shell.getArgs cmd
-    hasBuildConstraint cmd = Shell.hasFlag "constraint" cmd || Shell.hasFlag "c" cmd
-    packages cmd =
-      stripInstallPrefix $
-        Shell.getArgsNoFlags $
-          Shell.dropFlagArg
-            [ "abi",
-              "b",
-              "build",
-              "e",
-              "editable",
-              "extra-index-url",
-              "f",
-              "find-links",
-              "i",
-              "index-url",
-              "implementation",
-              "no-binary",
-              "only-binary",
-              "platform",
-              "prefix",
-              "progress-bar",
-              "proxy",
-              "python-version",
-              "root",
-              "src",
-              "t",
-              "target",
-              "trusted-host",
-              "upgrade-strategy"
-            ]
-            cmd
-    versionFixed package = hasVersionSymbol package || isVersionedGit package || isLocalPackage package
-    isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package
-    versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]
-    hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols]
-    localPackageFileExtensions = [".whl", ".tar.gz"]
-    isLocalPackage package = or [s `Text.isSuffixOf` package | s <- localPackageFileExtensions]
-
-stripInstallPrefix :: [Text.Text] -> [Text.Text]
-stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
-
--- |
---  Rule for pinning NPM packages to version, tag, or commit
---  supported formats by Hadolint
---    npm install (with no args, in package dir)
---    npm install [<@scope>/]<name>
---    npm install [<@scope>/]<name>@<tag>
---    npm install [<@scope>/]<name>@<version>
---    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]
---    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>]
-npmVersionPinned :: Rule
-npmVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3016"
-    severity = DLWarningC
-    message =
-      "Pin versions in npm. Instead of `npm install <package>` use `npm install \
-      \<package>@<version>`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args
-    check _ = True
-    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)
-    versionFixed package
-      | hasGitPrefix package = isVersionedGit package
-      | hasTarballSuffix package = True
-      | isFolder package = True
-      | otherwise = hasVersionSymbol package
-    gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
-    hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]
-    tarballSuffixes = [".tar", ".tar.gz", ".tgz"]
-    hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]
-    pathPrefixes = ["/", "./", "../", "~/"]
-    isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]
-    isVersionedGit package = "#" `Text.isInfixOf` package
-    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
-      where
-        dropScope pkg =
-          if "@" `Text.isPrefixOf` pkg
-            then Text.dropWhile ('/' <) pkg
-            else pkg
-
-aptGetYes :: Rule
-aptGetYes = instructionRule code severity message check
-  where
-    code = "DL3014"
-    severity = DLWarningC
-    message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotAptYesOption) args
-    check _ = True
-    forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)
-    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
-    hasYesOption = Shell.hasAnyFlag ["y", "yes", "q", "assume-yes"]
-
-aptGetNoRecommends :: Rule
-aptGetNoRecommends = instructionRule code severity message check
-  where
-    code = "DL3015"
-    severity = DLInfoC
-    message = "Avoid additional packages by specifying `--no-install-recommends`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotNoInstallRecommends) args
-    check _ = True
-    forgotNoInstallRecommends cmd = isAptGetInstall cmd && not (disablesRecommendOption cmd)
-    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
-    disablesRecommendOption cmd =
-      Shell.hasFlag "no-install-recommends" cmd
-        || Shell.hasArg "APT::Install-Recommends=false" cmd
-
-isArchive :: Text.Text -> Bool
-isArchive path =
-  or
-    ( [ ftype `Text.isSuffixOf` path
-        | ftype <-
-            [ ".tar",
-              ".gz",
-              ".bz2",
-              ".xz",
-              ".zip",
-              ".tgz",
-              ".tb2",
-              ".tbz",
-              ".tbz2",
-              ".lz",
-              ".lzma",
-              ".tlz",
-              ".txz",
-              ".Z",
-              ".tZ"
-            ]
-      ]
-    )
-
-isUrl :: Text.Text -> Bool
-isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])
-
-copyInsteadAdd :: Rule
-copyInsteadAdd = instructionRule code severity message check
-  where
-    code = "DL3020"
-    severity = DLErrorC
-    message = "Use COPY instead of ADD for files and folders"
-    check (Add (AddArgs srcs _ _)) =
-      and [isArchive src || isUrl src | SourcePath src <- toList srcs]
-    check _ = True
-
-copyEndingSlash :: Rule
-copyEndingSlash = instructionRule code severity message check
-  where
-    code = "DL3021"
-    severity = DLErrorC
-    message = "COPY with more than 2 arguments requires the last argument to end with /"
-    check (Copy (CopyArgs sources t _ _))
-      | length sources > 1 = endsWithSlash t
-      | otherwise = True
-    check _ = True
-    endsWithSlash (TargetPath t) = not (Text.null t) && Text.last t == '/'
-
-copyFromExists :: Rule
-copyFromExists dockerfile = instructionRuleLine code severity message check dockerfile
-  where
-    code = "DL3022"
-    severity = DLWarningC
-    message = "COPY --from should reference a previously defined FROM alias"
-    check l (Copy (CopyArgs _ _ _ (CopySource s))) = s `elem` previouslyDefinedAliases l dockerfile
-    check _ _ = True
-
-copyFromAnother :: Rule
-copyFromAnother = instructionRuleState code severity message check Nothing
-  where
-    code = "DL3023"
-    severity = DLErrorC
-    message = "COPY --from should reference a previously defined FROM alias"
-
-    check _ _ f@(From _) = withState (Just f) True -- Remember the last FROM instruction found
-    check st@(Just fromInstr) _ (Copy (CopyArgs _ _ _ (CopySource stageName))) =
-      withState st (aliasMustBe (/= stageName) fromInstr) -- Cannot copy from itself!
-    check state _ _ = withState state True
-
-fromAliasUnique :: Rule
-fromAliasUnique dockerfile = instructionRuleLine code severity message check dockerfile
-  where
-    code = "DL3024"
-    severity = DLErrorC
-    message = "FROM aliases (stage names) must be unique"
-    check line = aliasMustBe (not . alreadyTaken line)
-    alreadyTaken line alias = alias `elem` previouslyDefinedAliases line dockerfile
-
-useShell :: Rule
-useShell = instructionRule code severity message check
-  where
-    code = "DL4005"
-    severity = DLWarningC
-    message = "Use SHELL to change the default shell"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands (Shell.cmdHasArgs "ln" ["/bin/sh"])) args
-    check _ = True
-
-useJsonArgs :: Rule
-useJsonArgs = instructionRule code severity message check
-  where
-    code = "DL3025"
-    severity = DLWarningC
-    message = "Use arguments JSON notation for CMD and ENTRYPOINT arguments"
-    check (Cmd (ArgumentsText _)) = False
-    check (Entrypoint (ArgumentsText _)) = False
-    check _ = True
-
-noApt :: Rule
-noApt = instructionRule code severity message check
-  where
-    code = "DL3027"
-    severity = DLWarningC
-    message =
-      "Do not use apt as it is meant to be a end-user tool, use apt-get or apt-cache instead"
-    check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "apt") args
-    check _ = True
-
-usePipefail :: Rule
-usePipefail = instructionRuleState code severity message check False
-  where
-    code = "DL4006"
-    severity = DLWarningC
-    message =
-      "Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using \
-      \/bin/sh in an alpine image or if your shell is symlinked to busybox then consider \
-      \explicitly setting your SHELL to /bin/ash, or disable this check"
-    check _ _ From {} = (False, True) -- Reset the state each time we find a new FROM
-    check _ _ (Shell args)
-      | argumentsRule isPowerShell args = (True, True)
-      | otherwise = (argumentsRule hasPipefailOption args, True)
-    check False _ (Run (RunArgs args _)) = (False, argumentsRule notHasPipes args)
-    check st _ _ = (st, True)
-    isPowerShell (Shell.ParsedShell orig _ _) = "pwsh" `Text.isPrefixOf` orig
-    notHasPipes script = not (Shell.hasPipes script)
-    hasPipefailOption script =
-      not $
-        null
-          [ True
-            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script,
-              validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"],
-              name == validShell,
-              Shell.hasFlag "o" cmd,
-              arg <- Shell.arg <$> arguments,
-              arg == "pipefail"
-          ]
-
-registryIsAllowed :: Set.Set Registry -> Rule
-registryIsAllowed allowed = instructionRuleState code severity message check Set.empty
-  where
-    code = "DL3026"
-    severity = DLErrorC
-    message = "Use only an allowed registry in the FROM image"
-    check st _ (From BaseImage {image, alias}) = withState (Set.insert alias st) (doCheck st image)
-    check st _ _ = (st, True)
-    toImageAlias = Just . ImageAlias . imageName
-
-    doCheck st img = Set.member (toImageAlias img) st || Set.null allowed || isAllowed img
-    isAllowed Image {registryName = Just registry} = Set.member registry allowed
-    isAllowed Image {registryName = Nothing, imageName} =
-      imageName == "scratch"
-        || Set.member "docker.io" allowed
-        || Set.member "hub.docker.com" allowed
-
-gemVersionPinned :: Rule
-gemVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3028"
-    severity = DLWarningC
-    message =
-      "Pin versions in gem install. Instead of `gem install <gem>` use `gem \
-      \install <gem>:<version>`"
-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . gems) args
-    check _ = True
-    versionFixed package = ":" `Text.isInfixOf` package
-
-noPlatformFlag :: Rule
-noPlatformFlag = instructionRule code severity message check
-  where
-    code = "DL3029"
-    severity = DLWarningC
-    message = "Do not use --platform flag with FROM"
-    check (From BaseImage {platform = Just p}) = p == ""
-    check _ = True
-
-yumYes :: Rule
-yumYes = instructionRule code severity message check
-  where
-    code = "DL3030"
-    severity = DLWarningC
-    message = "Use the -y switch to avoid manual input `yum install -y <package`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotYumYesOption) args
-    check _ = True
-    forgotYumYesOption cmd = isYumInstall cmd && not (hasYesOption cmd)
-    isYumInstall = Shell.cmdHasArgs "yum" ["install", "groupinstall", "localinstall"]
-    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]
-
-noYumUpdate :: Rule
-noYumUpdate = instructionRule code severity message check
-  where
-    code = "DL3031"
-    severity = DLErrorC
-    message = "Do not use yum update."
-    check (Run (RunArgs args _)) =
-      argumentsRule
-        ( Shell.noCommands
-            ( Shell.cmdHasArgs
-                "yum"
-                [ "update",
-                  "update-to",
-                  "upgrade",
-                  "upgrade-to"
-                ]
-            )
-        )
-        args
-    check _ = True
-
-yumCleanup :: Rule
-yumCleanup = instructionRule code severity message check
-  where
-    code = "DL3032"
-    severity = DLWarningC
-    message = "`yum clean all` missing after yum command."
-    check (Run (RunArgs args _)) =
-      argumentsRule (Shell.noCommands yumInstall) args
-        || ( argumentsRule (Shell.anyCommands yumInstall) args
-               && argumentsRule (Shell.anyCommands yumClean) args
-           )
-    check _ = True
-    yumInstall = Shell.cmdHasArgs "yum" ["install"]
-    yumClean = Shell.cmdHasArgs "yum" ["clean", "all"]
-
-yumVersionPinned :: Rule
-yumVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3033"
-    severity = DLWarningC
-    message = "Specify version with `yum install -y <package>-<version>`."
-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . yumPackages) args
-    check _ = True
-    versionFixed package =
-      "-" `Text.isInfixOf` package
-        || ".rpm" `Text.isSuffixOf` package
-
-yumPackages :: Shell.ParsedShell -> [Text.Text]
-yumPackages args =
-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
-  ]
-
-zypperYes :: Rule
-zypperYes = instructionRule code severity message check
-  where
-    code = "DL3034"
-    severity = DLWarningC
-    message = "Non-interactive switch missing from `zypper` command: `zypper install -y`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotZypperYesOption) args
-    check _ = True
-    forgotZypperYesOption cmd = isZypperInstall cmd && not (hasYesOption cmd)
-    isZypperInstall =
-      Shell.cmdHasArgs
-        "zypper"
-        [ "install",
-          "in",
-          "remove",
-          "rm",
-          "source-install",
-          "si",
-          "patch"
-        ]
-    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]
-
-noZypperUpdate :: Rule
-noZypperUpdate = instructionRule code severity message check
-  where
-    code = "DL3035"
-    severity = DLWarningC
-    message = "Do not use `zypper update`."
-    check (Run (RunArgs args _)) =
-      argumentsRule
-        ( Shell.noCommands
-            ( Shell.cmdHasArgs
-                "zypper"
-                [ "update",
-                  "up",
-                  "dist-upgrade",
-                  "dup"
-                ]
-            )
-        )
-        args
-    check _ = True
-
-zypperCleanup :: Rule
-zypperCleanup = instructionRule code severity message check
-  where
-    code = "DL3036"
-    severity = DLWarningC
-    message = "`zypper clean` missing after zypper use."
-    check (Run (RunArgs args _)) =
-      argumentsRule (Shell.noCommands zypperInstall) args
-        || ( argumentsRule (Shell.anyCommands zypperInstall) args
-               && argumentsRule (Shell.anyCommands zypperClean) args
-           )
-    check _ = True
-    zypperInstall = Shell.cmdHasArgs "zypper" ["install", "in"]
-    zypperClean = Shell.cmdHasArgs "zypper" ["clean", "cc"]
-
-zypperVersionPinned :: Rule
-zypperVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3037"
-    severity = DLWarningC
-    message = "Specify version with `zypper install -y <package>=<version>`."
-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . zypperPackages) args
-    check _ = True
-    versionFixed package =
-      "=" `Text.isInfixOf` package
-        || ">=" `Text.isInfixOf` package
-        || ">" `Text.isInfixOf` package
-        || "<=" `Text.isInfixOf` package
-        || "<" `Text.isInfixOf` package
-        || ".rpm" `Text.isSuffixOf` package
-
-zypperPackages :: Shell.ParsedShell -> [Text.Text]
-zypperPackages args =
-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "zypper" ["install", "in"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install", arg /= "in"
-  ]
-
-dnfYes :: Rule
-dnfYes = instructionRule code severity message check
-  where
-    code = "DL3038"
-    severity = DLWarningC
-    message = "Use the -y switch to avoid manual input `dnf install -y <package`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotDnfYesOption) args
-    check _ = True
-    forgotDnfYesOption cmd = isDnfInstall cmd && not (hasYesOption cmd)
-    isDnfInstall = Shell.cmdHasArgs "dnf" ["install", "groupinstall", "localinstall"]
-    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]
-
-noDnfUpdate :: Rule
-noDnfUpdate = instructionRule code severity message check
-  where
-    code = "DL3039"
-    severity = DLErrorC
-    message = "Do not use dnf update."
-    check (Run (RunArgs args _)) =
-      argumentsRule
-        ( Shell.noCommands
-            ( Shell.cmdHasArgs
-                "dnf"
-                [ "upgrade",
-                  "upgrade-minimal"
-                ]
-            )
-        )
-        args
-    check _ = True
-
-dnfCleanup :: Rule
-dnfCleanup = instructionRule code severity message check
-  where
-    code = "DL3040"
-    severity = DLWarningC
-    message = "`dnf clean all` missing after dnf command."
-    check (Run (RunArgs args _)) =
-      argumentsRule (Shell.noCommands dnfInstall) args
-        || ( argumentsRule (Shell.anyCommands dnfInstall) args
-               && argumentsRule (Shell.anyCommands dnfClean) args
-           )
-    check _ = True
-    dnfInstall = Shell.cmdHasArgs "dnf" ["install"]
-    dnfClean = Shell.cmdHasArgs "dnf" ["clean", "all"]
-
-dnfVersionPinned :: Rule
-dnfVersionPinned = instructionRule code severity message check
-  where
-    code = "DL3041"
-    severity = DLWarningC
-    message = "Specify version with `dnf install -y <package>-<version>`."
-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . dnfPackages) args
-    check _ = True
-    versionFixed package =
-      "-" `Text.isInfixOf` package
-        || ".rpm" `Text.isSuffixOf` package
-
-dnfPackages :: Shell.ParsedShell -> [Text.Text]
-dnfPackages args =
-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "dnf" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"
-  ]
-
-pipNoCacheDir :: Rule
-pipNoCacheDir = instructionRule code severity message check
-  where
-    code = "DL3042"
-    severity = DLWarningC
-    message =
-      "Avoid use of cache directory with pip. Use `pip install --no-cache-dir <package>`"
-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotNoCacheDir) args
-    check _ = True
-    forgotNoCacheDir cmd =
-      isPipInstall cmd && not (usesNoCacheDir cmd) && not (isPipWrapper cmd)
-    usesNoCacheDir cmd = "--no-cache-dir" `elem` Shell.getArgs cmd
-
-isPipInstall :: Shell.Command -> Bool
-isPipInstall cmd@(Shell.Command name _ _) = isStdPipInstall || isPythonPipInstall
-  where
-    isStdPipInstall =
-      "pip" `Text.isPrefixOf` name
-        && ["install"] `isInfixOf` Shell.getArgs cmd
-    isPythonPipInstall =
-      "python" `Text.isPrefixOf` name
-        && ["-m", "pip", "install"] `isInfixOf` Shell.getArgs cmd
-
-isPipWrapper :: Shell.Command -> Bool
-isPipWrapper cmd@(Shell.Command name _ _) = isWrapper "pipx" || isWrapper "pipenv"
-  where
-    isWrapper :: Text.Text -> Bool
-    isWrapper w =
-      w `Text.isInfixOf` name
-        || ("python" `Text.isPrefixOf` name && ["-m", w] `isInfixOf` Shell.getArgs cmd)
-
-gems :: Shell.ParsedShell -> [Text.Text]
-gems shell =
-  [ arg
-    | cmd <- Shell.presentCommands shell,
-      Shell.cmdHasArgs "gem" ["install", "i"] cmd,
-      not (Shell.cmdHasArgs "gem" ["-v"] cmd),
-      not (Shell.cmdHasArgs "gem" ["--version"] cmd),
-      not (Shell.cmdHasPrefixArg "gem" "--version=" cmd),
-      arg <- Shell.getArgsNoFlags cmd,
-      arg /= "install",
-      arg /= "i",
-      arg /= "--"
-  ]
-
-noIllegalInstructionInOnbuild :: Rule
-noIllegalInstructionInOnbuild = instructionRule code severity message check
-  where
-    code = "DL3043"
-    severity = DLErrorC
-    message = "`ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction."
-    check (OnBuild (OnBuild _)) = False
-    check (OnBuild (From _)) = False
-    check (OnBuild (Maintainer _)) = False
-    check _ = True
-
-noSelfreferencingEnv :: Rule
-noSelfreferencingEnv = instructionRuleState code severity message check Set.empty
-  where
-    code = "DL3044"
-    severity = DLErrorC
-    message = "Do not refer to an environment variable within the same `ENV` statement where it is defined."
-    check st _ (Env pairs) = withState (Set.union st (Set.fromList (map fst pairs))) $
-        null [ env | env <- listOfReferences pairs, env `Set.notMember` st ]
-    check st _ (Arg arg _) = withState (Set.insert arg st) True
-    check st _ _ = withState st True
-
-    -- generates a list of references to variable names referenced on the right
-    -- hand side of a variable definition, except when the variable is
-    -- referenced on its own right hand side.
-    listOfReferences :: Pairs -> [Text.Text]
-    listOfReferences prs = [ var | (idx, (var, _)) <- indexed prs,
-                                   var `isSubstringOfAny` map (snd . snd) (filter ((/= idx) . fst) (indexed prs))]
-    -- is a reference of a variable substring of any text?
-    -- matches ${var_name} and $var_name, but not $var_nameblafoo
-    isSubstringOfAny :: Text.Text -> [Text.Text] -> Bool
-    isSubstringOfAny t l = not $ null [ v | v <- l,
-        (Text.pack "${" <> t <> Text.pack "}") `Text.isInfixOf` v
-        || (t `bareVariableInText` v)]
-
-    -- we find a 'bare' variable with name v in a text, if
-    -- '$v' is in the text at any place and any text following after that
-    -- occurence would terminate a variable name. To determine that, the text t
-    -- is split at every occurence of var, check if '$v' is in the text and if
-    -- any part of the split text would terminate a variable name.
-    bareVariableInText :: Text.Text -> Text.Text -> Bool
-    bareVariableInText v t =
-      let var = Text.pack "$" <> v
-          rest = Text.splitOn var t
-       in var `Text.isInfixOf` t && any terminatesVarName rest
-      where
-        -- x would terminate a variable name if it was appended directly to
-        -- that name
-        terminatesVarName :: Text.Text -> Bool
-        terminatesVarName x = not $ beginsWithAnyOf x varChar
-
-        -- txt begins with any character of String
-        beginsWithAnyOf :: Text.Text -> String -> Bool
-        beginsWithAnyOf txt str = Text.null txt || (Text.head txt `elem` str)
-
-        -- all characters valid in the inner of a shell variable name
-        varChar :: String
-        varChar = ['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z'] ++ ['_']
diff --git a/src/Hadolint/Shell.hs b/src/Hadolint/Shell.hs
--- a/src/Hadolint/Shell.hs
+++ b/src/Hadolint/Shell.hs
@@ -1,13 +1,9 @@
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE PatternSynonyms #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
 module Hadolint.Shell where
 
 import Control.Monad.Writer (Writer, execWriter, tell)
 import Data.Functor.Identity (runIdentity)
-import Data.Maybe (listToMaybe, mapMaybe)
+import Data.List (isInfixOf)
+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
 import qualified Data.Set as Set
 import Data.Text (Text)
 import qualified Data.Text as Text
@@ -19,22 +15,22 @@
 import qualified ShellCheck.Parser
 
 data CmdPart = CmdPart
-  { arg :: !Text,
-    partId :: !Int
+  { arg :: Text,
+    partId :: Int
   }
   deriving (Show)
 
 data Command = Command
-  { name :: !Text.Text,
+  { name :: Text.Text,
     arguments :: [CmdPart],
     flags :: [CmdPart]
   }
   deriving (Show)
 
 data ParsedShell = ParsedShell
-  { original :: !Text.Text,
-    parsed :: !ParseResult,
-    presentCommands :: ![Command]
+  { original :: Text.Text,
+    parsed :: ParseResult,
+    presentCommands :: [Command]
   }
 
 data ShellOpts = ShellOpts
@@ -80,15 +76,14 @@
           csShellTypeOverride = Nothing,
           csMinSeverity = StyleC
         }
-    script = "#!" ++ extractShell sh ++ "\n" ++ printVars ++ Text.unpack txt
+    script = Text.unpack $ "#!" <> extractShell sh <> "\n" <> printVars <> txt
     exclusions =
       [ 2187, -- exclude the warning about the ash shell not being supported
         1090 -- requires a directive (shell comment) that can't be expressed in a Dockerfile
       ]
 
-    extractShell s =
-      maybe "" Text.unpack (listToMaybe . Text.words $ s)
-    printVars = Text.unpack . Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env
+    extractShell s = fromMaybe "" (listToMaybe . Text.words $ s)
+    printVars = Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env
 
 parseShell :: Text.Text -> ParsedShell
 parseShell txt = ParsedShell {original = txt, parsed = parsedResult, presentCommands = commands}
@@ -99,7 +94,7 @@
           (mockedSystemInterface [("", "")])
           newParseSpec
             { psFilename = "", -- There is no filename
-              psScript = "#!/bin/bash\n" ++ Text.unpack txt,
+              psScript = Text.unpack $ "#!/bin/bash\n" <> txt,
               psCheckSourced = False
             }
 
@@ -164,7 +159,7 @@
   where
     mkPart token =
       CmdPart
-        (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)
+        (Text.concat . fmap Text.pack $ ShellCheck.ASTLib.oversimplify token)
         (mkId (ShellCheck.AST.getId token))
     mkId (Id i) = i
 extractAllArgs _ = []
@@ -201,5 +196,28 @@
     idsToDrop = Set.fromList [getValueId fId arguments | CmdPart f fId <- flags, f `elem` flagsToDrop]
     filterdArgs = [arg | arg@(CmdPart _ aId) <- arguments, not (aId `Set.member` idsToDrop)]
 
+-- | given a flag and a command, return list of arguments for that particulat
+-- flag. E.g., if the command is `useradd -u 12345 luser` and this function is
+-- called for the command `u`, it returns ["12345"].
+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]
+
 getValueId :: Int -> [CmdPart] -> Int
 getValueId fId flags = foldl min (maxBound :: Int) $ filter (> fId) $ map partId flags
+
+-- | Check if a command contains a program call in the Run instruction
+usingProgram :: Text.Text -> ParsedShell -> Bool
+usingProgram prog args = not $ null [cmd | cmd <- findCommandNames args, cmd == prog]
+
+isPipInstall :: Command -> Bool
+isPipInstall cmd@(Command name _ _) = isStdPipInstall || isPythonPipInstall
+  where
+    isStdPipInstall =
+      "pip" `Text.isPrefixOf` name
+        && ["install"] `isInfixOf` getArgs cmd
+    isPythonPipInstall =
+      "python" `Text.isPrefixOf` name
+        && ["-m", "pip", "install"] `isInfixOf` getArgs cmd
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
--- a/test/ConfigSpec.hs
+++ b/test/ConfigSpec.hs
@@ -4,8 +4,10 @@
 
 import Control.Monad (unless)
 import qualified Data.ByteString.Char8 as Bytes
+import Data.Map
 import qualified Data.YAML as Yaml
 import Hadolint.Config
+import Hadolint.Rule as Rule
 import Test.HUnit
 import Test.Hspec
 
@@ -20,7 +22,7 @@
               "    - SC1010"
             ]
           override = Just (OverrideConfig (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing)
-          expected = ConfigFile override Nothing Nothing
+          expected = ConfigFile override Nothing Nothing Nothing Nothing
        in assertConfig expected (Bytes.unlines configFile)
 
     it "Parses config with only warning severities" $
@@ -31,7 +33,7 @@
               "    - SC1010"
             ]
           override = Just (OverrideConfig Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing)
-          expected = ConfigFile override Nothing Nothing
+          expected = ConfigFile override Nothing Nothing Nothing Nothing
        in assertConfig expected (Bytes.unlines configFile)
 
     it "Parses config with only info severities" $
@@ -42,7 +44,7 @@
               "    - SC1010"
             ]
           override = Just (OverrideConfig Nothing Nothing (Just ["DL3000", "SC1010"]) Nothing)
-          expected = ConfigFile override Nothing Nothing
+          expected = ConfigFile override Nothing Nothing Nothing Nothing
        in assertConfig expected (Bytes.unlines configFile)
 
     it "Parses config with only style severities" $
@@ -53,7 +55,7 @@
               "    - SC1010"
             ]
           override = Just (OverrideConfig Nothing Nothing Nothing (Just ["DL3000", "SC1010"]))
-          expected = ConfigFile override Nothing Nothing
+          expected = ConfigFile override Nothing Nothing Nothing Nothing
        in assertConfig expected (Bytes.unlines configFile)
 
     it "Parses config with only ignores" $
@@ -62,7 +64,7 @@
               "- DL3000",
               "- SC1010"
             ]
-          expected = ConfigFile Nothing (Just ["DL3000", "SC1010"]) Nothing
+          expected = ConfigFile Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing
        in assertConfig expected (Bytes.unlines configFile)
 
     it "Parses config with only trustedRegistries" $
@@ -71,9 +73,23 @@
               "- hub.docker.com",
               "- my.shady.xyz"
             ]
-          expected = ConfigFile Nothing Nothing (Just ["hub.docker.com", "my.shady.xyz"])
+          expected = ConfigFile Nothing Nothing (Just ["hub.docker.com", "my.shady.xyz"]) Nothing Nothing
        in assertConfig expected (Bytes.unlines configFile)
 
+    it "Parses config with only label-schema" $
+      let configFile =
+            [ "label-schema:",
+              "  author: text",
+              "  url: url"
+            ]
+          expected = ConfigFile Nothing Nothing Nothing (Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])) Nothing
+       in assertConfig expected (Bytes.unlines configFile)
+
+    it "Parses config with only label-schema" $
+      let configFile = [ "strict-labels: true" ]
+          expected = ConfigFile Nothing Nothing Nothing Nothing (Just True)
+       in assertConfig expected (Bytes.unlines configFile)
+
     it "Parses full file" $
       let configFile =
             [ "override:",
@@ -90,10 +106,16 @@
               "- hub.docker.com",
               "",
               "ignored:",
-              "- DL3000"
+              "- DL3000",
+              "",
+              "strict-labels: false",
+              "label-schema:",
+              "  author: text",
+              "  url: url"
             ]
           override = Just (OverrideConfig (Just ["DL3001"]) (Just ["DL3003"]) (Just ["DL3002"]) (Just ["DL3004"]))
-          expected = ConfigFile override (Just ["DL3000"]) (Just ["hub.docker.com"])
+          labelschema = Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])
+          expected = ConfigFile override (Just ["DL3000"]) (Just ["hub.docker.com"]) labelschema (Just False)
        in assertConfig expected (Bytes.unlines configFile)
 
 assertConfig :: HasCallStack => ConfigFile -> Bytes.ByteString -> Assertion
diff --git a/test/ShellSpec.hs b/test/ShellSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ShellSpec.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module ShellSpec where
+
+import Data.Text as Text
+import Hadolint.Shell
+import Test.Hspec
+
+tests :: SpecWith ()
+tests =
+  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 (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` []
+  --
+    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
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,1533 +1,1998 @@
-{-# LANGUAGE OverloadedLists #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-import qualified ConfigSpec
-import Control.Monad (unless, when)
-import qualified Data.Text as Text
-import Hadolint.Formatter.TTY (formatChecks, formatError)
-import Hadolint.Rules
-import Language.Docker.Parser
-import Language.Docker.Syntax
-import Test.HUnit hiding (Label)
-import Test.Hspec
-
-main :: IO ()
-main =
-  hspec $ do
-    describe "FROM rules" $ do
-      it "no untagged" $ ruleCatches noUntagged "FROM debian"
-      it "no untagged with name" $ ruleCatches noUntagged "FROM debian AS builder"
-      it "explicit latest" $ ruleCatches noLatestTag "FROM debian:latest"
-      it "explicit latest with name" $ ruleCatches noLatestTag "FROM debian:latest AS builder"
-      it "explicit tagged" $ ruleCatchesNot noLatestTag "FROM debian:jessie"
-      it "explicit platform flag" $ ruleCatches noPlatformFlag "FROM --platform=linux debian:jessie"
-      it "no platform flag" $ ruleCatchesNot noPlatformFlag "FROM debian:jessie"
-      it "explicit SHA" $
-        ruleCatchesNot
-          noLatestTag
-          "FROM hub.docker.io/debian@sha256:\
-          \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"
-      it "explicit tagged with name" $
-        ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"
-      it "untagged digest is not an error" $
-        ruleCatchesNot noUntagged "FROM ruby@sha256:f1dbca0f5dbc9"
-      it "untagged digest is not an error" $
-        ruleCatchesNot noUntagged "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 noUntagged $ Text.unlines dockerFile
-              onBuildRuleCatchesNot noUntagged $ 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 noUntagged $ Text.unlines dockerFile
-              onBuildRuleCatches noUntagged $ Text.unlines dockerFile
-    --
-    describe "no root or sudo rules" $ do
-      it "sudo" $ do
-        ruleCatches noSudo "RUN sudo apt-get update"
-        onBuildRuleCatches noSudo "RUN sudo apt-get update"
-
-      it "last user should not be root" $
-        let dockerFile =
-              [ "FROM scratch",
-                "USER root"
-              ]
-         in ruleCatches noRootUser $ Text.unlines dockerFile
-
-      it "no root" $
-        let dockerFile =
-              [ "FROM scratch",
-                "USER foo"
-              ]
-         in ruleCatchesNot noRootUser $ Text.unlines dockerFile
-
-      it "no root UID" $
-        let dockerFile =
-              [ "FROM scratch",
-                "USER 0"
-              ]
-         in ruleCatches noRootUser $ Text.unlines dockerFile
-
-      it "no root:root" $
-        let dockerFile =
-              [ "FROM scratch",
-                "USER root:root"
-              ]
-         in ruleCatches noRootUser $ Text.unlines dockerFile
-
-      it "no UID:GID" $
-        let dockerFile =
-              [ "FROM scratch",
-                "USER 0:0"
-              ]
-         in ruleCatches noRootUser $ Text.unlines dockerFile
-
-      it "can switch back to non root" $
-        let dockerFile =
-              [ "FROM scratch",
-                "USER root",
-                "RUN something",
-                "USER foo"
-              ]
-         in ruleCatchesNot noRootUser $ 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 noRootUser $ 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 noRootUser $ 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 noRootUser $ Text.unlines dockerFile
-
-      it "install sudo" $ do
-        ruleCatchesNot noSudo "RUN apt-get install sudo"
-        onBuildRuleCatchesNot noSudo "RUN apt-get install sudo"
-      it "sudo chained programs" $ do
-        ruleCatches noSudo "RUN apt-get update && sudo apt-get install"
-        onBuildRuleCatches noSudo "RUN apt-get update && sudo apt-get install"
-    --
-    describe "invalid CMD rules" $ do
-      it "invalid cmd" $ do
-        ruleCatches invalidCmd "RUN top"
-        onBuildRuleCatches invalidCmd "RUN top"
-      it "install ssh" $ do
-        ruleCatchesNot invalidCmd "RUN apt-get install ssh"
-        onBuildRuleCatchesNot invalidCmd "RUN apt-get install ssh"
-    --
-    describe "gem" $
-      describe "version pinning" $ do
-        describe "i" $ do
-          it "unpinned" $ do
-            ruleCatches gemVersionPinned "RUN gem i bundler"
-            onBuildRuleCatches gemVersionPinned "RUN gem i bundler"
-          it "pinned" $ do
-            ruleCatchesNot gemVersionPinned "RUN gem i bundler:1"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem i bundler:1"
-          it "multi" $ do
-            ruleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"
-            onBuildRuleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"
-            ruleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogirii:1"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogiri:1"
-        describe "install" $ do
-          it "unpinned" $ do
-            ruleCatches gemVersionPinned "RUN gem install bundler"
-            onBuildRuleCatches gemVersionPinned "RUN gem install bundler"
-          it "pinned" $ do
-            ruleCatchesNot gemVersionPinned "RUN gem install bundler:1"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:1"
-          it "does not warn on -v" $ do
-            ruleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"
-          it "does not warn on --version without =" $ do
-            ruleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"
-          it "does not warn on --version with =" $ do
-            ruleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"
-          it "does not warn on extra flags" $ do
-            ruleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
-            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"
-    --
-    describe "yum rules" $ do
-        it "yum update" $ do
-            ruleCatches noYumUpdate "RUN yum update"
-            ruleCatchesNot noYumUpdate "RUN yum install -y httpd-2.4.42 && yum clean all"
-            ruleCatchesNot noYumUpdate "RUN bash -c `# not even a yum command`"
-            onBuildRuleCatches noYumUpdate "RUN yum update"
-            onBuildRuleCatchesNot noYumUpdate "RUN yum install -y httpd-2.4.42 && yum clean all"
-            onBuildRuleCatchesNot noYumUpdate "RUN bash -c `# not even a yum command`"
-        it "yum version pinning" $ do
-            ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"
-            ruleCatchesNot yumVersionPinned "RUN yum install -y tomcat-9.2 && yum clean all"
-            ruleCatchesNot yumVersionPinned "RUN bash -c `# not even a yum command`"
-            onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"
-            onBuildRuleCatchesNot yumVersionPinned "RUN yum install -y tomcat-9.2 && yum clean all"
-            onBuildRuleCatchesNot yumVersionPinned "RUN bash -c `# not even a yum command`"
-        it "yum no clean all" $ do
-            ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"
-            ruleCatchesNot yumCleanup "RUN yum install -y mariadb-10.4 && yum clean all"
-            ruleCatchesNot yumCleanup "RUN bash -c `# not even a yum command`"
-            onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"
-            onBuildRuleCatchesNot yumCleanup "RUN yum install -y mariadb-10.4 && yum clean all"
-            onBuildRuleCatchesNot yumCleanup "RUN bash -c `# not even a yum command`"
-        it "yum non-interactive" $ do
-            ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"
-            ruleCatchesNot yumYes "RUN yum install -y httpd-2.4.24 && yum clean all"
-            ruleCatchesNot yumYes "RUN bash -c `# not even a yum command`"
-            onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"
-            onBuildRuleCatchesNot yumYes "RUN yum install -y httpd-2.4.24 && yum clean all"
-            onBuildRuleCatchesNot yumYes "RUN bash -c `# not even a yum command`"
-    --
-    describe "zypper rules" $ do
-        it "zupper update" $ do
-            ruleCatches noZypperUpdate "RUN zypper update"
-            ruleCatches noZypperUpdate "RUN zypper up"
-            ruleCatches noZypperUpdate "RUN zypper dist-upgrade"
-            ruleCatches noZypperUpdate "RUN zypper dup"
-            onBuildRuleCatches noZypperUpdate "RUN zypper update"
-            onBuildRuleCatches noZypperUpdate "RUN zypper up"
-            onBuildRuleCatches noZypperUpdate "RUN zypper dist-upgrade"
-            onBuildRuleCatches noZypperUpdate "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 zypperVersionPinned "RUN zypper install -y tomcat && zypper clean"
-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat=9.0.39 && zypper clean"
-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>=9.0 && zypper clean"
-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>9.0 && zypper clean"
-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<=9.0 && zypper clean"
-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<9.0 && zypper clean"
-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"
-            onBuildRuleCatches zypperVersionPinned "RUN zypper install -y tomcat && zypper clean"
-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat=9.0.39 && zypper clean"
-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>=9.0 && zypper clean"
-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>9.0 && zypper clean"
-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<=9.0 && zypper clean"
-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<9.0 && zypper clean"
-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"
-        it "zypper no clean all" $ do
-            ruleCatches zypperCleanup "RUN zypper install -y mariadb=10.4"
-            ruleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper clean"
-            ruleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper cc"
-            onBuildRuleCatches zypperCleanup "RUN zypper install -y mariadb=10.4"
-            onBuildRuleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper clean"
-            onBuildRuleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper cc"
-        it "zypper non-interactive" $ do
-            ruleCatches zypperYes "RUN zypper install httpd=2.4.24 && zypper clean"
-            ruleCatchesNot zypperYes "RUN zypper install -y httpd=2.4.24 && zypper clean"
-            ruleCatchesNot zypperYes "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"
-            onBuildRuleCatches zypperYes "RUN zypper install httpd=2.4.24 && zypper clean"
-            onBuildRuleCatchesNot zypperYes "RUN zypper install -y httpd=2.4.24 && zypper clean"
-            onBuildRuleCatchesNot zypperYes "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"
-    --
-    describe "dnf rules" $ do
-      it "dnf update" $ do
-        ruleCatches noDnfUpdate "RUN dnf upgrade"
-        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade"
-      it "dnf version pinning" $ do
-        ruleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"
-        ruleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-        onBuildRuleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"
-        onBuildRuleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-      it "dnf no clean all" $ do
-        ruleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"
-        ruleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"
-        onBuildRuleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"
-        onBuildRuleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"
-      it "dnf non-interactive" $ do
-        ruleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"
-        ruleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-    --
-    describe "dnf rules" $ do
-      it "dnf update" $ do
-        ruleCatches noDnfUpdate "RUN dnf upgrade"
-        ruleCatches noDnfUpdate "RUN dnf upgrade-minimal"
-        ruleCatchesNot noDnfUpdate "RUN notdnf upgrade"
-        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade"
-        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade-minimal"
-        onBuildRuleCatchesNot noDnfUpdate "RUN notdnf upgrade"
-      it "dnf version pinning" $ do
-        ruleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"
-        ruleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-        ruleCatchesNot dnfVersionPinned "RUN notdnf install tomcat"
-        onBuildRuleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"
-        onBuildRuleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"
-        onBuildRuleCatchesNot dnfVersionPinned "RUN notdnf install tomcat"
-      it "dnf no clean all" $ do
-        ruleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"
-        ruleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"
-        ruleCatchesNot dnfCleanup "RUN notdnf install mariadb"
-        onBuildRuleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"
-        onBuildRuleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"
-        onBuildRuleCatchesNot dnfCleanup "RUN notdnf install mariadb"
-      it "dnf non-interactive" $ do
-        ruleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"
-        ruleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-        ruleCatchesNot dnfYes "RUN notdnf install httpd"
-        onBuildRuleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"
-        onBuildRuleCatchesNot dnfYes "RUN notdnf install httpd"
-    --
-    describe "apt-get rules" $ do
-      it "apt" $
-        let dockerFile =
-              [ "FROM ubuntu",
-                "RUN apt install python"
-              ]
-         in do
-              ruleCatches noApt $ Text.unlines dockerFile
-              onBuildRuleCatches noApt $ Text.unlines dockerFile
-      it "apt-get upgrade" $ do
-        ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"
-        onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"
-      it "apt-get dist-upgrade" $ do
-        ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get dist-upgrade"
-        onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get dist-upgrade"
-      it "apt-get version pinning" $ do
-        ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"
-        onBuildRuleCatches aptGetVersionPinned "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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatches aptGetCleanup $ 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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetCleanup $ 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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatches aptGetCleanup $ 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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatches aptGetCleanup $ 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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetCleanup $ 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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetCleanup $ 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 aptGetCleanup $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetCleanup $ 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 aptGetVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetVersionPinned $ 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 aptGetVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-
-      it "has deprecated maintainer" $
-        ruleCatches hasNoMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"
-    --
-    describe "apk add rules" $ do
-      it "apk upgrade" $ do
-        ruleCatches noApkUpgrade "RUN apk update && apk upgrade"
-        onBuildRuleCatches noApkUpgrade "RUN apk update && apk upgrade"
-      it "apk add version pinning single" $ do
-        ruleCatches apkAddVersionPinned "RUN apk add flex"
-        onBuildRuleCatches apkAddVersionPinned "RUN apk add flex"
-      it "apk add no version pinning single" $ do
-        ruleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"
-        onBuildRuleCatchesNot apkAddVersionPinned "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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot apkAddVersionPinned $ 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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot apkAddVersionPinned $ 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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatches apkAddVersionPinned $ Text.unlines dockerFile
-      it "apk add with --no-cache" $ do
-        ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"
-        onBuildRuleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"
-      it "apk add without --no-cache" $ do
-        ruleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"
-        onBuildRuleCatchesNot apkAddNoCache "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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot apkAddVersionPinned $ 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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot apkAddVersionPinned $ 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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot apkAddVersionPinned $ 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 apkAddVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile
-    --
-    describe "EXPOSE rules" $ do
-      it "invalid port" $ ruleCatches invalidPort "EXPOSE 80000"
-      it "valid port" $ ruleCatchesNot invalidPort "EXPOSE 60000"
-    --
-    describe "pip pinning" $ do
-      it "pip2 version not pinned" $ do
-        ruleCatches pipVersionPinned "RUN pip2 install MySQL_python"
-        onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"
-      it "pip3 version not pinned" $ do
-        ruleCatches pipVersionPinned "RUN pip3 install MySQL_python"
-        onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"
-      it "pip3 version pinned" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"
-      it "pip3 install from local package" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install mypkg.whl"
-        ruleCatchesNot pipVersionPinned "RUN pip3 install mypkg.tar.gz"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install mypkg.whl"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install mypkg.tar.gz"
-      it "pip install requirements" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
-      it "pip install requirements with long flag" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"
-      it "pip install use setup.py" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install ."
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install ."
-      it "pip version not pinned" $ do
-        ruleCatches pipVersionPinned "RUN pip install MySQL_python"
-        onBuildRuleCatches pipVersionPinned "RUN pip install MySQL_python"
-      it "pip version pinned" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
-      it "pip version pinned with ~= operator" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"
-      it "pip version pinned with === operator" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"
-      it "pip version pinned with flag --ignore-installed" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"
-      it "pip version pinned with flag --build" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"
-      it "pip version pinned with flag --prefix" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"
-      it "pip version pinned with flag --root" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"
-      it "pip version pinned with flag --target" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"
-      it "pip version pinned with flag --trusted-host" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"
-      it "pip version pinned with python -m" $ do
-        ruleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"
-        onBuildRuleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"
-      it "pip version not pinned with python -m" $ do
-        ruleCatches pipVersionPinned "RUN python -m pip install example"
-        onBuildRuleCatches pipVersionPinned "RUN python -m pip install --index-url url example"
-      it "pip install git" $ do
-        ruleCatchesNot
-          pipVersionPinned
-          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
-        onBuildRuleCatchesNot
-          pipVersionPinned
-          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"
-      it "pip install unversioned git" $ do
-        ruleCatches
-          pipVersionPinned
-          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
-        onBuildRuleCatches
-          pipVersionPinned
-          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"
-      it "pip install upper bound" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"
-      it "pip install lower bound" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"
-      it "pip install excluded version" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"
-      it "pip install user directory" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"
-      it "pip install no pip version check" $ do
-        ruleCatchesNot
-          pipVersionPinned
-          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
-        onBuildRuleCatchesNot
-          pipVersionPinned
-          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"
-      it "pip install --index-url" $ do
-        ruleCatchesNot
-          pipVersionPinned
-          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-        onBuildRuleCatchesNot
-          pipVersionPinned
-          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-      it "pip install index-url with -i flag" $ do
-        ruleCatchesNot
-          pipVersionPinned
-          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-        onBuildRuleCatchesNot
-          pipVersionPinned
-          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"
-      it "pip install --index-url with --extra-index-url" $ do
-        ruleCatchesNot
-          pipVersionPinned
-          "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"
-        onBuildRuleCatchesNot
-          pipVersionPinned
-          "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 pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"
-      it "pip install constraints file - long version argument" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"
-      it "pip install constraints file - short version argument" $ do
-        ruleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"
-        onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"
-      it "pip install --index-url with --extra-index-url with basic auth" $ do
-        ruleCatchesNot
-          pipVersionPinned
-          "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
-          pipVersionPinned
-          "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 pipNoCacheDir           "RUN pip2 install MySQL_python"
-        onBuildRuleCatches pipNoCacheDir    "RUN pip2 install MySQL_python"
-      it "pip3 --no-cache-dir not used" $ do
-        ruleCatches pipNoCacheDir           "RUN pip3 install MySQL_python"
-        onBuildRuleCatches pipNoCacheDir    "RUN pip3 install MySQL_python"
-      it "pip --no-cache-dir not used" $ do
-        ruleCatches pipNoCacheDir           "RUN pip install MySQL_python"
-        onBuildRuleCatches pipNoCacheDir    "RUN pip install MySQL_python"
-      it "pip2 --no-cache-dir used" $ do
-        ruleCatchesNot pipNoCacheDir        "RUN pip2 install MySQL_python --no-cache-dir"
-        onBuildRuleCatchesNot pipNoCacheDir "RUN pip2 install MySQL_python --no-cache-dir"
-      it "pip3 --no-cache-dir used" $ do
-        ruleCatchesNot pipNoCacheDir        "RUN pip3 install --no-cache-dir MySQL_python"
-        onBuildRuleCatchesNot pipNoCacheDir "RUN pip3 install --no-cache-dir MySQL_python"
-      it "pip --no-cache-dir used" $ do
-        ruleCatchesNot pipNoCacheDir        "RUN pip install MySQL_python --no-cache-dir"
-        onBuildRuleCatchesNot pipNoCacheDir "RUN pip install MySQL_python --no-cache-dir"
-      it "don't match on pipx" $ do
-        ruleCatchesNot pipNoCacheDir        "RUN pipx install software"
-        onBuildRuleCatchesNot pipNoCacheDir "Run pipx install software"
-      it "don't match on pipenv" $ do
-        ruleCatchesNot pipNoCacheDir        "RUN pipenv install library"
-        onBuildRuleCatchesNot pipNoCacheDir "RUN pipenv install library"
-    --
-    describe "npm pinning" $ do
-      it "version pinned in package.json" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install"
-      it "version pinned in package.json with arguments" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install --progress=false"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install --progress=false"
-      it "version pinned" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"
-      it "version pinned with scope" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""
-      it "version pinned multiple packages" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"
-      it "version pinned with --global" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""
-      it "version pinned with -g" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""
-      it "version does not have to be pinned for tarball suffix .tar" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"
-      it "version does not have to be pinned for tarball suffix .tar.gz" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"
-      it "version does not have to be pinned for tarball suffix .tgz" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"
-      it "version does not have to be pinned for folder - absolute path" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install /folder"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install /folder"
-      it "version does not have to be pinned for folder - relative path from current folder" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install ./folder"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ./folder"
-      it "version does not have to be pinned for folder - relative path to parent folder" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install ../folder"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ../folder"
-      it "version does not have to be pinned for folder - relative path from home" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install ~/folder"
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ~/folder"
-      it "commit pinned for git+ssh" $ do
-        ruleCatchesNot
-          npmVersionPinned
-          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
-        onBuildRuleCatchesNot
-          npmVersionPinned
-          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"
-      it "commit pinned for git+http" $ do
-        ruleCatchesNot
-          npmVersionPinned
-          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
-        onBuildRuleCatchesNot
-          npmVersionPinned
-          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"
-      it "commit pinned for git+https" $ do
-        ruleCatchesNot
-          npmVersionPinned
-          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
-        onBuildRuleCatchesNot
-          npmVersionPinned
-          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"
-      it "commit pinned for git" $ do
-        ruleCatchesNot
-          npmVersionPinned
-          "RUN npm install git://github.com/npm/npm.git#v1.0.27"
-        onBuildRuleCatchesNot
-          npmVersionPinned
-          "RUN npm install git://github.com/npm/npm.git#v1.0.27"
-      it "npm run install is fine" $ do
-        ruleCatchesNot
-          npmVersionPinned
-          "RUN npm run --crazy install"
-        onBuildRuleCatchesNot
-          npmVersionPinned
-          "RUN npm run --crazy install"
-
-      --version range is not supported
-      it "version pinned with scope" $ do
-        ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
-        onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""
-      it "version not pinned" $ do
-        ruleCatches npmVersionPinned "RUN npm install express"
-        onBuildRuleCatches npmVersionPinned "RUN npm install express"
-      it "version not pinned with scope" $ do
-        ruleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"
-        onBuildRuleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"
-      it "version not pinned multiple packages" $ do
-        ruleCatches npmVersionPinned "RUN npm install express sax@0.1.1"
-        onBuildRuleCatches npmVersionPinned "RUN npm install express sax@0.1.1"
-      it "version not pinned with --global" $ do
-        ruleCatches npmVersionPinned "RUN npm install --global express"
-        onBuildRuleCatches npmVersionPinned "RUN npm install --global express"
-      it "commit not pinned for git+ssh" $ do
-        ruleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"
-        onBuildRuleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"
-      it "commit not pinned for git+http" $ do
-        ruleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"
-        onBuildRuleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"
-      it "commit not pinned for git+https" $ do
-        ruleCatches
-          npmVersionPinned
-          "RUN npm install git+https://isaacs@github.com/npm/npm.git"
-        onBuildRuleCatches
-          npmVersionPinned
-          "RUN npm install git+https://isaacs@github.com/npm/npm.git"
-      it "commit not pinned for git" $ do
-        ruleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"
-        onBuildRuleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"
-    --
-    describe "use SHELL" $ do
-      it "RUN ln" $ do
-        ruleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"
-        onBuildRuleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"
-      it "RUN ln with unrelated symlinks" $ do
-        ruleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"
-        onBuildRuleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"
-      it "RUN ln with multiple acceptable commands" $ do
-        ruleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
-        onBuildRuleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
-    --
-    --
-    describe "Shellcheck" $ do
-      it "runs shellchek on RUN instructions" $ do
-        ruleCatches shellcheck "RUN echo $MISSING_QUOTES"
-        onBuildRuleCatches shellcheck "RUN echo $MISSING_QUOTES"
-      it "not warns on valid scripts" $ do
-        ruleCatchesNot shellcheck "RUN echo foo"
-        onBuildRuleCatchesNot shellcheck "RUN echo foo"
-
-      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
-              ruleCatchesNot shellcheck dockerFile
-              onBuildRuleCatchesNot shellcheck dockerFile
-
-      it "Complain on missing env vars" $
-        let dockerFile =
-              Text.unlines
-                [ "RUN echo \"$RTTP_PROXY\""
-                ]
-         in do
-              ruleCatches shellcheck dockerFile
-              onBuildRuleCatches shellcheck dockerFile
-
-      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
-              ruleCatchesNot shellcheck dockerFile
-              onBuildRuleCatchesNot shellcheck dockerFile
-
-      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
-              ruleCatches shellcheck dockerFile
-              onBuildRuleCatches shellcheck dockerFile
-
-      it "Defaults the shell to sh" $
-        let dockerFile =
-              Text.unlines
-                [ "RUN echo $RANDOM"
-                ]
-         in do
-              ruleCatches shellcheck dockerFile
-              onBuildRuleCatches shellcheck dockerFile
-
-      it "Can change the shell check to bash" $
-        let dockerFile =
-              Text.unlines
-                [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",
-                  "RUN echo $RANDOM"
-                ]
-         in do
-              ruleCatchesNot shellcheck dockerFile
-              onBuildRuleCatchesNot shellcheck dockerFile
-
-      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
-              ruleCatches shellcheck dockerFile
-              onBuildRuleCatches shellcheck dockerFile
-
-      it "Does not complain on ash shell" $
-        let dockerFile =
-              Text.unlines
-                [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]",
-                  "RUN echo hello"
-                ]
-         in do
-              ruleCatchesNot shellcheck dockerFile
-              onBuildRuleCatchesNot shellcheck dockerFile
-
-      it "Does not complain on powershell" $
-        let dockerFile =
-              Text.unlines
-                [ "SHELL [\"pwsh\", \"-c\"]",
-                  "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"
-                ]
-         in do
-              ruleCatchesNot shellcheck dockerFile
-              onBuildRuleCatchesNot shellcheck dockerFile
-    --
-    --
-    describe "COPY rules" $ do
-      it "use add" $ ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"
-      it "use not add" $ ruleCatchesNot useAdd "COPY package.json /usr/src/app"
-    --
-    describe "other rules" $ do
-      it "apt-get auto yes" $ do
-        ruleCatches aptGetYes "RUN apt-get install python"
-        onBuildRuleCatches aptGetYes "RUN apt-get install python"
-      it "apt-get yes shortflag" $ do
-        ruleCatchesNot aptGetYes "RUN apt-get install -yq python"
-        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -yq python"
-      it "apt-get yes quiet level 2 implies -y" $ do
-        ruleCatchesNot aptGetYes "RUN apt-get install -qq python"
-        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -qq python"
-      it "apt-get yes different pos" $ do
-        ruleCatchesNot aptGetYes "RUN apt-get install -y python"
-        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -y python"
-      it "apt-get with auto yes" $ do
-        ruleCatchesNot aptGetYes "RUN apt-get -y install python"
-        onBuildRuleCatchesNot aptGetYes "RUN apt-get -y install python"
-      it "apt-get with auto expanded yes" $ do
-        ruleCatchesNot aptGetYes "RUN apt-get --yes install python"
-        onBuildRuleCatchesNot aptGetYes "RUN apt-get --yes install python"
-      it "apt-get with assume-yes" $ do
-        ruleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"
-        onBuildRuleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"
-      it "apt-get install recommends" $ do
-        ruleCatchesNot
-          aptGetNoRecommends
-          "RUN apt-get install --no-install-recommends python"
-        onBuildRuleCatchesNot
-          aptGetNoRecommends
-          "RUN apt-get install --no-install-recommends python"
-      it "apt-get no install recommends" $ do
-        ruleCatches aptGetNoRecommends "RUN apt-get install python"
-        onBuildRuleCatches aptGetNoRecommends "RUN apt-get install python"
-      it "apt-get no install recommends" $ do
-        ruleCatches aptGetNoRecommends "RUN apt-get -y install python"
-        onBuildRuleCatches aptGetNoRecommends "RUN apt-get -y install python"
-      it "apt-get no install recommends via option" $ do
-        ruleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"
-        onBuildRuleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"
-      it "apt-get version" $ do
-        ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
-        onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
-      it "apt-get version" $ do
-        ruleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
-        onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"
-      it "apt-get pinned" $ do
-        ruleCatchesNot
-          aptGetVersionPinned
-          "RUN apt-get -y --no-install-recommends install nodejs=0.10"
-        onBuildRuleCatchesNot
-          aptGetVersionPinned
-          "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 aptGetVersionPinned $ Text.unlines dockerFile
-              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile
-
-      it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas"
-      it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN"
-      it "has no maintainer" $ ruleCatchesNot hasNoMaintainer "FROM debian"
-      it "using add" $ ruleCatches copyInsteadAdd "ADD file /usr/src/app/"
-
-      it "many cmds" $
-        let dockerFile =
-              [ "FROM debian",
-                "CMD bash",
-                "RUN foo",
-                "CMD another"
-              ]
-         in ruleCatches multipleCmds $ 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 multipleCmds $ 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 multipleCmds $ Text.unlines dockerFile
-
-      it "single cmd" $ ruleCatchesNot multipleCmds "CMD /bin/true"
-      it "no cmd" $ ruleCatchesNot multipleEntrypoints "FROM busybox"
-
-      it "many entrypoints" $
-        let dockerFile =
-              [ "FROM debian",
-                "ENTRYPOINT bash",
-                "RUN foo",
-                "ENTRYPOINT another"
-              ]
-         in ruleCatches multipleEntrypoints $ 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 multipleEntrypoints $ 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 multipleEntrypoints $ Text.unlines dockerFile
-
-      it "single entry" $ ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true"
-      it "no entry" $ ruleCatchesNot multipleEntrypoints "FROM busybox"
-      it "workdir relative" $ ruleCatches absoluteWorkdir "WORKDIR relative/dir"
-      it "workdir absolute" $ ruleCatchesNot absoluteWorkdir "WORKDIR /usr/local"
-      it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"
-      it "workdir relative single quotes" $ ruleCatches absoluteWorkdir "WORKDIR \'relative/dir\'"
-      it "workdir absolute single quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \'/usr/local\'"
-      -- no test for variable/single quotes since the variable would not expand.
-      it "workdir relative double quotes" $ ruleCatches absoluteWorkdir "WORKDIR \"relative/dir\""
-      it "workdir absolute double quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \"/usr/local\""
-      it "workdir variable double quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \"${dir}\""
-      it "scratch" $ ruleCatchesNot noUntagged "FROM scratch"
-    --
-    describe "add files and archives" $ do
-      it "add for tar" $ ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/"
-      it "add for zip" $ ruleCatchesNot copyInsteadAdd "ADD file.zip /usr/src/app/"
-      it "add for gzip" $ ruleCatchesNot copyInsteadAdd "ADD file.gz /usr/src/app/"
-      it "add for bz2" $ ruleCatchesNot copyInsteadAdd "ADD file.bz2 /usr/src/app/"
-      it "add for xz" $ ruleCatchesNot copyInsteadAdd "ADD file.xz /usr/src/app/"
-      it "add for tgz" $ ruleCatchesNot copyInsteadAdd "ADD file.tgz /usr/src/app/"
-      it "add for url" $ ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/"
-    --
-    describe "copy last argument" $ do
-      it "no warn on 2 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar"
-      it "warn on 3 args" $ ruleCatches copyEndingSlash "COPY foo bar baz"
-      it "no warn on 3 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar baz/"
-    --
-    describe "copy from existing alias" $ do
-      it "warn on missing alias" $ ruleCatches copyFromExists "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 copyFromExists $ 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 copyFromExists $ 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 copyFromAnother $ Text.unlines dockerFile
-      it "don't warn on copying form other sources" $
-        let dockerFile =
-              [ "FROM scratch as build",
-                "RUN foo",
-                "FROM node as run",
-                "COPY --from=build foo .",
-                "RUN baz"
-              ]
-         in ruleCatchesNot copyFromAnother $ 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 fromAliasUnique $ 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 fromAliasUnique $ 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 noRootUser $ Text.unlines dockerFile
-      it "ignores only the given rule" $
-        let dockerFile =
-              [ "FROM scratch",
-                "# hadolint ignore=DL3001",
-                "USER root"
-              ]
-         in ruleCatches noRootUser $ Text.unlines dockerFile
-      it "ignores only the given rule, when multiple passed" $
-        let dockerFile =
-              [ "FROM scratch",
-                "# hadolint ignore=DL3001,DL3002",
-                "USER root"
-              ]
-         in ruleCatchesNot noRootUser $ 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 noRootUser $ 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 noRootUser $ 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 copyFromAnother $ Text.unlines dockerFile
-              ruleCatchesNot copyEndingSlash $ 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 useJsonArgs $ Text.unlines dockerFile
-      it "don't warn on ENTRYPOINT json notation" $
-        let dockerFile =
-              [ "FROM scratch as build",
-                "ENTRYPOINT [\"foo\", \"bar\"]"
-              ]
-         in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile
-      it "warn on CMD" $
-        let dockerFile =
-              [ "FROM node as foo",
-                "CMD something"
-              ]
-         in ruleCatches useJsonArgs $ 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 useJsonArgs $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ 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 usePipefail $ Text.unlines dockerFile
-    --
-    describe "Allowed docker registries" $ do
-      it "warn on non-allowed registry" $
-        let dockerFile =
-              [ "FROM random.com/debian"
-              ]
-         in ruleCatches (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
-      it "don't warn on empty allowed registries" $
-        let dockerFile =
-              [ "FROM random.com/debian"
-              ]
-         in ruleCatchesNot (registryIsAllowed []) $ Text.unlines dockerFile
-      it "don't warn on allowed registries" $
-        let dockerFile =
-              [ "FROM random.com/debian"
-              ]
-         in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
-      it "doesn't warn on scratch image" $
-        let dockerFile =
-              [ "FROM scratch"
-              ]
-         in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
-      it "allows boths all forms of docker.io" $
-        let dockerFile =
-              [ "FROM ubuntu:18.04 AS builder1",
-                "FROM zemanlx/ubuntu:18.04 AS builder2",
-                "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
-              ]
-         in ruleCatchesNot (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
-
-      it "allows using previous stages" $
-        let dockerFile =
-              [ "FROM random.com/foo AS builder1",
-                "FROM builder1 AS builder2"
-              ]
-         in ruleCatchesNot (registryIsAllowed ["random.com"]) $ 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 wgetOrCurl $ 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 wgetOrCurl $ Text.unlines dockerFile
-      it "does not warn when using only wget" $
-        let dockerFile =
-              [ "FROM node as foo",
-                "RUN wget my.xyz"
-              ]
-         in ruleCatchesNot wgetOrCurl $ 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 wgetOrCurl $ 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 wgetOrCurl $ 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
-              wgetOrCurl
-              (Text.unlines dockerFile)
-              ( \checks ->
-                  assertBool
-                    "Expecting warnings only in 1 RUN instruction"
-                    (length checks == 1)
-              )
-      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
-              wgetOrCurl
-              (Text.unlines dockerFile)
-              ( \checks ->
-                  assertBool
-                    "Expecting warnings only in 2 RUN instructions"
-                    (length checks == 2)
-              )
-    --
-    describe "ONBUILD" $ do
-      it "error when using `ONBUILD` within `ONBUILD`" $
-        let dockerFile =
-              [ "ONBUILD ONBUILD RUN anything"
-              ]
-        in ruleCatches noIllegalInstructionInOnbuild $ Text.unlines dockerFile
-      it "error when using `FROM` within `ONBUILD`" $
-        let dockerFile =
-              [ "ONBUILD FROM debian:buster"
-              ]
-        in ruleCatches noIllegalInstructionInOnbuild $ Text.unlines dockerFile
-      it "error when using `MAINTAINER` within `ONBUILD`" $
-        let dockerFile =
-              [ "ONBUILD MAINTAINER \"BoJack Horseman\""
-              ]
-        in ruleCatches noIllegalInstructionInOnbuild $ Text.unlines dockerFile
-      it "ok with `ADD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ADD anything anywhere"
-      it "ok with `USER`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD USER anything"
-      it "ok with `LABEL`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD LABEL bla=\"blubb\""
-      it "ok with `STOPSIGNAL`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD STOPSIGNAL anything"
-      it "ok with `COPY`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD COPY anything anywhere"
-      it "ok with `RUN`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD RUN anything"
-      it "ok with `CMD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD CMD anything"
-      it "ok with `SHELL`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD SHELL anything"
-      it "ok with `WORKDIR`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD WORKDIR anything"
-      it "ok with `EXPOSE`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD EXPOSE 69"
-      it "ok with `VOLUME`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD VOLUME anything"
-      it "ok with `ENTRYPOINT`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ENTRYPOINT anything"
-      it "ok with `ENV`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ENV MYVAR=\"bla\""
-      it "ok with `ARG`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ARG anything"
-      it "ok with `HEALTHCHECK`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD HEALTHCHECK NONE"
-      it "ok with `FROM` outside of `ONBUILD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "FROM debian:buster"
-      it "ok with `MAINTAINER` outside of `ONBUILD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "MAINTAINER \"Some Guy\""
-    --
-    describe "Selfreferencing `ENV`s" $ do
-      it "ok with normal ENV" $
-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\"\nENV BLUBB=\"${BLA}/blubb\""
-      it "ok with partial match 1" $
-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"${FOOBLA}/blubb\""
-      it "ok with partial match 2" $
-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"${BLAFOO}/blubb\""
-      it "ok with partial match 3" $
-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$FOOBLA/blubb\""
-      it "ok with partial match 4" $
-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$BLAFOO/blubb\""
-      it "fail with partial match 5" $
-        ruleCatches noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$BLA/$BLAFOO/blubb\""
-      it "ok when previously defined in `ARG`" $
-        ruleCatchesNot noSelfreferencingEnv "ARG BLA\nENV BLA=${BLA}"
-      it "ok when previously defined in `ENV`" $
-        ruleCatchesNot noSelfreferencingEnv "ENV BLA blubb\nENV BLA=${BLA}"
-      it "ok with referencing a variable on its own right hand side" $
-        ruleCatchesNot noSelfreferencingEnv "ENV PATH=/bla:${PATH}"
-      it "ok with referencing a variable on its own right side twice in different `ENV`s" $
-        ruleCatchesNot noSelfreferencingEnv "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 noSelfreferencingEnv "ENV PATH=/bla:${PATH} PATH=/blubb:${PATH}"
-      it "fail with selfreferencing with curly braces ENV" $
-        ruleCatches noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"${BLA}/blubb\""
-      it "fail with selfreferencing without curly braces ENV" $
-        ruleCatches noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$BLA/blubb\""
-    --
-    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 usePipefail $ Text.unlines dockerFile
-      it "`ARG` can correctly unset variables" $
-        let dockerFile =
-              [ "ARG A_WITHOUT_EQ",
-                "ARG A_WITH_EQ=",
-                "RUN echo bla"
-              ]
-         in assertChecks
-              shellcheck
-              (Text.unlines dockerFile)
-              (assertBool "No Warnings or Errors should be triggered" . null)
-
-    -- Run tests for the Config module
-    ConfigSpec.tests
-
-assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
-assertChecks rule s makeAssertions =
-  case parseText (s <> "\n") of
-    Left err -> assertFailure $ show err
-    Right dockerFile -> makeAssertions $ analyze [rule] dockerFile
-
-assertOnBuildChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
-assertOnBuildChecks rule s makeAssertions =
-  case parseText (s <> "\n") of
-    Left err -> assertFailure $ show err
-    Right dockerFile -> checkOnBuild dockerFile
-  where
-    checkOnBuild dockerFile = makeAssertions $ analyze [rule] (fmap wrapInOnBuild dockerFile)
-    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li
-    wrapInOnBuild i = i
-
-selectChecksWithLines :: [RuleCheck] -> [RuleCheck]
-selectChecksWithLines checks = [c | c <- checks, linenumber c <= 0]
-
--- Assert a failed check exists for rule
-ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion
-ruleCatches rule s = assertChecks rule s f
-  where
-    f checks = do
-      when (null checks) $
-        assertFailure "I was expecting to catch at least one error"
-      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks
-
-onBuildRuleCatches :: HasCallStack => Rule -> Text.Text -> Assertion
-onBuildRuleCatches rule s = assertOnBuildChecks rule s f
-  where
-    f checks = do
-      when (length checks /= 1) $
-        assertFailure (Text.unpack . Text.unlines . formatChecksNoColor $ checks)
-      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks
-
-ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
-ruleCatchesNot rule s = assertChecks rule s f
-  where
-    f checks =
-      unless (null checks) $
-        assertFailure $
-          "Not expecting the following errors: \n"
-            ++ (Text.unpack . Text.unlines . formatChecksNoColor $ checks)
-
-onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
-onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f
-  where
-    f checks =
-      unless (null checks) $
-        assertFailure $
-          "Not expecting the following errors: \n"
-            ++ (Text.unpack . Text.unlines . formatChecksNoColor $ checks)
-
-formatChecksNoColor :: Functor f => f RuleCheck -> f Text.Text
-formatChecksNoColor checks = formatChecks checks False
+{-# 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
